VIVO-1408 Allow the use of a #-terminated prefix on JAVA class URIs

This commit is contained in:
Jim Blake 2018-01-08 11:19:53 -05:00
parent 8fbfc6d4ff
commit 7f8b16bc1b
5 changed files with 253 additions and 72 deletions

View file

@ -8,6 +8,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
@ -34,16 +35,42 @@ public class ConfigurationBeanLoader {
return JAVA_URI_PREFIX + clazz.getName();
}
public static String toCanonicalJavaUri(String uri) {
return uri.replace("#", ".");
}
public static boolean isJavaUri(String uri) {
return uri.startsWith(JAVA_URI_PREFIX);
}
public static String fromJavaUri(String uri) {
if (!isJavaUri(uri)) {
throw new IllegalArgumentException("Not a java class URI: '" + uri
+ "'");
public static Set<String> toPossibleJavaUris(Class<?> clazz) {
Set<String> set = new TreeSet<>();
String[] uriPieces = toJavaUri(clazz).split("\\.");
for (int hashIndex = 0; hashIndex < uriPieces.length; hashIndex++) {
set.add(joinWithPeriodsAndAHash(uriPieces, hashIndex));
}
return uri.substring(JAVA_URI_PREFIX.length());
return set;
}
private static String joinWithPeriodsAndAHash(String[] pieces,
int hashIndex) {
StringBuilder buffer = new StringBuilder(pieces[0]);
for (int i = 1; i < pieces.length; i++) {
buffer.append(i == hashIndex ? '#' : '.').append(pieces[i]);
}
return buffer.toString();
}
public static String classnameFromJavaUri(String uri) {
if (!isJavaUri(uri)) {
throw new IllegalArgumentException(
"Not a java class URI: '" + uri + "'");
}
return toCanonicalJavaUri(uri).substring(JAVA_URI_PREFIX.length());
}
public static boolean isMatchingJavaUri(String uri1, String uri2) {
return toCanonicalJavaUri(uri1).equals(toCanonicalJavaUri(uri2));
}
// ----------------------------------------------------------------------
@ -85,9 +112,11 @@ public class ConfigurationBeanLoader {
this(new LockableModel(model), req);
}
public ConfigurationBeanLoader(LockableModel locking, HttpServletRequest req) {
this(locking, (req == null) ? null : req.getSession()
.getServletContext(), req);
public ConfigurationBeanLoader(LockableModel locking,
HttpServletRequest req) {
this(locking,
(req == null) ? null : req.getSession().getServletContext(),
req);
}
private ConfigurationBeanLoader(LockableModel locking, ServletContext ctx,
@ -111,18 +140,18 @@ public class ConfigurationBeanLoader {
}
try {
ConfigurationRdf<T> parsedRdf = ConfigurationRdfParser.parse(
locking, uri, resultClass);
WrappedInstance<T> wrapper = InstanceWrapper.wrap(parsedRdf
.getConcreteClass());
ConfigurationRdf<T> parsedRdf = ConfigurationRdfParser
.parse(locking, uri, resultClass);
WrappedInstance<T> wrapper = InstanceWrapper
.wrap(parsedRdf.getConcreteClass());
wrapper.satisfyInterfaces(ctx, req);
wrapper.checkCardinality(parsedRdf.getPropertyStatements());
wrapper.setProperties(this, parsedRdf.getPropertyStatements());
wrapper.validate();
return wrapper.getInstance();
} catch (Exception e) {
throw new ConfigurationBeanLoaderException("Failed to load '" + uri
+ "'", e);
throw new ConfigurationBeanLoaderException(
"Failed to load '" + uri + "'", e);
}
}
@ -133,14 +162,16 @@ public class ConfigurationBeanLoader {
throws ConfigurationBeanLoaderException {
Set<String> uris = new HashSet<>();
try (LockedModel m = locking.read()) {
for (String typeUri : toPossibleJavaUris(resultClass)) {
List<Resource> resources = m.listResourcesWithProperty(RDF.type,
createResource(toJavaUri(resultClass))).toList();
createResource(typeUri)).toList();
for (Resource r : resources) {
if (r.isURIResource()) {
uris.add(r.getURI());
}
}
}
}
Set<T> instances = new HashSet<>();
for (String uri : uris) {

View file

@ -2,16 +2,18 @@
package edu.cornell.mannlib.vitro.webapp.utils.configuration;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.classnameFromJavaUri;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.isJavaUri;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.isMatchingJavaUri;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri;
import static org.apache.jena.rdf.model.ResourceFactory.createResource;
import static org.apache.jena.rdf.model.ResourceFactory.createStatement;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.fromJavaUri;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.isJavaUri;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import org.apache.jena.rdf.model.Property;
@ -64,23 +66,30 @@ public class ConfigurationRdfParser {
private static void confirmEligibilityForResultClass(LockableModel locking,
String uri, Class<?> resultClass)
throws InvalidConfigurationRdfException {
Statement s = createStatement(createResource(uri), RDF.type,
createResource(toJavaUri(resultClass)));
String resultClassUri = toJavaUri(resultClass);
try (LockedModel m = locking.read()) {
if (!m.contains(s)) {
throw noTypeStatementForResultClass(s);
Set<RDFNode> types = //
m.listObjectsOfProperty(createResource(uri), RDF.type)
.toSet();
for (RDFNode typeNode : types) {
if (typeNode.isURIResource()) {
String typeUri = typeNode.asResource().getURI();
if (isMatchingJavaUri(resultClassUri, typeUri)) {
return;
}
}
}
throw noTypeStatementForResultClass(uri, resultClassUri);
}
}
private static Set<PropertyStatement> loadProperties(LockableModel locking,
String uri) throws InvalidConfigurationRdfException {
Set<PropertyStatement> set = new HashSet<>();
try (LockedModel m = locking.read()) {
List<Statement> rawStatements = m.listStatements(
m.getResource(uri), (Property) null, (RDFNode) null)
.toList();
List<Statement> rawStatements = m.listStatements(m.getResource(uri),
(Property) null, (RDFNode) null).toList();
if (rawStatements.isEmpty()) {
throw noRdfStatements(uri);
}
@ -108,8 +117,9 @@ public class ConfigurationRdfParser {
Set<Class<? extends T>> concreteClasses = new HashSet<>();
try (LockedModel m = locking.read()) {
for (RDFNode node : m.listObjectsOfProperty(createResource(uri),
RDF.type).toSet()) {
for (RDFNode node : m
.listObjectsOfProperty(createResource(uri), RDF.type)
.toSet()) {
if (!node.isURIResource()) {
throw typeMustBeUriResource(node);
}
@ -140,7 +150,7 @@ public class ConfigurationRdfParser {
if (!isJavaUri(typeUri)) {
return false;
}
Class<?> clazz = Class.forName(fromJavaUri(typeUri));
Class<?> clazz = Class.forName(classnameFromJavaUri(typeUri));
if (clazz.isInterface()) {
return false;
}
@ -157,7 +167,7 @@ public class ConfigurationRdfParser {
private static <T> Class<? extends T> processTypeUri(String typeUri,
Class<T> resultClass) throws InvalidConfigurationRdfException {
try {
Class<?> clazz = Class.forName(fromJavaUri(typeUri));
Class<?> clazz = Class.forName(classnameFromJavaUri(typeUri));
if (!resultClass.isAssignableFrom(clazz)) {
throw notAssignable(resultClass, clazz);
}
@ -180,22 +190,23 @@ public class ConfigurationRdfParser {
"The model contains no statements about '" + uri + "'");
}
private static InvalidConfigurationRdfException noConcreteClasses(String uri) {
private static InvalidConfigurationRdfException noConcreteClasses(
String uri) {
return new InvalidConfigurationRdfException(
"No concrete class is declared for '" + uri + "'");
}
private static InvalidConfigurationRdfException tooManyConcreteClasses(
String uri, Set<?> concreteClasses) {
return new InvalidConfigurationRdfException("'" + uri
+ "' is declared with more than one " + "concrete class: "
+ concreteClasses);
return new InvalidConfigurationRdfException(
"'" + uri + "' is declared with more than one "
+ "concrete class: " + concreteClasses);
}
private static InvalidConfigurationRdfException notAssignable(
Class<?> resultClass, Class<?> clazz) {
return new InvalidConfigurationRdfException(clazz
+ " cannot be assigned to " + resultClass);
return new InvalidConfigurationRdfException(
clazz + " cannot be assigned to " + resultClass);
}
private static InvalidConfigurationRdfException noZeroArgumentConstructor(
@ -212,8 +223,8 @@ public class ConfigurationRdfParser {
private static InvalidConfigurationRdfException failedToLoadClass(
String typeUri, Throwable e) {
return new InvalidConfigurationRdfException("Can't load this type: '"
+ typeUri + "'", e);
return new InvalidConfigurationRdfException(
"Can't load this type: '" + typeUri + "'", e);
}
private static InvalidConfigurationRdfException typeMustBeUriResource(
@ -223,14 +234,17 @@ public class ConfigurationRdfParser {
}
private static InvalidConfigurationRdfException noTypeStatementForResultClass(
Statement s) {
String uri, String resultClassUri) {
return new InvalidConfigurationRdfException(
"A type statement is required: '" + s);
"A type statement is required: '"
+ createStatement(createResource(uri), RDF.type,
createResource(resultClassUri)));
}
private static InvalidConfigurationRdfException noRdfStatements(String uri) {
return new InvalidConfigurationRdfException("'" + uri
+ "' does not appear as the subject of any "
private static InvalidConfigurationRdfException noRdfStatements(
String uri) {
return new InvalidConfigurationRdfException(
"'" + uri + "' does not appear as the subject of any "
+ "statements in the model.");
}
@ -239,7 +253,8 @@ public class ConfigurationRdfParser {
super(message);
}
public InvalidConfigurationRdfException(String message, Throwable cause) {
public InvalidConfigurationRdfException(String message,
Throwable cause) {
super(message, cause);
}
}

View file

@ -95,8 +95,36 @@ The principal methods are:
+ Search the graph for all individuals of type `resultClass`. For each such individual, call `loadInstance`.
Return a set containing the created instances. If no individuals are found, return an empty `Set`.
### Specifying Java class URIs
Java classes are specified as types in the configurations. The type URIs consist of `java:` and the fully-qualified class path and name. For example,
```
:application
a <java:edu.cornell.mannlib.vitro.webapp.application.ApplicationImpl> .
```
It would be nice to use prefixes to make URIs more readable. This doesn't
work with the scheme above, since none of the characters in the URI are valid
as delimiters of a prefix.
For this reason, the loader will also recognize a type URI if one of the periods is replaced by a hash (`#`). So, this is equivalent to the previous example (note the `#` after `webapp`):
```
:application
a <java:edu.cornell.mannlib.vitro.webapp#application.ApplicationImpl> .
```
which implies that this is equivalent also:
```
@prefix javaWebapp: <java:edu.cornell.mannlib.vitro.webapp#>
:application a javaWebapp:application.ApplicationImpl .
```
### Restrictions on instantiated classes.
Each class to be instantiated must have a niladic constructor.
Each class to be instantiated must have a public niladic constructor.
### Property methods
When the loader encounters a data property or an object property in a description,
@ -116,7 +144,8 @@ For example:
In more detail:
+ A class must contain exactly one method that serves each property URI in the description.
+ Each property URI in the description may be served by only one method in the class.
+ If a property URI in the description is not served by any method in the class, the loader will ignore that property.
+ The description need not include properies for all of the property methods in the class.
+ Each property method must be public, must have exactly one parameter, and must return null.
+ The name of the property method is immaterial, except that there must not be another method
@ -159,7 +188,7 @@ Again, in detail:
+ Each validation method must be public, must accept no parameters, and must return null.
+ The name of the validation method is immaterial, except that there must not be another
+ method with the same name in the lass.
+ method with the same name in the class.
+ Validation methods in superclasses will be called, but may not be overridden in a subclass.
### Life cycle

View file

@ -0,0 +1,111 @@
/* $This file is distributed under the terms of the license in /doc/license.txt$ */
package edu.cornell.mannlib.vitro.webapp.utils.configuration;
import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.typeStatement;
import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toPossibleJavaUris;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import org.junit.Test;
/**
* Assure that we can use "namespaces" for Java URIs. The namespace must end
* with a '#'.
*/
public class ConfigurationBeanLoader_NamespacesTest
extends ConfigurationBeanLoaderTestBase {
// ----------------------------------------------------------------------
// toPossibleJavaUris()
// ----------------------------------------------------------------------
@Test
public void possibleForJavaLangString() {
Set<String> expected = new HashSet<>();
expected.add("java:java.lang.String");
expected.add("java:java#lang.String");
expected.add("java:java.lang#String");
assertEquals(expected, toPossibleJavaUris(String.class));
}
// ----------------------------------------------------------------------
// loadAll()
// ----------------------------------------------------------------------
@Test
public void loadAllForJavaUtilRandom()
throws ConfigurationBeanLoaderException {
model.add(typeStatement("http://noPound", "java:java.util.Random"));
model.add(typeStatement("http://firstPound", "java:java#util.Random"));
model.add(typeStatement("http://secondPound", "java:java.util#Random"));
model.add(typeStatement("http://notARandom", "java:java.util.Set"));
Set<Random> instances = loader.loadAll(Random.class);
assertEquals(3, instances.size());
}
@Test
public void loadAlForCustomInnerClass()
throws ConfigurationBeanLoaderException {
Set<String> typeUris = toPossibleJavaUris(ExampleClassForLoadAll.class);
for (String typeUri : typeUris) {
model.add(typeStatement("http://testUri" + model.size(), typeUri));
}
Set<ExampleClassForLoadAll> instances = loader
.loadAll(ExampleClassForLoadAll.class);
assertEquals(typeUris.size(), instances.size());
}
public static class ExampleClassForLoadAll {
// Nothing of interest
}
// ----------------------------------------------------------------------
// loadInstance()
// ----------------------------------------------------------------------
@Test
public void loadInstanceVariationsForJavaUtilRandom()
throws ConfigurationBeanLoaderException {
model.add(typeStatement("http://noPound", "java:java.util.Random"));
model.add(typeStatement("http://firstPound", "java:java#util.Random"));
model.add(typeStatement("http://secondPound", "java:java.util#Random"));
model.add(typeStatement("http://notARandom", "java:java.util.Set"));
assertNotNull(loader.loadInstance("http://noPound", Random.class));
assertNotNull(loader.loadInstance("http://firstPound", Random.class));
assertNotNull(loader.loadInstance("http://secondPound", Random.class));
try {
loader.loadInstance("http://notARandom", Random.class);
fail("Should not be a Random");
} catch (Exception e) {
// Expected it
}
}
@Test
public void loadInstanceVariationsForCustomInnerClass()
throws ConfigurationBeanLoaderException {
Set<String> typeUris = toPossibleJavaUris(
ExampleClassForLoadInstance.class);
for (String typeUri : typeUris) {
model.add(typeStatement("http://testUri" + model.size(), typeUri));
}
for (int i = 0; i < model.size(); i++) {
String instanceUri = "http://testUri" + i;
assertNotNull("No instance for " + instanceUri, loader.loadInstance(
instanceUri, ExampleClassForLoadInstance.class));
}
}
public static class ExampleClassForLoadInstance {
// Nothing of interest
}
}

View file

@ -15,8 +15,8 @@ import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.Vali
/**
* Test the @Validation annotation.
*/
public class ConfigurationBeanLoader_ValidationTest extends
ConfigurationBeanLoaderTestBase {
public class ConfigurationBeanLoader_ValidationTest
extends ConfigurationBeanLoaderTestBase {
// --------------------------------------------
@Test
@ -25,8 +25,7 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(ValidationMethodWithParameter.class)));
expectSimpleFailure(
ValidationMethodWithParameter.class,
expectSimpleFailure(ValidationMethodWithParameter.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(InstanceWrapperException.class,
@ -49,11 +48,11 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(ValidationMethodShouldReturnVoid.class)));
expectSimpleFailure(
ValidationMethodShouldReturnVoid.class,
expectSimpleFailure(ValidationMethodShouldReturnVoid.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(InstanceWrapperException.class, "should return void"));
throwable(InstanceWrapperException.class,
"should return void"));
}
public static class ValidationMethodShouldReturnVoid {
@ -71,8 +70,7 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(ValidationMethodIsPrivate.class)));
expectSimpleFailure(
ValidationMethodIsPrivate.class,
expectSimpleFailure(ValidationMethodIsPrivate.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(ValidationFailedException.class,
@ -94,8 +92,7 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(ValidationThrowsException.class)));
expectSimpleFailure(
ValidationThrowsException.class,
expectSimpleFailure(ValidationThrowsException.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(ValidationFailedException.class,
@ -144,8 +141,7 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(ValidationOverValidationSubclass.class)));
expectSimpleFailure(
ValidationOverValidationSubclass.class,
expectSimpleFailure(ValidationOverValidationSubclass.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(InstanceWrapperException.class,
@ -158,8 +154,7 @@ public class ConfigurationBeanLoader_ValidationTest extends
model.add(typeStatement(GENERIC_INSTANCE_URI,
toJavaUri(PlainOverValidationSubclass.class)));
expectSimpleFailure(
PlainOverValidationSubclass.class,
expectSimpleFailure(PlainOverValidationSubclass.class,
throwable(ConfigurationBeanLoaderException.class,
"Failed to load"),
throwable(InstanceWrapperException.class,
@ -182,8 +177,8 @@ public class ConfigurationBeanLoader_ValidationTest extends
// Just want to see that the superclass validation is run.
}
public static class AdditionalValidationSubclass extends
ValidationSuperclass {
public static class AdditionalValidationSubclass
extends ValidationSuperclass {
public boolean validatorSubHasRun = false;
@Validation
@ -195,8 +190,8 @@ public class ConfigurationBeanLoader_ValidationTest extends
}
}
public static class ValidationOverValidationSubclass extends
EmptyValidationSubclass {
public static class ValidationOverValidationSubclass
extends EmptyValidationSubclass {
@Override
@Validation
public void validatorSuper() {
@ -204,8 +199,8 @@ public class ConfigurationBeanLoader_ValidationTest extends
}
}
public static class PlainOverValidationSubclass extends
ValidationSuperclass {
public static class PlainOverValidationSubclass
extends ValidationSuperclass {
@Override
public void validatorSuper() {
// Should fail