diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java index 9f7f603aa..8c45a2fca 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java @@ -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 toPossibleJavaUris(Class clazz) { + Set 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 parsedRdf = ConfigurationRdfParser.parse( - locking, uri, resultClass); - WrappedInstance wrapper = InstanceWrapper.wrap(parsedRdf - .getConcreteClass()); + ConfigurationRdf parsedRdf = ConfigurationRdfParser + .parse(locking, uri, resultClass); + WrappedInstance 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,11 +162,13 @@ public class ConfigurationBeanLoader { throws ConfigurationBeanLoaderException { Set uris = new HashSet<>(); try (LockedModel m = locking.read()) { - List resources = m.listResourcesWithProperty(RDF.type, - createResource(toJavaUri(resultClass))).toList(); - for (Resource r : resources) { - if (r.isURIResource()) { - uris.add(r.getURI()); + for (String typeUri : toPossibleJavaUris(resultClass)) { + List resources = m.listResourcesWithProperty(RDF.type, + createResource(typeUri)).toList(); + for (Resource r : resources) { + if (r.isURIResource()) { + uris.add(r.getURI()); + } } } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java index e96488f3a..4153d2295 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java @@ -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,12 +66,20 @@ 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 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); } } @@ -78,9 +88,8 @@ public class ConfigurationRdfParser { Set set = new HashSet<>(); try (LockedModel m = locking.read()) { - List rawStatements = m.listStatements( - m.getResource(uri), (Property) null, (RDFNode) null) - .toList(); + List 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> 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 Class processTypeUri(String typeUri, Class 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,15 +234,18 @@ 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 " - + "statements in the model."); + private static InvalidConfigurationRdfException noRdfStatements( + String uri) { + return new InvalidConfigurationRdfException( + "'" + uri + "' does not appear as the subject of any " + + "statements in the model."); } public static class InvalidConfigurationRdfException extends Exception { @@ -239,7 +253,8 @@ public class ConfigurationRdfParser { super(message); } - public InvalidConfigurationRdfException(String message, Throwable cause) { + public InvalidConfigurationRdfException(String message, + Throwable cause) { super(message, cause); } } diff --git a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md index 68da421fa..0527158e4 100644 --- a/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md +++ b/api/src/main/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md @@ -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 . +``` + +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 . +``` + +which implies that this is equivalent also: + +``` +@prefix javaWebapp: +: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 diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_NamespacesTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_NamespacesTest.java new file mode 100644 index 000000000..61e2b5916 --- /dev/null +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_NamespacesTest.java @@ -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 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 instances = loader.loadAll(Random.class); + assertEquals(3, instances.size()); + } + + @Test + public void loadAlForCustomInnerClass() + throws ConfigurationBeanLoaderException { + Set typeUris = toPossibleJavaUris(ExampleClassForLoadAll.class); + for (String typeUri : typeUris) { + model.add(typeStatement("http://testUri" + model.size(), typeUri)); + } + Set 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 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 + } + +} diff --git a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java index 59a57a0b0..60e66a5d2 100644 --- a/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java +++ b/api/src/test/java/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java @@ -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