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 new file mode 100644 index 000000000..df6760e4f --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoader.java @@ -0,0 +1,139 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.*; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.Resource; +import com.hp.hpl.jena.rdf.model.ResourceFactory; +import com.hp.hpl.jena.vocabulary.RDF; + +import edu.cornell.mannlib.vitro.webapp.utils.jena.Critical; + +/** + * Load one or more Configuration beans from a specified model. + */ +public class ConfigurationBeanLoader { + + private static final String JAVA_URI_PREFIX = "java:"; + + // ---------------------------------------------------------------------- + // utility methods + // ---------------------------------------------------------------------- + + public static String toJavaUri(Class clazz) { + return JAVA_URI_PREFIX + clazz.getName(); + } + + 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 + + "'"); + } + return uri.substring(JAVA_URI_PREFIX.length()); + } + + // ---------------------------------------------------------------------- + // the instance + // ---------------------------------------------------------------------- + + /** Must not be null. */ + private final Model model; + + /** + * May be null, but the loader will be unable to satisfy instances of + * ContextModelUser. + */ + private final ServletContext ctx; + + /** + * May be null, but the loader will be unable to satisfy instances of + * RequestModelUser. + */ + private final HttpServletRequest req; + + public ConfigurationBeanLoader(Model model) { + this(model, null, null); + } + + public ConfigurationBeanLoader(Model model, ServletContext ctx) { + this(model, ctx, null); + } + + public ConfigurationBeanLoader(Model model, HttpServletRequest req) { + this(model, + (req == null) ? null : req.getSession().getServletContext(), + req); + } + + private ConfigurationBeanLoader(Model model, ServletContext ctx, + HttpServletRequest req) { + if (model == null) { + throw new NullPointerException("model may not be null."); + } + + this.model = model; + this.req = req; + this.ctx = ctx; + } + + /** + * Load the instance with this URI, if it is assignable to this class. + */ + public T loadInstance(String uri, Class resultClass) + throws ConfigurationBeanLoaderException { + if (uri == null) { + throw new NullPointerException("uri may not be null."); + } + if (resultClass == null) { + throw new NullPointerException("resultClass may not be null."); + } + + try { + ConfigurationRdf parsedRdf = ConfigurationRdfParser.parse(model, + uri, resultClass); + WrappedInstance wrapper = InstanceWrapper.wrap(parsedRdf + .getConcreteClass()); + wrapper.satisfyInterfaces(ctx, req); + wrapper.setProperties(this, parsedRdf.getPropertyStatements()); + wrapper.validate(); + return wrapper.getInstance(); + } catch (Exception e) { + throw new ConfigurationBeanLoaderException("Failed to load '" + uri + + "'", e); + } + } + + /** + * Find all of the resources with the specified class, and instantiate them. + */ + public Set loadAll(Class resultClass) + throws ConfigurationBeanLoaderException { + Set uris = new HashSet<>(); + try (Critical section = Critical.read(model)) { + List resources = model.listResourcesWithProperty( + RDF.type, createResource(toJavaUri(resultClass))).toList(); + for (Resource r : resources) { + if (r.isURIResource()) { + uris.add(r.getURI()); + } + } + } + + Set instances = new HashSet<>(); + for (String uri : uris) { + instances.add(loadInstance(uri, resultClass)); + } + return instances; + } +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderException.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderException.java new file mode 100644 index 000000000..e1084f7bc --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderException.java @@ -0,0 +1,16 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +/** + * Indicates that the loading of configuration beans did not succeed. + */ +public class ConfigurationBeanLoaderException extends Exception { + public ConfigurationBeanLoaderException(String message) { + super(message); + } + + public ConfigurationBeanLoaderException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdf.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdf.java new file mode 100644 index 000000000..b43fc2b16 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdf.java @@ -0,0 +1,29 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyStatement; + +public class ConfigurationRdf { + private final Class concreteClass; + private final Set properties; + + public ConfigurationRdf(Class concreteClass, + Set properties) { + this.concreteClass = concreteClass; + this.properties = Collections + .unmodifiableSet(new HashSet<>(properties)); + } + + public Class getConcreteClass() { + return concreteClass; + } + + public Set getPropertyStatements() { + return new HashSet<>(properties); + } +} \ No newline at end of file diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java new file mode 100644 index 000000000..507e71319 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationRdfParser.java @@ -0,0 +1,251 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; +import static com.hp.hpl.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.Set; + +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.Property; +import com.hp.hpl.jena.rdf.model.RDFNode; +import com.hp.hpl.jena.rdf.model.Selector; +import com.hp.hpl.jena.rdf.model.SimpleSelector; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.vocabulary.RDF; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyStatement; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; +import edu.cornell.mannlib.vitro.webapp.utils.jena.Critical; + +/** + * Parse the RDF for a single individual in the model to create a + * ConfigurationRdf object. + */ +public class ConfigurationRdfParser { + public static ConfigurationRdf parse(Model model, String uri, + Class resultClass) throws InvalidConfigurationRdfException { + if (model == null) { + throw new NullPointerException("model may not be null."); + } + if (uri == null) { + throw new NullPointerException("uri may not be null."); + } + if (resultClass == null) { + throw new NullPointerException("resultClass may not be null."); + } + + confirmExistenceInModel(model, uri); + + confirmEligibilityForResultClass(model, uri, resultClass); + + Set properties = loadProperties(model, uri); + + Class concreteClass = determineConcreteClass(model, uri, + resultClass); + + return new ConfigurationRdf(concreteClass, properties); + } + + private static void confirmExistenceInModel(Model model, String uri) + throws InvalidConfigurationRdfException { + Selector s = new SimpleSelector(createResource(uri), null, + (RDFNode) null); + try (Critical section = Critical.read(model)) { + if (model.listStatements(s).toList().isEmpty()) { + throw individualDoesNotAppearInModel(uri); + } + } + } + + private static void confirmEligibilityForResultClass(Model model, + String uri, Class resultClass) + throws InvalidConfigurationRdfException { + Statement s = createStatement(createResource(uri), RDF.type, + createResource(toJavaUri(resultClass))); + try (Critical section = Critical.read(model)) { + if (!model.contains(s)) { + throw noTypeStatementForResultClass(s); + } + } + } + + private static Set loadProperties(Model model, String uri) + throws InvalidConfigurationRdfException { + Set set = new HashSet<>(); + + try (Critical section = Critical.read(model)) { + List rawStatements = model.listStatements( + model.getResource(uri), (Property) null, (RDFNode) null) + .toList(); + if (rawStatements.isEmpty()) { + throw noRdfStatements(uri); + } + + for (Statement s : rawStatements) { + if (s.getPredicate().equals(RDF.type)) { + continue; + } else { + try { + set.add(PropertyType.createPropertyStatement(s)); + } catch (PropertyTypeException e) { + throw new InvalidConfigurationRdfException( + "Invalid property statement on '" + uri + "'", + e); + } + } + } + return set; + } + } + + private static Class determineConcreteClass(Model model, + String uri, Class resultClass) + throws InvalidConfigurationRdfException { + Set> concreteClasses = new HashSet<>(); + + try (Critical section = Critical.read(model)) { + for (RDFNode node : model.listObjectsOfProperty( + createResource(uri), RDF.type).toSet()) { + if (!node.isURIResource()) { + throw typeMustBeUriResource(node); + } + + String typeUri = node.asResource().getURI(); + if (!isConcreteClass(typeUri)) { + continue; + } + + concreteClasses.add(processTypeUri(typeUri, resultClass)); + } + } + + if (concreteClasses.isEmpty()) { + throw noConcreteClasses(uri); + } + + if (concreteClasses.size() > 1) { + throw tooManyConcreteClasses(uri, concreteClasses); + } + + return concreteClasses.iterator().next(); + } + + private static boolean isConcreteClass(String typeUri) + throws InvalidConfigurationRdfException { + try { + if (!isJavaUri(typeUri)) { + return false; + } + Class clazz = Class.forName(fromJavaUri(typeUri)); + if (clazz.isInterface()) { + return false; + } + if (Modifier.isAbstract(clazz.getModifiers())) { + return false; + } + return true; + } catch (ClassNotFoundException | ExceptionInInitializerError e) { + throw failedToLoadClass(typeUri, e); + } + } + + @SuppressWarnings("unchecked") + private static Class processTypeUri(String typeUri, + Class resultClass) throws InvalidConfigurationRdfException { + try { + Class clazz = Class.forName(fromJavaUri(typeUri)); + if (!resultClass.isAssignableFrom(clazz)) { + throw notAssignable(resultClass, clazz); + } + try { + clazz.getConstructor(); + } catch (NoSuchMethodException e) { + throw noZeroArgumentConstructor(clazz); + } catch (SecurityException e) { + throw constructorNotPublic(clazz); + } + return (Class) clazz; + } catch (ClassNotFoundException e) { + throw failedToLoadClass(typeUri, e); + } + } + + private static InvalidConfigurationRdfException individualDoesNotAppearInModel( + String uri) { + return new InvalidConfigurationRdfException( + "The model contains no statements about '" + 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); + } + + private static InvalidConfigurationRdfException notAssignable( + Class resultClass, Class clazz) { + return new InvalidConfigurationRdfException(clazz + + " cannot be assigned to " + resultClass); + } + + private static InvalidConfigurationRdfException noZeroArgumentConstructor( + Class clazz) { + return new InvalidConfigurationRdfException("Can't instantiate '" + + clazz + "': no zero-argument constructor."); + } + + private static InvalidConfigurationRdfException constructorNotPublic( + Class clazz) { + return new InvalidConfigurationRdfException("Can't instantiate '" + + clazz + "': zero-argument constructor is not public."); + } + + private static InvalidConfigurationRdfException failedToLoadClass( + String typeUri, Throwable e) { + return new InvalidConfigurationRdfException("Can't load this type: '" + + typeUri + "'", e); + } + + private static InvalidConfigurationRdfException typeMustBeUriResource( + RDFNode node) { + return new InvalidConfigurationRdfException( + "Type must be a URI Resource: " + node); + } + + private static InvalidConfigurationRdfException noTypeStatementForResultClass( + Statement s) { + return new InvalidConfigurationRdfException( + "A type statement is required: '" + s); + } + + 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 { + public InvalidConfigurationRdfException(String message) { + super(message); + } + + public InvalidConfigurationRdfException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ContextModelsUser.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ContextModelsUser.java new file mode 100644 index 000000000..304b37bf7 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/ContextModelsUser.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import edu.cornell.mannlib.vitro.webapp.modelaccess.ContextModelAccess; + +/** + * When the ConfigurationBeanLoader creates an instance of this class, it will + * call this method, supplying the RDF models from the context. + */ +public interface ContextModelsUser { + void setContextModels(ContextModelAccess models); +} 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 new file mode 100644 index 000000000..c882ccfd5 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/InstanceWrapper.java @@ -0,0 +1,100 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyMethod; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; + +/** + * Parse the annotations on this class and package them with a newly-created + * instance of the class. + */ +public class InstanceWrapper { + public static WrappedInstance wrap(Class concreteClass) + throws InstanceWrapperException { + return new WrappedInstance(createInstance(concreteClass), + parsePropertyAnnotations(concreteClass), + parseValidationAnnotations(concreteClass)); + } + + private static T createInstance(Class concreteClass) + throws InstanceWrapperException { + try { + return concreteClass.newInstance(); + } catch (Exception e) { + throw new InstanceWrapperException("Failed to create an instance.", + e); + } + } + + 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); + } + } + 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; + } + 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; + } + + public static class InstanceWrapperException extends Exception { + public InstanceWrapperException(String message) { + super(message); + } + + public InstanceWrapperException(String message, Throwable cause) { + super(message, cause); + } + + } +} 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 new file mode 100644 index 000000000..0eecb23e8 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Property.java @@ -0,0 +1,18 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method should be called each time a property with a matching + * URI is found on the bean. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Property { + String uri(); +} 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 new file mode 100644 index 000000000..397008548 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/PropertyType.java @@ -0,0 +1,243 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +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 java.lang.reflect.InvocationTargetException; +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; + +/** + * An enumeration of the types of properties that the ConfigurationBeanLoader + * will support. + * + * Also, classes that represent the Java methods and RDF statements associated + * with those types. + */ +public enum PropertyType { + RESOURCE { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new ResourcePropertyStatement(s.getPredicate(), s + .getObject().asResource().getURI()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method) { + return new ResourcePropertyMethod(method); + } + + }, + STRING { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new StringPropertyStatement(s.getPredicate(), s.getObject() + .asLiteral().getString()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method) { + return new StringPropertyMethod(method); + } + }, + FLOAT { + @Override + public PropertyStatement buildPropertyStatement(Statement s) { + return new FloatPropertyStatement(s.getPredicate(), s.getObject() + .asLiteral().getFloat()); + } + + @Override + protected PropertyMethod buildPropertyMethod(Method method) { + return new FloatPropertyMethod(method); + } + }; + + public static PropertyType typeForObject(RDFNode object) + throws PropertyTypeException { + if (object.isURIResource()) { + return RESOURCE; + } + if (object.isLiteral()) { + Literal literal = object.asLiteral(); + RDFDatatype datatype = literal.getDatatype(); + if (datatype == null || datatype.equals(XSDstring)) { + return STRING; + } + if (datatype.equals(XSDfloat)) { + return FLOAT; + } + } + throw new PropertyTypeException("Unsupported datatype on object: " + + object); + } + + public static PropertyType typeForParameterType(Class parameterType) + throws PropertyTypeException { + if (Float.TYPE.equals(parameterType)) { + return FLOAT; + } + if (String.class.equals(parameterType)) { + return STRING; + } + if (!parameterType.isPrimitive()) { + return RESOURCE; + } + throw new PropertyTypeException( + "Unsupported parameter type on method: " + parameterType); + } + + public static PropertyStatement createPropertyStatement(Statement s) + throws PropertyTypeException { + PropertyType type = PropertyType.typeForObject(s.getObject()); + return type.buildPropertyStatement(s); + } + + public static PropertyMethod createPropertyMethod(Method method) + throws PropertyTypeException { + Class parameterType = method.getParameterTypes()[0]; + PropertyType type = PropertyType.typeForParameterType(parameterType); + return type.buildPropertyMethod(method); + } + + protected abstract PropertyStatement buildPropertyStatement(Statement s); + + protected abstract PropertyMethod buildPropertyMethod(Method method); + + public static abstract class PropertyStatement { + private final PropertyType type; + private final String predicateUri; + + public PropertyStatement(PropertyType type, Property predicate) { + this.type = type; + this.predicateUri = predicate.getURI(); + } + + public PropertyType getType() { + return type; + } + + public String getPredicateUri() { + return predicateUri; + } + + public abstract Object getValue(); + } + + public static class ResourcePropertyStatement extends PropertyStatement { + private final String objectUri; + + public ResourcePropertyStatement(Property predicate, String objectUri) { + super(RESOURCE, predicate); + this.objectUri = objectUri; + } + + @Override + public String getValue() { + return objectUri; + } + } + + public static class StringPropertyStatement extends PropertyStatement { + private final String string; + + public StringPropertyStatement(Property predicate, String string) { + super(STRING, predicate); + this.string = string; + } + + @Override + public String getValue() { + return string; + } + } + + public static class FloatPropertyStatement extends PropertyStatement { + private final float f; + + public FloatPropertyStatement(Property predicate, float f) { + super(FLOAT, predicate); + this.f = f; + } + + @Override + public Float getValue() { + return f; + } + } + + public static abstract class PropertyMethod { + protected final PropertyType type; + protected final Method method; + + public PropertyMethod(PropertyType type, Method method) { + this.type = type; + this.method = method; + } + + public Method getMethod() { + return method; + } + + public Class getParameterType() { + return method.getParameterTypes()[0]; + } + + public void confirmCompatible(PropertyStatement ps) + throws PropertyTypeException { + if (type != ps.getType()) { + throw new PropertyTypeException( + "Can't apply statement of type " + ps.getType() + + " to a method of type " + type); + } + } + + public void invoke(Object instance, Object value) + throws PropertyTypeException { + try { + method.invoke(instance, value); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new PropertyTypeException("Property method failed.", e); + } + } + + } + + public static class ResourcePropertyMethod extends PropertyMethod { + public ResourcePropertyMethod(Method method) { + super(RESOURCE, method); + } + } + + public static class StringPropertyMethod extends PropertyMethod { + public StringPropertyMethod(Method method) { + super(STRING, method); + } + } + + public static class FloatPropertyMethod extends PropertyMethod { + public FloatPropertyMethod(Method method) { + super(FLOAT, method); + } + } + + public static class PropertyTypeException extends Exception { + public PropertyTypeException(String message) { + super(message); + } + + public PropertyTypeException(String message, Throwable cause) { + super(message, cause); + } + + } + +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/RequestModelsUser.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/RequestModelsUser.java new file mode 100644 index 000000000..e5dfe08e2 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/RequestModelsUser.java @@ -0,0 +1,13 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import edu.cornell.mannlib.vitro.webapp.modelaccess.RequestModelAccess; + +/** + * When the ConfigurationBeanLoader creates an instance of this class, it will + * call this method, supplying the RDF models for the current HTTP request. + */ +public interface RequestModelsUser { + void setRequestModels(RequestModelAccess models); +} diff --git a/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Validation.java b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Validation.java new file mode 100644 index 000000000..2e3c7c1f5 --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/Validation.java @@ -0,0 +1,20 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.webapp.utils.configuration; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The annotated method should be called after the bean is instantiated, to + * confirm that the bean is correctly formed. + * + * If the bean is not correctly formed, throw a runtime exception. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Validation { + // No elements +} 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 new file mode 100644 index 000000000..ced8285bb --- /dev/null +++ b/webapp/src/edu/cornell/mannlib/vitro/webapp/utils/configuration/WrappedInstance.java @@ -0,0 +1,135 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +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.Map; +import java.util.Set; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import edu.cornell.mannlib.vitro.webapp.modelaccess.ModelAccess; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyMethod; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyStatement; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.PropertyTypeException; +import edu.cornell.mannlib.vitro.webapp.utils.configuration.PropertyType.ResourcePropertyStatement; + +/** + * An instance of a ConfigurationBean, packaged with the distilled information + * about the annotated methods on the class. + */ +public class WrappedInstance { + private final T instance; + private final Map propertyMethods; + private final Set validationMethods; + + public WrappedInstance(T instance, + Map propertyMethods, + Set validationMethods) { + this.instance = instance; + this.propertyMethods = propertyMethods; + this.validationMethods = validationMethods; + } + + /** + * The loader calls this as soon as the instance is created. + * + * If the loader did not have access to a request object, then req will be + * null. If the instance expects request models, an exception will be + * thrown. + */ + public void satisfyInterfaces(ServletContext ctx, HttpServletRequest req) + throws ResourceUnavailableException { + if (instance instanceof ContextModelsUser) { + if (ctx == null) { + throw new ResourceUnavailableException("Cannot satisfy " + + "ContextModelsUser interface: context not available."); + } else { + ContextModelsUser cmu = (ContextModelsUser) instance; + cmu.setContextModels(ModelAccess.on(ctx)); + } + } + if (instance instanceof RequestModelsUser) { + if (req == null) { + throw new ResourceUnavailableException("Cannot satisfy " + + "RequestModelsUser interface: request not available."); + } else { + RequestModelsUser rmu = (RequestModelsUser) instance; + rmu.setRequestModels(ModelAccess.on(req)); + } + } + } + + /** + * The loader provides the distilled property statements from the RDF, to + * populate the instance. + */ + public void setProperties(ConfigurationBeanLoader loader, + Collection propertyStatements) + throws PropertyTypeException, NoSuchPropertyMethodException, + ConfigurationBeanLoaderException { + for (PropertyStatement ps : propertyStatements) { + PropertyMethod pm = propertyMethods.get(ps.getPredicateUri()); + if (pm == null) { + throw new NoSuchPropertyMethodException(ps); + } + + pm.confirmCompatible(ps); + + if (ps instanceof ResourcePropertyStatement) { + ResourcePropertyStatement rps = (ResourcePropertyStatement) ps; + Object subordinate = loader.loadInstance(rps.getValue(), + pm.getParameterType()); + pm.invoke(instance, subordinate); + } else { + pm.invoke(instance, ps.getValue()); + } + } + } + + /** + * After the interfaces have been satisfied and the instance has been + * populated, call any validation methods to see whether the instance is + * viable. + */ + public void validate() throws ValidationFailedException { + for (Method method : validationMethods) { + try { + method.invoke(instance); + } catch (IllegalAccessException | IllegalArgumentException + | InvocationTargetException e) { + throw new ValidationFailedException( + "Error executing validation method '" + method + "'", e); + } + } + } + + /** + * Once satisfied, populated, and validated, the instance is ready to go. + */ + public T getInstance() { + return instance; + } + + public static class ResourceUnavailableException extends Exception { + public ResourceUnavailableException(String message) { + super(message); + } + } + + public static class ValidationFailedException extends Exception { + public ValidationFailedException(String message, Throwable cause) { + super(message, cause); + } + } + + public static class NoSuchPropertyMethodException extends Exception { + public NoSuchPropertyMethodException(PropertyStatement ps) { + super("No property method for '" + ps.getPredicateUri() + "'"); + } + } + +} \ 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 new file mode 100644 index 000000000..69c29537f --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/testing/ModelUtilitiesTestHelper.java @@ -0,0 +1,68 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.testing; + +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createLangLiteral; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createPlainLiteral; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createProperty; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createResource; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createStatement; +import static com.hp.hpl.jena.rdf.model.ResourceFactory.createTypedLiteral; + +import java.util.SortedSet; +import java.util.TreeSet; + +import com.hp.hpl.jena.datatypes.xsd.XSDDatatype; +import com.hp.hpl.jena.rdf.model.Model; +import com.hp.hpl.jena.rdf.model.ModelFactory; +import com.hp.hpl.jena.rdf.model.Statement; +import com.hp.hpl.jena.vocabulary.RDF; + +/** + * Just some helper methods for Test classes that work with models. + */ +public class ModelUtilitiesTestHelper { + public static Model model(Statement... stmts) { + return ModelFactory.createDefaultModel().add(stmts); + } + + public static Statement typeStatement(String subjectUri, String classUri) { + return createStatement(createResource(subjectUri), RDF.type, + createResource(classUri)); + } + + public static Statement objectProperty(String subjectUri, + String propertyUri, String objectUri) { + return createStatement(createResource(subjectUri), + createProperty(propertyUri), createResource(objectUri)); + } + + public static Statement dataProperty(String subjectUri, String propertyUri, + String objectValue) { + return createStatement(createResource(subjectUri), + createProperty(propertyUri), createPlainLiteral(objectValue)); + } + + public static Statement dataProperty(String subjectUri, String propertyUri, + Object objectValue, XSDDatatype dataType) { + return createStatement(createResource(subjectUri), + createProperty(propertyUri), + createTypedLiteral(String.valueOf(objectValue), dataType)); + } + + public static Statement dataProperty(String subjectUri, String propertyUri, + String objectValue, String language) { + return createStatement(createResource(subjectUri), + createProperty(propertyUri), + createLangLiteral(objectValue, language)); + } + + public static SortedSet modelToStrings(Model m) { + SortedSet set = new TreeSet<>(); + for (Statement stmt : m.listStatements().toList()) { + set.add(stmt.toString()); + } + return set; + } + +} 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 new file mode 100644 index 000000000..a6b17e116 --- /dev/null +++ b/webapp/test/edu/cornell/mannlib/vitro/webapp/utils/configuration/ConfigurationBeanLoaderTest.java @@ -0,0 +1,1106 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +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; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.text.SimpleDateFormat; +import java.util.Arrays; +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 + * + * Circularity prevention. Before setting properties, create a WeakMap of + * 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); + } + + // ---------------------------------------------------------------------- + // Constructor tests + // ---------------------------------------------------------------------- + + @Test + public void constructor_modelIsNull_throwsException() { + expectException(NullPointerException.class, "model may not be null"); + + @SuppressWarnings("unused") + Object unused = new ConfigurationBeanLoader(null); + } + + // ---------------------------------------------------------------------- + // loadInstance() failures + // ---------------------------------------------------------------------- + + @Test + public void loadInstance_uriIsNull_throwsException() + throws ConfigurationBeanLoaderException { + expectException(NullPointerException.class, "uri may not be null"); + + @SuppressWarnings("unused") + Object unused = loader.loadInstance(null, SimpleSuccess.class); + } + + @Test + public void load_instance_resultClassIsNull_throwsException() + throws ConfigurationBeanLoaderException { + expectException(NullPointerException.class, + "resultClass may not be null"); + + @SuppressWarnings("unused") + Object unused = loader.loadInstance(GENERIC_INSTANCE_URI, null); + } + + @Test + public void noStatementsAboutUri_throwsException() + throws ConfigurationBeanLoaderException { + expectSimpleFailure( + SimpleSuccess.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "The model contains no statements about")); + } + + @Test + public void uriDoesNotDeclareResultClassAsType_throwsException() + throws ConfigurationBeanLoaderException { + model.add(dataProperty(GENERIC_INSTANCE_URI, + "http://some.simple/property", "a value")); + + expectSimpleFailure( + SimpleSuccess.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "A type statement is required")); + } + + // -------------------------------------------- + + @Test + public void uriHasNoConcreteType_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SimpleInterfaceFailure.class))); + + expectSimpleFailure( + SimpleInterfaceFailure.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "No concrete class is declared")); + } + + public static interface SimpleInterfaceFailure { + // This is not concrete, and there is no concrete implementation. + } + + // -------------------------------------------- + + @Test + public void uriHasMultipleConcreteTypes_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SecondConcreteClass.class))); + + expectSimpleFailure( + SimpleSuccess.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "more than one concrete class")); + } + + public static class SecondConcreteClass extends SimpleSuccess { + // Since this and SimpleSuccessClass are both concrete, they may not be + // used together. + } + + // -------------------------------------------- + + @Test + public void cantLoadConcreteType_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(UnloadableClass.class))); + + expectSimpleFailure( + UnloadableClass.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "Can't load this type")); + } + + public static class UnloadableClass { + static { + if (true) { + throw new IllegalStateException("This class cannot be loaded."); + } + } + } + + // -------------------------------------------- + + @Test + public void concreteTypeNotAssignableToResultClass_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + model.add(typeStatement(GENERIC_INSTANCE_URI, toJavaUri(String.class))); + + expectSimpleFailure( + SimpleSuccess.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "cannot be assigned to class")); + } + + // -------------------------------------------- + + @Test + public void noNiladicConstructor_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NoNiladicConstructor.class))); + + expectSimpleFailure( + NoNiladicConstructor.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "no zero-argument constructor.")); + } + + public static class NoNiladicConstructor { + @SuppressWarnings("unused") + public NoNiladicConstructor(String s) { + // Not suitable as a bean + } + } + + // -------------------------------------------- + + @Test + public void niladicConstructorNotAccessible_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(PrivateConstructor.class))); + + expectSimpleFailure( + PrivateConstructor.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InvalidConfigurationRdfException.class, + "no zero-argument constructor.")); + } + + public static class PrivateConstructor { + private PrivateConstructor() { + // Can't access the constructor. + } + } + + // -------------------------------------------- + + @Test + public void constructorThrowsException_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ConstructorFails.class))); + + expectSimpleFailure( + ConstructorFails.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(InstanceWrapperException.class, + "Failed to create an instance.")); + } + + public static class ConstructorFails { + public ConstructorFails() { + if (true) { + throw new IllegalStateException( + "The constructor throws an exception."); + } + } + } + + // -------------------------------------------- + + @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 { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NeedsContextModels.class))); + + loader = noContextLoader; + + expectSimpleFailure( + NeedsContextModels.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(ResourceUnavailableException.class, + "Cannot satisfy ContextModelsUser")); + } + + public static class NeedsContextModels implements ContextModelsUser { + @Override + public void setContextModels(ContextModelAccess models) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void loaderCantSatisfyRequestModelsUser_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NeedsRequestModels.class))); + + loader = noRequestLoader; + + expectSimpleFailure( + NeedsRequestModels.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(ResourceUnavailableException.class, + "Cannot satisfy RequestModelsUser")); + } + + public static class NeedsRequestModels implements RequestModelsUser { + @Override + public void setRequestModels(RequestModelAccess models) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void tripleHasUnrecognizedProperty_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + model.add(dataProperty(GENERIC_INSTANCE_URI, + "http://bogus.property/name", "No place to put it.")); + + expectSimpleFailure( + SimpleSuccess.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(NoSuchPropertyMethodException.class, + "No property method")); + } + + // -------------------------------------------- + + @Test + public void valueTypeDoesNotMatchArgumentOfPropertyMethod_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(ExpectingAString.class))); + model.add(objectProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "http://some.other/uri")); + + expectSimpleFailure( + ExpectingAString.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(PropertyTypeException.class, + "type RESOURCE to a method of type STRING")); + } + + public static class ExpectingAString { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void setString(String s) { + // Nothing to do + } + } + + // -------------------------------------------- + + @Test + public void subordinateObjectCantBeLoaded_throwsException() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(NoSuchSubordinateInstance.class))); + model.add(objectProperty(GENERIC_INSTANCE_URI, GENERIC_PROPERTY_URI, + "http://some.other/uri")); + + expectSimpleFailure( + NoSuchSubordinateInstance.class, + throwable(ConfigurationBeanLoaderException.class, + "Failed to load"), + throwable(ConfigurationBeanLoaderException.class, + "Failed to load")); + } + + public static class NoSuchSubordinateInstance { + @SuppressWarnings("unused") + @Property(uri = GENERIC_PROPERTY_URI) + public void setHelper(SimpleDateFormat sdf) { + // Nothing to do + } + } + + // ---------------------------------------------------------------------- + // loadInstance() successes + // ---------------------------------------------------------------------- + + /** + * Has a concrete result class. + */ + @Test + public void simpleSuccess() throws ConfigurationBeanLoaderException { + model.add(typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + + SimpleSuccess instance = loader.loadInstance( + SIMPLE_SUCCESS_INSTANCE_URI, SimpleSuccess.class); + + assertNotNull(instance); + } + + public static class SimpleSuccess { + // Nothing of interest. + } + + // -------------------------------------------- + + /** + * Exercise the full repertoire: properties of all types, validation + * methods. Result class is an interface; result class of helper is + * abstract. + */ + @Test + public void fullSuccess() throws ConfigurationBeanLoaderException { + model.add(FULL_SUCCESS_STATEMENTS); + + FullSuccessResultClass instance = loader.loadInstance( + FULL_SUCCESS_INSTANCE_URI, FullSuccessResultClass.class); + + assertNotNull(instance); + + HashSet expectedTextValues = new HashSet<>( + Arrays.asList(new String[] { "Huey", "Dewey", "Louis" })); + assertEquals(expectedTextValues, instance.getTextValues()); + + HashSet expectedBoostValues = new HashSet<>( + Arrays.asList(new Float[] { 1.5F, -99F })); + assertEquals(expectedBoostValues, instance.getBoostValues()); + + assertEquals(1, instance.getHelpers().size()); + assertTrue(instance.isValidated()); + } + + public static interface FullSuccessResultClass { + Set getTextValues(); + + Set getBoostValues(); + + Set getHelpers(); + + boolean isValidated(); + } + + public static class FullSuccessConcreteClass implements + FullSuccessResultClass { + private Set textValues = new HashSet<>(); + private Set boostValues = new HashSet<>(); + private Set helpers = new HashSet<>(); + + private boolean validatorOneHasRun; + private boolean validatorTwoHasRun; + + @Property(uri = FULL_SUCCESS_TEXT_PROPERTY) + public void addText(String text) { + textValues.add(text); + } + + @Property(uri = FULL_SUCCESS_BOOST_PROPERTY) + public void addBoost(float boost) { + boostValues.add(boost); + } + + @Property(uri = FULL_SUCCESS_HELPER_PROPERTY) + public void addHelper(FullSuccessHelperResultClass helper) { + helpers.add(helper); + } + + @Validation + public void validatorOne() { + if (validatorOneHasRun) { + throw new RuntimeException("validatorOne has already run."); + } + validatorOneHasRun = true; + } + + @Validation + public void validatorTwo() { + if (validatorTwoHasRun) { + throw new RuntimeException("validatorTwo has already run."); + } + validatorTwoHasRun = true; + } + + @Override + public Set getTextValues() { + return textValues; + } + + @Override + public Set getBoostValues() { + return boostValues; + } + + @Override + public Set getHelpers() { + return helpers; + } + + @Override + public boolean isValidated() { + return validatorOneHasRun && validatorTwoHasRun; + } + + } + + public static abstract class FullSuccessHelperResultClass { + // Abstract class, with concrete subclass. + } + + public static class FullSuccessHelperConcreteClass extends + FullSuccessHelperResultClass { + // No properties + } + + private static final Statement[] FULL_SUCCESS_STATEMENTS = new Statement[] { + // Create the instance itself. + typeStatement(FULL_SUCCESS_INSTANCE_URI, + toJavaUri(FullSuccessResultClass.class)), + typeStatement(FULL_SUCCESS_INSTANCE_URI, + toJavaUri(FullSuccessConcreteClass.class)), + + // Add some boost values. + dataProperty(FULL_SUCCESS_INSTANCE_URI, + FULL_SUCCESS_BOOST_PROPERTY, 1.5F, XSDfloat), + dataProperty(FULL_SUCCESS_INSTANCE_URI, + FULL_SUCCESS_BOOST_PROPERTY, -99F, XSDfloat), + + // Add some text values: plain, typed, language + dataProperty(FULL_SUCCESS_INSTANCE_URI, FULL_SUCCESS_TEXT_PROPERTY, + "Huey", XSDstring), + dataProperty(FULL_SUCCESS_INSTANCE_URI, FULL_SUCCESS_TEXT_PROPERTY, + "Dewey", "en-US"), + dataProperty(FULL_SUCCESS_INSTANCE_URI, FULL_SUCCESS_TEXT_PROPERTY, + "Louis"), + + // Add a subordinate object. + objectProperty(FULL_SUCCESS_INSTANCE_URI, + FULL_SUCCESS_HELPER_PROPERTY, + FULL_SUCCESS_HELPER_INSTANCE_URI), + typeStatement(FULL_SUCCESS_HELPER_INSTANCE_URI, + toJavaUri(FullSuccessHelperResultClass.class)), + typeStatement(FULL_SUCCESS_HELPER_INSTANCE_URI, + toJavaUri(FullSuccessHelperConcreteClass.class)) }; + + // -------------------------------------------- + + @Test + public void irrelevantNonConcreteTypesAreIgnored() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(SimpleSuccess.class)), + typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(IrrelevantInterface.class)), + typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(IrrelevantAbstractClass.class)), + typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + "http://irrelevant.nonJava/class") }); + + SimpleSuccess instance = loader.loadInstance( + SIMPLE_SUCCESS_INSTANCE_URI, SimpleSuccess.class); + + assertNotNull(instance); + } + + public interface IrrelevantInterface { + // Nothing of interest. + } + + public abstract class IrrelevantAbstractClass { + // Nothing of interest. + } + + // -------------------------------------------- + + @Test + public void loaderHasNoRequestButClassDoesntRequireIt_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + + loader = noRequestLoader; + + SimpleSuccess instance = loader.loadInstance( + SIMPLE_SUCCESS_INSTANCE_URI, SimpleSuccess.class); + + assertNotNull(instance); + } + + // -------------------------------------------- + + @Test + public void loaderHasNoContextButClassDoesntRequireIt_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(SIMPLE_SUCCESS_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + + loader = noContextLoader; + + SimpleSuccess instance = loader.loadInstance( + SIMPLE_SUCCESS_INSTANCE_URI, SimpleSuccess.class); + + assertNotNull(instance); + } + + // -------------------------------------------- + + /** + * FullSuccess already tests for multiple validation methods. + * + * SimpleSuccess already test for no validation methods, and for no property + * methods. + */ + @Test + public void noValuesForProperty_success() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement(FULL_SUCCESS_INSTANCE_URI, + toJavaUri(FullSuccessConcreteClass.class)), + typeStatement(FULL_SUCCESS_INSTANCE_URI, + toJavaUri(FullSuccessResultClass.class)) }); + + FullSuccessResultClass instance = loader.loadInstance( + FULL_SUCCESS_INSTANCE_URI, FullSuccessResultClass.class); + + assertNotNull(instance); + assertEquals(Collections.emptySet(), instance.getTextValues()); + assertEquals(Collections.emptySet(), instance.getBoostValues()); + assertEquals(Collections.emptySet(), instance.getHelpers()); + assertTrue(instance.isValidated()); + } + + // ---------------------------------------------------------------------- + // loadAll() failures + // ---------------------------------------------------------------------- + + @Test + public void loadAll_oneObjectCantBeLoaded_throwsException() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement("http://apple/good", + toJavaUri(OneBadAppleSpoilsTheBunch.class)), + typeStatement("http://apple/good", toJavaUri(AGoodApple.class)), + typeStatement("http://apple/bad", + toJavaUri(OneBadAppleSpoilsTheBunch.class)) }); + + expectException(ConfigurationBeanLoaderException.class, + "Failed to load", InvalidConfigurationRdfException.class, + "No concrete class is declared"); + + loader.loadAll(OneBadAppleSpoilsTheBunch.class); + } + + public interface OneBadAppleSpoilsTheBunch { + // Nothing. + } + + public class AGoodApple implements OneBadAppleSpoilsTheBunch { + // Nothing + } + + // ---------------------------------------------------------------------- + // loadAll() successes + // ---------------------------------------------------------------------- + + @Test + public void loadAll_noResults_success() + throws ConfigurationBeanLoaderException { + Set instances = loader.loadAll(SimpleSuccess.class); + assertTrue(instances.isEmpty()); + } + + // -------------------------------------------- + + @Test + public void loadAll_oneResult_success() + throws ConfigurationBeanLoaderException { + model.add(typeStatement(GENERIC_INSTANCE_URI, + toJavaUri(SimpleSuccess.class))); + + Set instances = loader.loadAll(SimpleSuccess.class); + assertEquals(1, instances.size()); + } + + // -------------------------------------------- + + @Test + public void loadAll_multipleResults_success() + throws ConfigurationBeanLoaderException { + model.add(new Statement[] { + typeStatement("http://simple.instance/one", + toJavaUri(InstanceWithProperty.class)), + dataProperty("http://simple.instance/one", + "http://simple.text/property", "FIRST"), + typeStatement("http://simple.instance/two", + toJavaUri(InstanceWithProperty.class)), + dataProperty("http://simple.instance/two", + "http://simple.text/property", "SECOND") }); + + Set instances = loader + .loadAll(InstanceWithProperty.class); + assertEquals(2, instances.size()); + + Set textValues = new HashSet<>(); + for (InstanceWithProperty instance : instances) { + textValues.add(instance.getText()); + } + assertEquals(new HashSet<>(Arrays.asList("FIRST", "SECOND")), + textValues); + } + + public static class InstanceWithProperty { + private String text; + + @Property(uri = "http://simple.text/property") + public void setText(String text) { + this.text = text; + } + + public String getText() { + return text; + } + } + + // ---------------------------------------------------------------------- + // Additional tests + // ---------------------------------------------------------------------- + + @Test + @Ignore + // TODO + public void circularReferencesAreNotFatal() + throws ConfigurationBeanLoaderException { + fail("circularReferencesAreNotFatal not implemented"); + } + + @Test + @Ignore + // TODO deals with circularity. + public void subordinateObjectCantBeLoaded_leavesNoAccessibleInstanceOfParent() + throws ConfigurationBeanLoaderException { + fail("subordinateObjectCantBeLoaded_leavesNoAccessibleInstanceOfParent not implemented"); + } + + @Test + @Ignore + // TODO deals with circularity. + public void parentObjectCantBeLoaded_leavesNoAccessibleInstanceOfSubordinate() + throws ConfigurationBeanLoaderException { + 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; + } + } + +}