From 481a2152065e24697446fae6cf9c0e9fe6d59fe4 Mon Sep 17 00:00:00 2001 From: Jim Blake Date: Sun, 29 May 2016 10:25:23 -0400 Subject: [PATCH] VIVO-1246 improve the ConfigurationBeanLoader: Add cardinality parameters minOccurs and maxOccurs Create README.md document in the edu.cornell.mannlib.vitro.webapp.utils.configuration package Split large class of unit tests into separate classes by functionality --- .../ConfigurationBeanLoader.java | 1 + .../utils/configuration/InstanceWrapper.java | 181 ++++++--- .../webapp/utils/configuration/Property.java | 2 + .../utils/configuration/PropertyType.java | 90 +++-- .../webapp/utils/configuration/README.md | 174 +++++++++ .../utils/configuration/WrappedInstance.java | 47 ++- .../testing/ModelUtilitiesTestHelper.java | 6 + .../ConfigurationBeanLoaderTest.java | 364 +----------------- .../ConfigurationBeanLoaderTestBase.java | 102 +++++ ...figurationBeanLoader_Cardinality_Test.java | 252 ++++++++++++ .../ConfigurationBeanLoader_PropertyTest.java | 339 ++++++++++++++++ ...onfigurationBeanLoader_ValidationTest.java | 215 +++++++++++ 12 files changed, 1332 insertions(+), 441 deletions(-) create mode 100644 webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTestBase.java create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_Cardinality_Test.java create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_PropertyTest.java create mode 100644 webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java index fa7249297..36d399224 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java @@ -116,6 +116,7 @@ public class ConfigurationBeanLoader { WrappedInstance wrapper = InstanceWrapper.wrap(parsedRdf .getConcreteClass()); wrapper.satisfyInterfaces(ctx, req); + wrapper.checkCardinality(parsedRdf.getPropertyStatements()); wrapper.setProperties(this, parsedRdf.getPropertyStatements()); wrapper.validate(); return wrapper.getInstance(); diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/InstanceWrapper.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/InstanceWrapper.java index c882ccfd5..45db851ad 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/InstanceWrapper.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/InstanceWrapper.java @@ -6,7 +6,9 @@ import java.lang.reflect.Method; import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyMethod; import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; @@ -16,11 +18,17 @@ import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.Propert * instance of the class. */ public class InstanceWrapper { + private static final Log log = LogFactory.getLog(InstanceWrapper.class); + public static WrappedInstance wrap(Class concreteClass) throws InstanceWrapperException { - return new WrappedInstance(createInstance(concreteClass), - parsePropertyAnnotations(concreteClass), - parseValidationAnnotations(concreteClass)); + T instance = createInstance(concreteClass); + HashSet validationMethods = new HashSet<>( + parseValidationAnnotations(concreteClass).values()); + Map propertyMethods = new PropertyAnnotationsMap( + concreteClass).byUri(); + return new WrappedInstance(instance, propertyMethods, + validationMethods); } private static T createInstance(Class concreteClass) @@ -33,58 +41,131 @@ public class InstanceWrapper { } } - private static Map parsePropertyAnnotations( - Class concreteClass) throws InstanceWrapperException { - Map map = new HashMap<>(); - for (Method method : concreteClass.getDeclaredMethods()) { - Property annotation = method.getAnnotation(Property.class); - if (annotation == null) { - continue; - } - if (!method.getReturnType().equals(Void.TYPE)) { - throw new InstanceWrapperException("Property method '" + method - + "' should return void."); - } - Class[] parameterTypes = method.getParameterTypes(); - if (parameterTypes.length != 1) { - throw new InstanceWrapperException("Property method '" + method - + "' must accept exactly one parameter."); - } - - String uri = annotation.uri(); - if (map.containsKey(uri)) { - throw new InstanceWrapperException( - "Two property methods have the same URI value: " - + map.get(uri).getMethod() + ", and " + method); - } - try { - map.put(uri, PropertyType.createPropertyMethod(method)); - } catch (PropertyTypeException e) { - throw new InstanceWrapperException( - "Failed to create the PropertyMethod", e); + private static Map parseValidationAnnotations(Class clazz) + throws InstanceWrapperException { + if (Object.class.equals(clazz)) { + return new HashMap<>(); + } else { + Map methods = parseValidationAnnotations(clazz + .getSuperclass()); + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + if (methods.containsKey(name)) { + Method m = methods.get(name); + throw new InstanceWrapperException("Method " + name + + " in " + method.getDeclaringClass().getName() + + " overrides a validation method in " + + m.getDeclaringClass().getName()); + } + if (method.getAnnotation(Validation.class) == null) { + continue; + } + if (method.getParameterTypes().length > 0) { + throw new InstanceWrapperException("Validation method '" + + method + "' should not have parameters."); + } + if (!method.getReturnType().equals(Void.TYPE)) { + throw new InstanceWrapperException("Validation method '" + + method + "' should return void."); + } + methods.put(name, method); } + return methods; } - return map; } - private static Set parseValidationAnnotations(Class concreteClass) - throws InstanceWrapperException { - Set methods = new HashSet<>(); - for (Method method : concreteClass.getDeclaredMethods()) { - if (method.getAnnotation(Validation.class) == null) { - continue; + private static class PropertyAnnotationsMap { + private Map mapByUri = new HashMap<>(); + private Map mapByName = new HashMap<>(); + + public PropertyAnnotationsMap(Class clazz) + throws InstanceWrapperException { + if (!Object.class.equals(clazz)) { + populateTheMaps(clazz); } - if (method.getParameterTypes().length > 0) { - throw new InstanceWrapperException("Validation method '" - + method + "' should not have parameters."); - } - if (!method.getReturnType().equals(Void.TYPE)) { - throw new InstanceWrapperException("Validation method '" - + method + "' should return void."); - } - methods.add(method); } - return methods; + + private void populateTheMaps(Class clazz) + throws InstanceWrapperException { + PropertyAnnotationsMap superMap = new PropertyAnnotationsMap( + clazz.getSuperclass()); + mapByUri = superMap.byUri(); + mapByName = superMap.byName(); + for (Method method : clazz.getDeclaredMethods()) { + String name = method.getName(); + + Method matchByName = methodByName(name); + if (matchByName != null) { + throw new InstanceWrapperException("Method " + name + + " in " + clazz.getName() + + " conflicts with a property method in " + + matchByName.getDeclaringClass().getName()); + } + + Property annotation = method.getAnnotation(Property.class); + if (annotation == null) { + continue; + } + + if (!method.getReturnType().equals(Void.TYPE)) { + throw new InstanceWrapperException("Property method '" + + method + "' should return void."); + } + + if (method.getParameterTypes().length != 1) { + throw new InstanceWrapperException("Property method '" + + method + "' must accept exactly one parameter."); + } + + String uri = annotation.uri(); + Method matchByUri = methodByUri(uri); + if (matchByUri != null) { + throw new InstanceWrapperException( + "Two property methods have the same URI value: " + + matchByUri + ", and " + method); + } + + if (annotation.minOccurs() < 0) { + throw new InstanceWrapperException( + "minOccurs must not be negative."); + } + + if (annotation.maxOccurs() < annotation.minOccurs()) { + throw new InstanceWrapperException( + "maxOccurs must not be less than minOccurs."); + } + + try { + PropertyMethod pm = PropertyType.createPropertyMethod( + method, annotation); + mapByUri.put(uri, pm); + mapByName.put(name, pm); + } catch (PropertyTypeException e) { + throw new InstanceWrapperException( + "Failed to create the PropertyMethod", e); + } + } + + } + + private Method methodByName(String name) { + PropertyMethod pm = mapByName.get(name); + return (pm == null) ? null : pm.getMethod(); + } + + private Method methodByUri(String name) { + PropertyMethod pm = mapByUri.get(name); + return (pm == null) ? null : pm.getMethod(); + } + + public Map byUri() { + return mapByUri; + } + + public Map byName() { + return mapByName; + } + } public static class InstanceWrapperException extends Exception { diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Property.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Property.java index 0eecb23e8..bcc60bba3 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Property.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Property.java @@ -15,4 +15,6 @@ import java.lang.annotation.Target; @Target(ElementType.METHOD) public @interface Property { String uri(); + int minOccurs() default 0; + int maxOccurs() default Integer.MAX_VALUE; } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java index 397008548..9560fd5f8 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java @@ -10,7 +10,6 @@ import java.lang.reflect.Method; import com.hp.hpl.jena.datatypes.RDFDatatype; import com.hp.hpl.jena.rdf.model.Literal; -import com.hp.hpl.jena.rdf.model.Property; import com.hp.hpl.jena.rdf.model.RDFNode; import com.hp.hpl.jena.rdf.model.Statement; @@ -25,38 +24,41 @@ public enum PropertyType { RESOURCE { @Override public PropertyStatement buildPropertyStatement(Statement s) { - return new ResourcePropertyStatement(s.getPredicate(), s + return new ResourcePropertyStatement(s.getPredicate().getURI(), s .getObject().asResource().getURI()); } @Override - protected PropertyMethod buildPropertyMethod(Method method) { - return new ResourcePropertyMethod(method); + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new ResourcePropertyMethod(method, annotation); } }, STRING { @Override public PropertyStatement buildPropertyStatement(Statement s) { - return new StringPropertyStatement(s.getPredicate(), s.getObject() - .asLiteral().getString()); + return new StringPropertyStatement(s.getPredicate().getURI(), s + .getObject().asLiteral().getString()); } @Override - protected PropertyMethod buildPropertyMethod(Method method) { - return new StringPropertyMethod(method); + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new StringPropertyMethod(method, annotation); } }, FLOAT { @Override public PropertyStatement buildPropertyStatement(Statement s) { - return new FloatPropertyStatement(s.getPredicate(), s.getObject() - .asLiteral().getFloat()); + return new FloatPropertyStatement(s.getPredicate().getURI(), s + .getObject().asLiteral().getFloat()); } @Override - protected PropertyMethod buildPropertyMethod(Method method) { - return new FloatPropertyMethod(method); + protected PropertyMethod buildPropertyMethod(Method method, + Property annotation) { + return new FloatPropertyMethod(method, annotation); } }; @@ -100,24 +102,25 @@ public enum PropertyType { return type.buildPropertyStatement(s); } - public static PropertyMethod createPropertyMethod(Method method) - throws PropertyTypeException { + public static PropertyMethod createPropertyMethod(Method method, + Property annotation) throws PropertyTypeException { Class parameterType = method.getParameterTypes()[0]; PropertyType type = PropertyType.typeForParameterType(parameterType); - return type.buildPropertyMethod(method); + return type.buildPropertyMethod(method, annotation); } protected abstract PropertyStatement buildPropertyStatement(Statement s); - protected abstract PropertyMethod buildPropertyMethod(Method method); + protected abstract PropertyMethod buildPropertyMethod(Method method, + Property annotation); public static abstract class PropertyStatement { private final PropertyType type; private final String predicateUri; - public PropertyStatement(PropertyType type, Property predicate) { + public PropertyStatement(PropertyType type, String predicateUri) { this.type = type; - this.predicateUri = predicate.getURI(); + this.predicateUri = predicateUri; } public PropertyType getType() { @@ -134,8 +137,8 @@ public enum PropertyType { public static class ResourcePropertyStatement extends PropertyStatement { private final String objectUri; - public ResourcePropertyStatement(Property predicate, String objectUri) { - super(RESOURCE, predicate); + public ResourcePropertyStatement(String predicateUri, String objectUri) { + super(RESOURCE, predicateUri); this.objectUri = objectUri; } @@ -148,8 +151,8 @@ public enum PropertyType { public static class StringPropertyStatement extends PropertyStatement { private final String string; - public StringPropertyStatement(Property predicate, String string) { - super(STRING, predicate); + public StringPropertyStatement(String predicateUri, String string) { + super(STRING, predicateUri); this.string = string; } @@ -162,8 +165,8 @@ public enum PropertyType { public static class FloatPropertyStatement extends PropertyStatement { private final float f; - public FloatPropertyStatement(Property predicate, float f) { - super(FLOAT, predicate); + public FloatPropertyStatement(String predicateUri, float f) { + super(FLOAT, predicateUri); this.f = f; } @@ -176,10 +179,23 @@ public enum PropertyType { public static abstract class PropertyMethod { protected final PropertyType type; protected final Method method; + protected final String propertyUri; + protected final int minOccurs; + protected final int maxOccurs; - public PropertyMethod(PropertyType type, Method method) { + // Add cardinality values here! Final, with getters. + public PropertyMethod(PropertyType type, Method method, + Property annotation) { this.type = type; this.method = method; + this.propertyUri = annotation.uri(); + this.minOccurs = annotation.minOccurs(); + this.maxOccurs = annotation.maxOccurs(); + checkCardinalityBounds(); + } + + private void checkCardinalityBounds() { + // This is where we check for negative values or out of order. } public Method getMethod() { @@ -190,6 +206,18 @@ public enum PropertyType { return method.getParameterTypes()[0]; } + public String getPropertyUri() { + return propertyUri; + } + + public int getMinOccurs() { + return minOccurs; + } + + public int getMaxOccurs() { + return maxOccurs; + } + public void confirmCompatible(PropertyStatement ps) throws PropertyTypeException { if (type != ps.getType()) { @@ -212,20 +240,20 @@ public enum PropertyType { } public static class ResourcePropertyMethod extends PropertyMethod { - public ResourcePropertyMethod(Method method) { - super(RESOURCE, method); + public ResourcePropertyMethod(Method method, Property annotation) { + super(RESOURCE, method, annotation); } } public static class StringPropertyMethod extends PropertyMethod { - public StringPropertyMethod(Method method) { - super(STRING, method); + public StringPropertyMethod(Method method, Property annotation) { + super(STRING, method, annotation); } } public static class FloatPropertyMethod extends PropertyMethod { - public FloatPropertyMethod(Method method) { - super(FLOAT, method); + public FloatPropertyMethod(Method method, Property annotation) { + super(FLOAT, method, annotation); } } diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md new file mode 100644 index 000000000..68da421fa --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/README.md @@ -0,0 +1,174 @@ +# package edu.cornell.mannlib.vitro.webapp.utils.configuration; +## Overview +### Purpose +This package consists of `ConfigurationBeanLoader` and associated classes. +`ConfigurationBeanLoader` will instantiate and populate objects according to a +description encoded in the triples of an RDF Graph, +and annotations within the Java class of the instantiated object. + +The description must include + ++ the URI of exactly one concrete Java class, from which the instance will be created. + +The description may also include + ++ URIs of Java interfaces which the concrete class implements. +The description may be use to satisfy a request +for any of those interfaces, as well as a request for the concrete class. + ++ Data properties. These will be passed to "property methods" in the instance +as part of the creation/initialization. The data value must be an untyped +literal (String) or a numeric literal (Float). + ++ Object properties. The URI is assumed to be that of another loader description. +The loader will attempt to instantiate the described object, and pass it to +the appropriate property method on the original instance. The result may be a +network of instances, nested to an arbitrary level. + +The loader also recognizes two special interfaces: `RequestModelsUser` and `ContextModelsUser`. +If a created instance implements these interfaces, +the loader will provide the appropriate `ModelAccess` object, +allowing the instance to access the Vitro triple-stores. + +### Examples of use + +#### ApplicationSetup + +When Vitro starts up, `ApplicationSetup` uses a `ConfigurationBeanLoader` to instantiate the Vitro's component modules. +The loader creates an RDF Graph from the file `applicationSetup.n3` and instantiates a `SearchEngine` instance, +a `FileStorage` instance, etc. + +Here is some RDF that might be used by `ApplicationSetup`: + + @prefix : . + :application + a , + ; + :hasSearchEngine :instrumentedSearchEngineWrapper ; + :hasFileStorage :ptiFileStorage . + + :ptiFileStorage + a , + . + + :instrumentedSearchEngineWrapper + a , + ; + :wraps :solrSearchEngine . + + :solrSearchEngine + a , + . + +In this case, the `ConfigurationBeanLoader` would be asked to load all instances of +`edu.cornell.mannlib.vitro.webapp.modules.Application`. +The application individual is declared to be both an `Application` and an `ApplicationImpl`. +This is valid because `Application` is an interface, and `ApplicationImpl` implements that interface. +An instance of `ApplicationImpl` will be created. + +The application instance has two child objects: a `SearchEngine` and a `FileStorage`. +These objects will also be created, and calls will be made to the application's "property methods" (see below). + +The `SearchEngine` in turn has a child object, so that also will be created, and provided to the `SearchEngine`. + +#### SearchIndexer + +When Vitro's `SearchIndexer` is initialized, it uses a `ConfigurationBeanLoader` to create +lists of `SearchIndexExcluder`s, `DocumentModifier`s, and `IndexingUriFinder`s. +Descriptions of these are taken from Vitro's display model. + +## Specifications + +### ConfigurationBeanLoader +The principal methods are: + ++ `public T loadInstance(String uri, Class resultClass) throws ConfigurationBeanLoaderException` + + Search the graph for triples that describe the `uri`. + If the description indicates that the individual is of type `resultClass`, create an instance and populate it. + Return a reference to the created instance. Throw an exception if the `uri` does not exist in the graph, + or if the description does not correctly describe an individual. + + The `resultClass` may be an interface. In that case, each individual must also have a type statement that refers + to a concrete class that satisfies the interface. An instance of the concrete class will be created. + ++ `public Set loadAll(Class resultClass) throws ConfigurationBeanLoaderException` + + 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`. + +### Restrictions on instantiated classes. +Each class to be instantiated must have a niladic constructor. + +### Property methods +When the loader encounters a data property or an object property in a description, +it will look in the instantiated class for a method tagged with the +`edu.cornell.mannlib.vitro.webapp.utils.configuration.Property` annotation. + +For example: + + @Property( + uri = "http://vitro.mannlib.cornell.edu/ns/vitro/ApplicationSetup#hasConfigurationTripleSource" + minOccurs = 1, + maxOccurs = 3) + public void setTBoxReasonerModule(TBoxReasonerModule module) { + this.module = module; + } + + +In more detail: + ++ A class must contain exactly one method that serves each property URI in the description. ++ 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 +with the same name in the class. ++ Property methods in superclasses will be recognized and accepted, but they may not be +overridden in a subclass. ++ If `minOccurs` is omitted, the default is `0`. If `minOccurs` is provided, it must be non-negative. ++ If `maxOccurs` is omitted, the default is `MAXINT`. If `maxOccurs` is provided, it must not be less than `minOccurs`. + +When instantiating: + ++ The parameter on a property method must match the value supplied in the RDF description. + + If the type of the parameter is `Float`, the object of the triple must be a numeric literal, or +an untyped literal that can be parsed as a number. + + If the type of the parameter is `String`, the object of the triple must be a String literal or an untyped literal. + + If the type of the parameter is another class, then the object of the triple must be the URI of +another RDF description, from which the loader can create an instance of the required class. ++ The number of values for a given property URI must not be less than the `minOccurs` value on the corresponding property method. ++ The number of values for a given property URI must not be greater than the `maxOccurs` value on the corresponding property method. + +### Validation methods +When the loader has satisfied all of the properties in an instance, it will +look in the instantiated class for any methods tagged with the +`edu.cornell.mannlib.vitro.webapp.utils.configuration.Validation` annotation. + +For example: + + @Validation + public void validate() throws Exception { + if (baseUri == null) { + throw new IllegalStateException( + "Configuration did not include a BaseURI."); + } + } + +Each such method will be called by the loader, and provides a opportunity to +confirm that the bean has been properly initialized. + +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. ++ Validation methods in superclasses will be called, but may not be overridden in a subclass. + +### Life cycle +For each instance that the loader creates, the loader will: + ++ Call the appropriate property method for each property in the description. +For object properties, this includes recursive calls to create subordinate objects. +The order of property method calls is undefined. ++ Call the validation methods on the class. The order of validation method calls is undefined. + +If any property method or validation method throws an exception, the process stops, +and the exception is propagated upward. \ No newline at end of file diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java index ced8285bb..4b739c6ec 100644 --- a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java @@ -5,6 +5,7 @@ package edu.cornell.mannlib.vitro.webapp.utils.configuration; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Collection; +import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -63,6 +64,45 @@ public class WrappedInstance { } } + /** + * The loader provides the distilled property statements from the RDF. Check + * that they satisfy the cardinality requested on their methods. + */ + public void checkCardinality(Set propertyStatements) + throws CardinalityException { + Map statementCounts = countPropertyStatementsByPredicateUri(propertyStatements); + for (PropertyMethod pm : propertyMethods.values()) { + Integer c = statementCounts.get(pm.getPropertyUri()); + int count = (c == null) ? 0 : c; + if (count < pm.getMinOccurs()) { + throw new CardinalityException("Expecting at least " + + pm.getMinOccurs() + " values for '" + + pm.getPropertyUri() + "', but found " + count + "."); + } + if (count > pm.getMaxOccurs()) { + throw new CardinalityException("Expecting no more than " + + pm.getMaxOccurs() + " values for '" + + pm.getPropertyUri() + "', but found " + count + "."); + } + } + statementCounts.hashCode(); + } + + private Map countPropertyStatementsByPredicateUri( + Set propertyStatements) { + Map statementCounts = new HashMap<>(); + for (String pmPredicateUri : propertyMethods.keySet()) { + int count = 0; + for (PropertyStatement ps : propertyStatements) { + if (ps.getPredicateUri().equals(pmPredicateUri)) { + count++; + } + } + statementCounts.put(pmPredicateUri, count); + } + return statementCounts; + } + /** * The loader provides the distilled property statements from the RDF, to * populate the instance. @@ -76,7 +116,6 @@ public class WrappedInstance { if (pm == null) { throw new NoSuchPropertyMethodException(ps); } - pm.confirmCompatible(ps); if (ps instanceof ResourcePropertyStatement) { @@ -132,4 +171,10 @@ public class WrappedInstance { } } + public static class CardinalityException extends Exception { + public CardinalityException(String message) { + super(message); + } + } + } \ No newline at end of file diff --git a/webapp/test/edu/cornell/mannlib/vitro/testing/ModelUtilitiesTestHelper.java b/webapp/test/edu/cornell/mannlib/vitro/testing/ModelUtilitiesTestHelper.java index 69c29537f..e38a0e3b7 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/testing/ModelUtilitiesTestHelper.java +++ b/webapp/test/edu/cornell/mannlib/vitro/testing/ModelUtilitiesTestHelper.java @@ -43,6 +43,12 @@ public class ModelUtilitiesTestHelper { createProperty(propertyUri), createPlainLiteral(objectValue)); } + public static Statement dataProperty(String subjectUri, String propertyUri, + Float objectValue) { + return createStatement(createResource(subjectUri), + createProperty(propertyUri), createTypedLiteral(objectValue)); + } + public static Statement dataProperty(String subjectUri, String propertyUri, Object objectValue, XSDDatatype dataType) { return createStatement(createResource(subjectUri), diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTest.java index da924567a..3d2ff1fff 100644 --- a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTest.java +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTest.java @@ -5,7 +5,6 @@ package edu.cornell.mannlib.vitro.webapp.utils.configuration; import static com.hp.hpl.jena.datatypes.xsd.XSDDatatype.XSDfloat; import static com.hp.hpl.jena.datatypes.xsd.XSDDatatype.XSDstring; import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.dataProperty; -import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.model; import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.typeStatement; import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri; @@ -20,28 +19,19 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import org.junit.Before; import org.junit.Ignore; import org.junit.Test; -import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; -import stubs.javax.servlet.ServletContextStub; -import stubs.javax.servlet.http.HttpServletRequestStub; -import stubs.javax.servlet.http.HttpSessionStub; - import com.hp.hpl.jena.rdf.model.Model; import com.hp.hpl.jena.rdf.model.Statement; -import edu.cornell.mannlib.vitro.testing.AbstractTestClass; import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess; -import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.ModelAccessFactory; import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; import edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationRdfParser.InvalidConfigurationRdfException; import edu.cornell.mannlib.vitro.webapp.utils.configuration.InstanceWrapper.InstanceWrapperException; import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.NoSuchPropertyMethodException; import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.ResourceUnavailableException; -import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.ValidationFailedException; /** * TODO @@ -50,47 +40,8 @@ import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.Vali * instances by URIs, so if a property refers to a created instance, we just * pass it in. */ -public class ConfigurationBeanLoaderTest extends AbstractTestClass { - private static final String GENERIC_INSTANCE_URI = "http://mytest.edu/some_instance"; - private static final String GENERIC_PROPERTY_URI = "http://mytest.edu/some_property"; - - private static final String SIMPLE_SUCCESS_INSTANCE_URI = "http://mytest.edu/simple_success_instance"; - - private static final String FULL_SUCCESS_INSTANCE_URI = "http://mytest.edu/full_success_instance"; - private static final String FULL_SUCCESS_BOOST_PROPERTY = "http://mydomain.edu/hasBoost"; - private static final String FULL_SUCCESS_TEXT_PROPERTY = "http://mydomain.edu/hasText"; - private static final String FULL_SUCCESS_HELPER_PROPERTY = "http://mydomain.edu/hasHelper"; - private static final String FULL_SUCCESS_HELPER_INSTANCE_URI = "http://mytest.edu/full_success_helper_instance"; - - private ServletContextStub ctx; - private HttpSessionStub session; - private HttpServletRequestStub req; - - private Model model; - - private ConfigurationBeanLoader loader; - private ConfigurationBeanLoader noRequestLoader; - private ConfigurationBeanLoader noContextLoader; - - @Before - public void setup() { - ctx = new ServletContextStub(); - - session = new HttpSessionStub(); - session.setServletContext(ctx); - - req = new HttpServletRequestStub(); - req.setSession(session); - - @SuppressWarnings("unused") - ModelAccessFactory maf = new ModelAccessFactoryStub(); - - model = model(); - - loader = new ConfigurationBeanLoader(model, req); - noRequestLoader = new ConfigurationBeanLoader(model, ctx); - noContextLoader = new ConfigurationBeanLoader(model); - } +public class ConfigurationBeanLoaderTest extends + ConfigurationBeanLoaderTestBase { // ---------------------------------------------------------------------- // Constructor tests @@ -308,276 +259,6 @@ public class ConfigurationBeanLoaderTest extends AbstractTestClass { // -------------------------------------------- - @Test - public void propertyMethodHasNoParameter_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(NoParameterOnPropertyMethod.class))); - - expectSimpleFailure( - NoParameterOnPropertyMethod.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, - "must accept exactly one parameter")); - } - - public static class NoParameterOnPropertyMethod { - @Property(uri = GENERIC_PROPERTY_URI) - public void methodTakesNoParameters() { - // Not suitable as a property method. - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodHasMultipleParameters_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(MultipleParametersOnPropertyMethod.class))); - - expectSimpleFailure( - MultipleParametersOnPropertyMethod.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, - "must accept exactly one parameter")); - } - - public static class MultipleParametersOnPropertyMethod { - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - public void methodTakesMultipleParameters(String s, Float f) { - // Not suitable as a property method. - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodHasInvalidParameter_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(InvalidParameterOnPropertyMethod.class))); - - expectSimpleFailure( - InvalidParameterOnPropertyMethod.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, - "Failed to create the PropertyMethod")); - } - - public static class InvalidParameterOnPropertyMethod { - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - public void methodTakesInvalidParameters(byte b) { - // Not suitable as a property method. - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodDoesNotReturnVoid_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(PropertyMethodMustReturnVoid.class))); - - expectSimpleFailure( - PropertyMethodMustReturnVoid.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, "should return void")); - } - - public static class PropertyMethodMustReturnVoid { - @Property(uri = GENERIC_PROPERTY_URI) - public String methodReturnIsNotVoid(String s) { - // Not suitable as a property method. - return s; - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodNotAccessible_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(PropertyMethodIsPrivate.class))); - model.add(dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, - "can't store in a private method.")); - - expectSimpleFailure( - PropertyMethodIsPrivate.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(PropertyTypeException.class, - "Property method failed.")); - } - - public static class PropertyMethodIsPrivate { - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - private void methodReturnIsNotVoid(String s) { - // Not suitable as a property method. - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodThrowsException_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(PropertyMethodFails.class))); - model.add(dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, - "exception while loading.")); - - expectSimpleFailure( - PropertyMethodFails.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(PropertyTypeException.class, - "Property method failed.")); - } - - public static class PropertyMethodFails { - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - public void methodThrowsException(String s) { - if (true) { - throw new RuntimeException("property method fails."); - } - } - } - - // -------------------------------------------- - - @Test - public void propertyMethodDuplicateUri_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(TwoMethodsWithSameUri.class))); - - expectSimpleFailure( - TwoMethodsWithSameUri.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, - "methods have the same URI")); - } - - public static class TwoMethodsWithSameUri { - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - public void firstProperty(String s) { - // Nothing to do - } - - @SuppressWarnings("unused") - @Property(uri = GENERIC_PROPERTY_URI) - public void secondProperty(String s) { - // Nothing to do - } - } - - // -------------------------------------------- - - @Test - public void validationMethodHasParameters_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(ValidationMethodWithParameter.class))); - - expectSimpleFailure( - ValidationMethodWithParameter.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, - "should not have parameters")); - } - - public static class ValidationMethodWithParameter { - @SuppressWarnings("unused") - @Validation - public void validateWithParameter(String s) { - // Nothing to do - } - } - - // -------------------------------------------- - - @Test - public void validationMethodDoesNotReturnVoid_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(ValidationMethodShouldReturnVoid.class))); - - expectSimpleFailure( - ValidationMethodShouldReturnVoid.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(InstanceWrapperException.class, "should return void")); - } - - public static class ValidationMethodShouldReturnVoid { - @Validation - public String validateWithReturnType() { - return "Hi there!"; - } - } - - // -------------------------------------------- - - @Test - public void validationMethodNotAccessible_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(ValidationMethodIsPrivate.class))); - - expectSimpleFailure( - ValidationMethodIsPrivate.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(ValidationFailedException.class, - "Error executing validation method")); - } - - public static class ValidationMethodIsPrivate { - @Validation - private void validateIsPrivate() { - // private method - } - } - - // -------------------------------------------- - - @Test - public void validationMethodThrowsException_throwsException() - throws ConfigurationBeanLoaderException { - model.add(typeStatement(GENERIC_INSTANCE_URI, - toJavaUri(ValidationThrowsException.class))); - - expectSimpleFailure( - ValidationThrowsException.class, - throwable(ConfigurationBeanLoaderException.class, - "Failed to load"), - throwable(ValidationFailedException.class, - "Error executing validation method")); - } - - public static class ValidationThrowsException { - @Validation - public void validateFails() { - throw new RuntimeException("from validation method"); - } - } - - // -------------------------------------------- - @Test public void loaderCantSatisfyContextModelsUser_throwsException() throws ConfigurationBeanLoaderException { @@ -1041,6 +722,7 @@ public class ConfigurationBeanLoaderTest extends AbstractTestClass { // Additional tests // ---------------------------------------------------------------------- + @SuppressWarnings("unused") @Test @Ignore // TODO @@ -1049,6 +731,7 @@ public class ConfigurationBeanLoaderTest extends AbstractTestClass { fail("circularReferencesAreNotFatal not implemented"); } + @SuppressWarnings("unused") @Test @Ignore // TODO deals with circularity. @@ -1057,6 +740,7 @@ public class ConfigurationBeanLoaderTest extends AbstractTestClass { fail("subordinateObjectCantBeLoaded_leavesNoAccessibleInstanceOfParent not implemented"); } + @SuppressWarnings("unused") @Test @Ignore // TODO deals with circularity. @@ -1065,42 +749,4 @@ public class ConfigurationBeanLoaderTest extends AbstractTestClass { fail("parentObjectCantBeLoaded_leavesNoAccessibleInstanceOfSubordinate not implemented"); } - // ---------------------------------------------------------------------- - // Helper methods for simple failure - // ---------------------------------------------------------------------- - - private void expectSimpleFailure(Class failureClass, - ExpectedThrowable expected, ExpectedThrowable cause) - throws ConfigurationBeanLoaderException { - expectException(expected.getClazz(), expected.getMessageSubstring(), - cause.getClazz(), cause.getMessageSubstring()); - - @SuppressWarnings("unused") - Object unused = loader.loadInstance(GENERIC_INSTANCE_URI, failureClass); - } - - private ExpectedThrowable throwable(Class clazz, - String messageSubstring) { - return new ExpectedThrowable(clazz, messageSubstring); - } - - private static class ExpectedThrowable { - private final Class clazz; - private final String messageSubstring; - - public ExpectedThrowable(Class clazz, - String messageSubstring) { - this.clazz = clazz; - this.messageSubstring = messageSubstring; - } - - public Class getClazz() { - return clazz; - } - - public String getMessageSubstring() { - return messageSubstring; - } - } - } diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTestBase.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTestBase.java new file mode 100644 index 000000000..0725f9a6c --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTestBase.java @@ -0,0 +1,102 @@ +/* $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.model; + +import org.junit.Before; + +import stubs.edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccessFactoryStub; +import stubs.javax.servlet.ServletContextStub; +import stubs.javax.servlet.http.HttpServletRequestStub; +import stubs.javax.servlet.http.HttpSessionStub; + +import com.hp.hpl.jena.rdf.model.Model; + +import edu.cornell.mannlib.vitro.testing.AbstractTestClass; +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess.ModelAccessFactory; + +/** + * TODO + */ +public class ConfigurationBeanLoaderTestBase extends AbstractTestClass { + protected static final String GENERIC_INSTANCE_URI = "http://mytest.edu/some_instance"; + protected static final String GENERIC_PROPERTY_URI = "http://mytest.edu/some_property"; + + protected static final String SIMPLE_SUCCESS_INSTANCE_URI = "http://mytest.edu/simple_success_instance"; + + protected static final String FULL_SUCCESS_INSTANCE_URI = "http://mytest.edu/full_success_instance"; + protected static final String FULL_SUCCESS_BOOST_PROPERTY = "http://mydomain.edu/hasBoost"; + protected static final String FULL_SUCCESS_TEXT_PROPERTY = "http://mydomain.edu/hasText"; + protected static final String FULL_SUCCESS_HELPER_PROPERTY = "http://mydomain.edu/hasHelper"; + protected static final String FULL_SUCCESS_HELPER_INSTANCE_URI = "http://mytest.edu/full_success_helper_instance"; + + private ServletContextStub ctx; + private HttpSessionStub session; + private HttpServletRequestStub req; + + protected Model model; + + protected ConfigurationBeanLoader loader; + protected ConfigurationBeanLoader noRequestLoader; + protected ConfigurationBeanLoader noContextLoader; + + @Before + public void setup() { + ctx = new ServletContextStub(); + + session = new HttpSessionStub(); + session.setServletContext(ctx); + + req = new HttpServletRequestStub(); + req.setSession(session); + + @SuppressWarnings("unused") + ModelAccessFactory maf = new ModelAccessFactoryStub(); + + model = model(); + + loader = new ConfigurationBeanLoader(model, req); + noRequestLoader = new ConfigurationBeanLoader(model, ctx); + noContextLoader = new ConfigurationBeanLoader(model); + } + + // ---------------------------------------------------------------------- + // Helper methods for simple failure + // ---------------------------------------------------------------------- + + protected void expectSimpleFailure(Class failureClass, + ExpectedThrowable expected, ExpectedThrowable cause) + throws ConfigurationBeanLoaderException { + expectException(expected.getClazz(), expected.getMessageSubstring(), + cause.getClazz(), cause.getMessageSubstring()); + + @SuppressWarnings("unused") + Object unused = loader.loadInstance(GENERIC_INSTANCE_URI, failureClass); + } + + protected ExpectedThrowable throwable(Class clazz, + String messageSubstring) { + return new ExpectedThrowable(clazz, messageSubstring); + } + + private static class ExpectedThrowable { + private final Class clazz; + private final String messageSubstring; + + public ExpectedThrowable(Class clazz, + String messageSubstring) { + this.clazz = clazz; + this.messageSubstring = messageSubstring; + } + + public Class getClazz() { + return clazz; + } + + public String getMessageSubstring() { + return messageSubstring; + } + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_Cardinality_Test.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_Cardinality_Test.java new file mode 100644 index 000000000..13e767265 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_Cardinality_Test.java @@ -0,0 +1,252 @@ +/* $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.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.objectProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.typeStatement; +import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri; +import static org.junit.Assert.assertEquals; + +import java.util.Set; + +import org.junit.Test; + +import com.hp.hpl.jena.rdf.model.Statement; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.InstanceWrapper.InstanceWrapperException; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.CardinalityException; + +/** + * Tests for minOccurs, maxOccurs on the @Property annotation. + */ +public class ConfigurationBeanLoader_Cardinality_Test extends + ConfigurationBeanLoaderTestBase { + + private static final Statement[] FOUND_NONE = new Statement[0]; + + private static final Statement[] FOUND_ONE = new Statement[] { dataProperty( + GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, "value One") }; + + private static final Statement[] FOUND_TWO = new Statement[] { + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value One"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Two") }; + + private static final Statement[] FOUND_THREE = new Statement[] { + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value One"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Two"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Three") }; + + private static final Statement[] FOUND_FOUR = new Statement[] { + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value One"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Two"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Three"), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "value Four") }; + + // -------------------------------------------- + + @Test + public void minOccursIsNegative_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NegativeMinOccurs.class))); + model.add(objectProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "http://some.other/uri")); + + expectSimpleFailure( + NegativeMinOccurs.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "must not be negative")); + } + + public static class NegativeMinOccurs { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI, minOccurs = -1) + public void setValue(String value) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void maxOccursLessThanMinOccurs_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(MaxOccursLessThanMinOccurs.class))); + model.add(objectProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "http://some.other/uri")); + + expectSimpleFailure( + MaxOccursLessThanMinOccurs.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "not be less than minOccurs")); + } + + public static class MaxOccursLessThanMinOccurs { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI, minOccurs = 2, maxOccurs = 1) + public void setValue(String value) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void expectingSomeFoundNone_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingSome.class))); + model.add(FOUND_NONE); + + expectSimpleFailure( + ExpectingSome.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(CardinalityException.class, "Expecting at least 2")); + } + + @Test + public void expectingSomeFoundFewer_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingSome.class))); + model.add(FOUND_ONE); + + expectSimpleFailure( + ExpectingSome.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(CardinalityException.class, "Expecting at least 2")); + } + + @Test + public void expectingSomeFoundMore_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingSome.class))); + model.add(FOUND_FOUR); + + expectSimpleFailure( + ExpectingSome.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(CardinalityException.class, + "Expecting no more than 3")); + } + + @Test + public void expectingSomeFoundSome_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingSome.class))); + model.add(FOUND_THREE); + Set instances = loader.loadAll(ExpectingSome.class); + assertEquals(1, instances.size()); + model.removeAll(); + + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingSome.class))); + model.add(FOUND_TWO); + instances = loader.loadAll(ExpectingSome.class); + assertEquals(1, instances.size()); + } + + public static class ExpectingSome { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI, minOccurs = 2, maxOccurs = 3) + public void setValue(String value) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void notSpecifiedFoundNone_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(CardinalityNotSpecified.class))); + model.add(FOUND_NONE); + + Set instances = loader + .loadAll(CardinalityNotSpecified.class); + assertEquals(1, instances.size()); + } + + @Test + public void notSpecifiedFoundSome_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(CardinalityNotSpecified.class))); + model.add(FOUND_FOUR); + + Set instances = loader + .loadAll(CardinalityNotSpecified.class); + assertEquals(1, instances.size()); + } + + public static class CardinalityNotSpecified { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void setValue(String value) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void expectNoneFoundNone_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectNone.class))); + model.add(FOUND_NONE); + + Set instances = loader.loadAll(ExpectNone.class); + assertEquals(1, instances.size()); + } + + public static class ExpectNone { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI, maxOccurs = 0) + public void setValue(String value) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void expectExactlyFoundExactly_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectTwo.class))); + model.add(FOUND_TWO); + + Set instances = loader.loadAll(ExpectTwo.class); + assertEquals(1, instances.size()); + } + + public static class ExpectTwo { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI, minOccurs = 2, maxOccurs = 2) + public void setValue(String value) { + // Nothing to do + } + } +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_PropertyTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_PropertyTest.java new file mode 100644 index 000000000..52996bd0e --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_PropertyTest.java @@ -0,0 +1,339 @@ +/* $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.dataProperty; +import static edu.cornell.mannlib.vitro.testing.ModelUtilitiesTestHelper.typeStatement; +import static edu.cornell.mannlib.vitro.webapp.utils.configuration.ConfigurationBeanLoader.toJavaUri; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.junit.Test; + +import com.hp.hpl.jena.rdf.model.Statement; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.InstanceWrapper.InstanceWrapperException; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; + +/** + * Tests of the @Property annotation. + */ +public class ConfigurationBeanLoader_PropertyTest extends + ConfigurationBeanLoaderTestBase { + protected static final String OTHER_PROPERTY_URI = "http://mytest.edu/different_property"; + + // -------------------------------------------- + + @Test + public void propertyMethodHasNoParameter_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NoParameterOnPropertyMethod.class))); + + expectSimpleFailure( + NoParameterOnPropertyMethod.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "must accept exactly one parameter")); + } + + public static class NoParameterOnPropertyMethod { + @Property(uri = GENERIC_PROPERTY_URI) + public void methodTakesNoParameters() { + // Not suitable as a property method. + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodHasMultipleParameters_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(MultipleParametersOnPropertyMethod.class))); + + expectSimpleFailure( + MultipleParametersOnPropertyMethod.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "must accept exactly one parameter")); + } + + public static class MultipleParametersOnPropertyMethod { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void methodTakesMultipleParameters(String s, Float f) { + // Not suitable as a property method. + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodHasInvalidParameter_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(InvalidParameterOnPropertyMethod.class))); + + expectSimpleFailure( + InvalidParameterOnPropertyMethod.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "Failed to create the PropertyMethod")); + } + + public static class InvalidParameterOnPropertyMethod { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void methodTakesInvalidParameters(byte b) { + // Not suitable as a property method. + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodDoesNotReturnVoid_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PropertyMethodMustReturnVoid.class))); + + expectSimpleFailure( + PropertyMethodMustReturnVoid.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, "should return void")); + } + + public static class PropertyMethodMustReturnVoid { + @Property(uri = GENERIC_PROPERTY_URI) + public String methodReturnIsNotVoid(String s) { + // Not suitable as a property method. + return s; + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodNotAccessible_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PropertyMethodIsPrivate.class))); + model.add(dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "can't store in a private method.")); + + expectSimpleFailure( + PropertyMethodIsPrivate.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(PropertyTypeException.class, + "Property method failed.")); + } + + public static class PropertyMethodIsPrivate { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + private void methodReturnIsNotVoid(String s) { + // Not suitable as a property method. + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodThrowsException_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PropertyMethodFails.class))); + model.add(dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "exception while loading.")); + + expectSimpleFailure( + PropertyMethodFails.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(PropertyTypeException.class, + "Property method failed.")); + } + + public static class PropertyMethodFails { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void methodThrowsException(String s) { + if (true) { + throw new RuntimeException("property method fails."); + } + } + } + + // -------------------------------------------- + + @Test + public void propertyMethodDuplicateUri_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(TwoMethodsWithSameUri.class))); + + expectSimpleFailure( + TwoMethodsWithSameUri.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "methods have the same URI")); + } + + public static class TwoMethodsWithSameUri { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void firstProperty(String s) { + // Nothing to do + } + + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void secondProperty(String s) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void superclassContainsPropertyAnnotation_success() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(EmptyPropertyMethodSubclass.class)), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "Value") }); + + EmptyPropertyMethodSubclass instance = loader.loadInstance( + GENERIC_INSTANCE_URI, EmptyPropertyMethodSubclass.class); + + assertNotNull(instance); + assertEquals("Value", instance.value); + } + + @Test + public void propertyMethodOverridesPropertyMethod_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PropertyMethodOverPropertyMethodSubclass.class))); + + expectSimpleFailure( + PropertyMethodOverPropertyMethodSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "conflicts with a property method")); + } + + @Test + public void plainMethodOverridesPropertyMethod_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PlainOverPropertyMethodSubclass.class))); + + expectSimpleFailure( + PlainOverPropertyMethodSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "conflicts with a property method")); + } + + @Test + public void uriConflictsBetweenSubclassAndSuperclassPropertyMethods_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ConflictingUriPropertyMethodSubclass.class))); + + expectSimpleFailure( + ConflictingUriPropertyMethodSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "Two property methods have the same URI")); + } + + @Test + public void propertyMethodSameNameButDoesNotOverride_throwsException() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(DistinctPropertyMethodSubclass.class)), + dataProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "Value"), + dataProperty(GENERIC_INSTANCE_URI, OTHER_PROPERTY_URI, + 100.0F) }); + + expectSimpleFailure( + DistinctPropertyMethodSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "conflicts with a property method")); + } + + public static class PropertyMethodSuperclass { + public String value = null; + + @Property(uri = GENERIC_PROPERTY_URI) + public void propertySuper(String v) { + if (value != null) { + throw new RuntimeException("propertySuper has already run."); + } + value = v; + } + } + + public static class EmptyPropertyMethodSubclass extends + PropertyMethodSuperclass { + // Just want to see that the superclass method is run. + } + + public static class DistinctPropertyMethodSubclass extends + PropertyMethodSuperclass { + public float fvalue; + + @Property(uri = OTHER_PROPERTY_URI) + public void propertySuper(Float f) { + if (fvalue != 0.0) { + throw new RuntimeException("propertySub has already run."); + } + fvalue = f; + } + } + + public static class ConflictingUriPropertyMethodSubclass extends + PropertyMethodSuperclass { + + @Property(uri = GENERIC_PROPERTY_URI) + public void propertyConflict(String v) { + // nothing to do. + } + } + + public static class PropertyMethodOverPropertyMethodSubclass extends + EmptyPropertyMethodSubclass { + @Override + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void propertySuper(String v) { + // Should fail (two levels down) + } + } + + public static class PlainOverPropertyMethodSubclass extends + PropertyMethodSuperclass { + @SuppressWarnings("unused") + public void propertySuper(Float f) { + // nothing to do + } + } + +} diff --git a/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java new file mode 100644 index 000000000..8e3498a56 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader_ValidationTest.java @@ -0,0 +1,215 @@ +/* $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.toJavaUri; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.InstanceWrapper.InstanceWrapperException; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.WrappedInstance.ValidationFailedException; + +/** + * Test the @Validation annotation. + */ +public class ConfigurationBeanLoader_ValidationTest extends + ConfigurationBeanLoaderTestBase { + // -------------------------------------------- + + @Test + public void validationMethodHasParameters_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ValidationMethodWithParameter.class))); + + expectSimpleFailure( + ValidationMethodWithParameter.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "should not have parameters")); + } + + public static class ValidationMethodWithParameter { + @SuppressWarnings("unused") + @Validation + public void validateWithParameter(String s) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void validationMethodDoesNotReturnVoid_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ValidationMethodShouldReturnVoid.class))); + + expectSimpleFailure( + ValidationMethodShouldReturnVoid.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, "should return void")); + } + + public static class ValidationMethodShouldReturnVoid { + @Validation + public String validateWithReturnType() { + return "Hi there!"; + } + } + + // -------------------------------------------- + + @Test + public void validationMethodNotAccessible_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ValidationMethodIsPrivate.class))); + + expectSimpleFailure( + ValidationMethodIsPrivate.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(ValidationFailedException.class, + "Error executing validation method")); + } + + public static class ValidationMethodIsPrivate { + @Validation + private void validateIsPrivate() { + // private method + } + } + + // -------------------------------------------- + + @Test + public void validationMethodThrowsException_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ValidationThrowsException.class))); + + expectSimpleFailure( + ValidationThrowsException.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(ValidationFailedException.class, + "Error executing validation method")); + } + + public static class ValidationThrowsException { + @Validation + public void validateFails() { + throw new RuntimeException("from validation method"); + } + } + + // -------------------------------------------- + + @Test + public void superclassContainsValidationMethod_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(EmptyValidationSubclass.class))); + + EmptyValidationSubclass instance = loader.loadInstance( + GENERIC_INSTANCE_URI, EmptyValidationSubclass.class); + + assertNotNull(instance); + assertTrue(instance.validatorSuperHasRun); + } + + @Test + public void superclassAndSubclassContainValidationMethods_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(AdditionalValidationSubclass.class))); + + AdditionalValidationSubclass instance = loader.loadInstance( + GENERIC_INSTANCE_URI, AdditionalValidationSubclass.class); + + assertNotNull(instance); + assertTrue(instance.validatorSuperHasRun); + assertTrue(instance.validatorSubHasRun); + } + + @Test + public void validationMethodOverridesValidationMethod_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ValidationOverValidationSubclass.class))); + + expectSimpleFailure( + ValidationOverValidationSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "overrides a validation method")); + } + + @Test + public void plainMethodOverridesValidationMethod_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PlainOverValidationSubclass.class))); + + expectSimpleFailure( + PlainOverValidationSubclass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "overrides a validation method")); + } + + public static class ValidationSuperclass { + public boolean validatorSuperHasRun = false; + + @Validation + public void validatorSuper() { + if (validatorSuperHasRun) { + throw new RuntimeException("validatorSuper has already run."); + } + validatorSuperHasRun = true; + } + } + + public static class EmptyValidationSubclass extends ValidationSuperclass { + // Just want to see that the superclass validation is run. + } + + public static class AdditionalValidationSubclass extends + ValidationSuperclass { + public boolean validatorSubHasRun = false; + + @Validation + public void validatorSub() { + if (validatorSubHasRun) { + throw new RuntimeException("validatorSub has already run."); + } + validatorSubHasRun = true; + } + } + + public static class ValidationOverValidationSubclass extends + EmptyValidationSubclass { + @Override + @Validation + public void validatorSuper() { + // Should fail (two levels down) + } + } + + public static class PlainOverValidationSubclass extends + ValidationSuperclass { + @Override + public void validatorSuper() { + // Should fail + } + } + +}