1. Michael Ludwig
  2. entreri

Commits

Michael Ludwig  committed e4a3211

Implement mirror-based generator and greatly clean up the component specification API.

  • Participants
  • Parent commits 5946c7d
  • Branches default

Comments (0)

Files changed (19)

File pom.xml

View file
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>com.lhkbob.entreri</groupId>
     <artifactId>entreri</artifactId>
             <plugin>
                 <groupId>org.apache.maven.plugins</groupId>
                 <artifactId>maven-compiler-plugin</artifactId>
-                <version>2.3.2</version>
+                <version>3.1</version>
                 <configuration>
-                    <source>1.6</source>
-                    <target>1.6</target>
+                    <source>1.7</source>
+                    <target>1.7</target>
                     <showWarnings>true</showWarnings>
                 </configuration>
+
+                <executions>
+                    <execution>
+                        <id>default-compile</id>
+                        <configuration>
+                            <proc>none</proc>
+                        </configuration>
+                    </execution>
+                </executions>
             </plugin>
 
             <plugin>

File src/main/java/com/lhkbob/entreri/GenerateAtBuild.java

View file
+package com.lhkbob.entreri;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target(ElementType.TYPE)
+public @interface GenerateAtBuild {
+    // FIXME I can get the processor to handle all components by filtering over all
+    // received types. Is this better? It does slow the build down and makes testing both
+    // the janino and compiled classes trickier, on the other hand always processing them
+    // means that a) I could remove the janino dependency (not sure if i want to though),
+    // b) all components are build-time checked
+    //
+    // But, what if intellij doesn't integrate well if APT?
+}

File src/main/java/com/lhkbob/entreri/IllegalComponentDefinitionException.java

View file
 public class IllegalComponentDefinitionException extends RuntimeException {
     private static final long serialVersionUID = 1L;
 
+    private final String componentTypeName;
+
     /**
      * Create an exception that specifies the leaf-level class in a Component type
      * hierarchy has some problem with its definition
      *
-     * @param type    The leaf, concrete type
-     * @param problem A generic error message to be tacked to the end of the final error
-     *                message
+     * @param componentTypeName The canonical class name of the component type
+     * @param problem           A generic error message to be tacked to the end of the
+     *                          final error message
      */
-    public IllegalComponentDefinitionException(Class<? extends Component> type,
-                                               String problem) {
-        super("Type has illegal definition: " + type + ", error: " + problem);
+    public IllegalComponentDefinitionException(String componentTypeName, String problem) {
+        super(componentTypeName + " is invalid, error: " + problem);
+        this.componentTypeName = componentTypeName;
+    }
+
+    /**
+     * @return The canonical name of the invalid component type
+     */
+    public String getComponentTypeName() {
+        return componentTypeName;
     }
 }

File src/main/java/com/lhkbob/entreri/impl/CachingDelegatingFactoryProvider.java

View file
     }
 
     @Override
+    @SuppressWarnings("unchecked")
     public <T extends Component> Factory<T> getFactory(Class<T> componentType) {
         // blocking synchronization is easiest to do and it shouldn't be a high contention
         // point because factories are only gotten when the component repository is created

File src/main/java/com/lhkbob/entreri/impl/CompiledFactoryProvider.java

View file
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
-import java.util.List;
 
 /**
  * CompiledFactoryProvider searches the classpath for existing class definitions of the
     @Override
     public <T extends Component> Factory<T> getFactory(Class<T> componentType) {
         try {
-            return new CompiledFactory<T>(componentType);
+            return new CompiledFactory<>(componentType);
         } catch (ClassNotFoundException cfe) {
             // if class is not present we just return null to delegate to Janino
             return null;
 
     private static class CompiledFactory<T extends Component> implements Factory<T> {
         final Class<? extends AbstractComponent<T>> implType;
-        final List<PropertySpecification> specification;
+        final ComponentSpecification specification;
 
         final Constructor<? extends AbstractComponent<T>> ctor;
 
         @SuppressWarnings("unchecked")
         public CompiledFactory(Class<T> type) throws ClassNotFoundException {
+            specification = ComponentSpecification.Factory.fromClass(type);
             String implName = ComponentFactoryProvider
-                    .getImplementationClassName(type, true);
+                    .getImplementationClassName(specification, true);
 
             Class<?> loaded = Class.forName(implName);
-
+            System.out.println("Discovered compiled class: " + loaded);
             // although the compiled classes should have been produced from the same
             // generated source used by Janino, we can't be certain because we're reading
             // them from the classpath, so this factory has to validate certain elements
 
             // at this point it's a safe cast
             implType = (Class<? extends AbstractComponent<T>>) loaded;
-            specification = PropertySpecification.getSpecification(type);
 
             try {
                 ctor = implType.getConstructor(ComponentRepository.class);
         public AbstractComponent<T> newInstance(ComponentRepository<T> forRepository) {
             try {
                 return ctor.newInstance(forRepository);
-            } catch (InstantiationException e) {
-                throw new RuntimeException(
-                        "Exception instantiating compiled component impl", e);
-            } catch (IllegalAccessException e) {
-                throw new RuntimeException(
-                        "Exception instantiating compiled component impl", e);
-            } catch (InvocationTargetException e) {
+            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                 throw new RuntimeException(
                         "Exception instantiating compiled component impl", e);
             }
         }
 
         @Override
-        public List<PropertySpecification> getSpecification() {
+        public ComponentSpecification getSpecification() {
             return specification;
         }
     }

File src/main/java/com/lhkbob/entreri/impl/ComponentFactoryProvider.java

View file
 package com.lhkbob.entreri.impl;
 
 import com.lhkbob.entreri.Component;
-import com.lhkbob.entreri.Ownable;
-import com.lhkbob.entreri.Owner;
 import com.lhkbob.entreri.property.ObjectProperty;
 
-import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.nio.charset.Charset;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
+import java.util.*;
 
 /**
  * ComponentFactoryProvider provides Factory instances that can create instances of
          *
          * @return The property specification used by the factory
          */
-        public List<PropertySpecification> getSpecification();
+        public ComponentSpecification getSpecification();
     }
 
     /**
      * <var>includePackage</var> is true, the returned string will include the package
      * name to create a valid, absolute type name.
      *
-     * @param type           The component type
+     * @param spec           The component specification
      * @param includePackage True if the package should be included
      *
      * @return The class name that corresponds to the generated proxy implmentation for
      *         the given type
      */
-    public static String getImplementationClassName(Class<? extends Component> type,
+    public static String getImplementationClassName(ComponentSpecification spec,
                                                     boolean includePackage) {
         // first get the simple name, concatenating all types in the hierarchy
-        // (e.g. if its an inner class the name is Foo$Blah and this converts it to FooBlah)
-        String scrubbed = type.getSimpleName().replace("[\\.$]", "");
+        // (e.g. if its an inner class the name is Foo.Blah and this converts it to FooBlah)
+        String scrubbed = spec.getType().replace("[\\.]", "");
 
         // compute a unique hash on the original canonical class name to guarantee
         // the uniqueness of the generated class as well
         int hash;
         try {
             MessageDigest md = MessageDigest.getInstance("MD5");
-            md.update(type.getCanonicalName().getBytes(CHARSET));
+            md.update((spec.getPackage() + "." + spec.getType()).getBytes(CHARSET));
 
             ByteBuffer md5 = ByteBuffer.wrap(md.digest());
             hash = Math.abs(md5.getInt() ^ md5.getInt() ^ md5.getInt() ^ md5.getInt());
 
         StringBuilder sb = new StringBuilder();
         if (includePackage) {
-            sb.append(type.getPackage().getName()).append('.');
+            sb.append(spec.getPackage()).append('.');
         }
         sb.append(scrubbed).append("Impl").append(hash);
 
     /**
      * Generate valid Java source code for a proxy implementation of the given component
      * type. The name and package of the generated class are consistent with the results
-     * of calling {@link #getImplementationClassName(Class, boolean)}. It is assumed (and
-     * not validated) that the property specification is valid and corresponds to the
-     * component type. The new class will also extend from AbstractComponent and has a
-     * single constructor that takes a ComponentRepository.
+     * of calling {@link #getImplementationClassName(ComponentSpecification, boolean)}. It
+     * is assumed (and not validated) that the property specification is valid and
+     * corresponds to the component type. The new class will also extend from
+     * AbstractComponent and has a single constructor that takes a ComponentRepository.
      * <p/>
      * If <var>useGenerics</var> is true, the source code will use generics for
      * AbstractComponent, its ComponentRepository, and any ObjectProperty instances
      * If not, undefined behavior can occur later on if the specification is inconsistent
      * with the interface declared by the component
      *
-     * @param type        The component interface that must be implemented
-     * @param spec        The corresponding property specification used to generate the
-     *                    source code
+     * @param spec        The component specification that must be implemented
      * @param useGenerics True if the generated source should take advantage of Java
      *                    generics
      *
      * @return Source code of a valid implementation for the component type
      */
-    public static String generateJavaCode(Class<? extends Component> type,
-                                          List<PropertySpecification> spec,
+    // FIXME use the SourceVersion enum instead of useGenerics
+    public static String generateJavaCode(ComponentSpecification spec,
                                           boolean useGenerics) {
-        String baseTypeName = safeName(type);
-
-        String implName = getImplementationClassName(type, false);
+        String implName = getImplementationClassName(spec, false);
 
         // the implementation will extend AbstractComponent sans generics because
         // Janino does not support them right now
         StringBuilder sb = new StringBuilder();
-        sb.append("package ").append(type.getPackage().getName()).append(";\n\n")
+        sb.append("package ").append(spec.getPackage()).append(";\n\n")
           .append("public class ").append(implName).append(" extends ")
           .append(ABSTRACT_COMPONENT_NAME);
         if (useGenerics) {
-            sb.append('<').append(baseTypeName).append('>');
+            sb.append('<').append(spec.getType()).append('>');
         }
-        sb.append(" implements ").append(baseTypeName).append(" {\n");
+        sb.append(" implements ").append(spec.getType()).append(" {\n");
 
         // add property instances with proper cast so we don't have to do that every
         // time a property is accessed, and add any shared instance field declarations
         int property = 0;
-        for (PropertySpecification s : spec) {
-            String fld = (useGenerics && s.getPropertyType().equals(ObjectProperty.class)
-                          ? OBJECT_PROP_NAME + "<" + baseTypeName + ">"
-                          : safeName(s.getPropertyType()));
+        for (PropertyDeclaration s : spec.getProperties()) {
+            String fld = (
+                    useGenerics && s.getPropertyImplementation().equals(OBJECT_PROP_NAME)
+                    ? OBJECT_PROP_NAME + "<" + s.getType() + ">"
+                    : s.getPropertyImplementation());
 
             sb.append("\tprivate final ").append(fld).append(' ')
               .append(PROPERTY_FIELD_PREFIX).append(property).append(";\n");
-            if (s.isSharedInstance()) {
-                sb.append("\tprivate final ").append(safeName(s.getType())).append(' ')
+            if (s.isShared()) {
+                sb.append("\tprivate final ").append(s.getType()).append(' ')
                   .append(SHARED_FIELD_PREFIX).append(property).append(";\n");
             }
             property++;
         // shared instances; as with type declaration we cannot use generics
         sb.append("\n\tpublic ").append(implName).append("(").append(COMPONENT_REPO_NAME);
         if (useGenerics) {
-            sb.append('<').append(baseTypeName).append('>');
+            sb.append('<').append(spec.getType()).append('>');
         }
 
         sb.append(" ").append(REPO_FIELD_NAME).append(") {\n").append("\t\tsuper(")
           .append(REPO_FIELD_NAME).append(");\n");
         property = 0;
-        for (PropertySpecification s : spec) {
-            String cast = (useGenerics && s.getPropertyType().equals(ObjectProperty.class)
-                           ? OBJECT_PROP_NAME + "<" + baseTypeName + ">"
-                           : safeName(s.getPropertyType()));
+        for (PropertyDeclaration s : spec.getProperties()) {
+            String cast = (
+                    useGenerics && s.getPropertyImplementation().equals(OBJECT_PROP_NAME)
+                    ? OBJECT_PROP_NAME + "<" + s.getType() + ">"
+                    : s.getPropertyImplementation());
 
             sb.append("\t\t").append(PROPERTY_FIELD_PREFIX).append(property)
               .append(" = (").append(cast).append(") ").append(REPO_FIELD_NAME)
               .append('.').append(GET_PROPERTY_METHOD).append('(').append(property)
               .append(");\n");
-            if (s.isSharedInstance()) {
+            if (s.isShared()) {
                 sb.append("\t\t").append(SHARED_FIELD_PREFIX).append(property)
                   .append(" = ").append(PROPERTY_FIELD_PREFIX).append(property)
                   .append('.').append(CREATE_SHARE_METHOD).append("();\n");
         }
         sb.append("\t}\n");
 
-        // implement all methods of the interface
-        for (Method m : type.getMethods()) {
-            // skip the same methods skipped by the property specification
-            if (m.getDeclaringClass().equals(Component.class) ||
-                m.getDeclaringClass().equals(Owner.class) ||
-                m.getDeclaringClass().equals(Ownable.class)) {
-                continue;
+        // implement all getters of the interface, and accumulate setters
+        Map<String, List<PropertyDeclaration>> setters = new HashMap<>();
+        property = 0;
+        for (PropertyDeclaration s : spec.getProperties()) {
+            appendGetter(s, property, sb, useGenerics);
+            List<PropertyDeclaration> setterParams = setters.get(s.getSetterMethod());
+            if (setterParams == null) {
+                setterParams = new ArrayList<>();
+                setters.put(s.getSetterMethod(), setterParams);
             }
+            setterParams.add(s);
 
-            if (!appendGetter(m, spec, sb, useGenerics)) {
-                if (!appendSetter(m, spec, sb)) {
-                    throw new IllegalStateException(
-                            "Unexpected method during code generation: " + m);
-                }
-            }
+            property++;
+        }
+
+        // implement all setters
+        for (List<PropertyDeclaration> setter : setters.values()) {
+            appendSetter(setter, spec, sb);
         }
 
         sb.append("}\n");
     // Internal helper functions to generate the source code
 
     /**
-     * @param type The type whose name is returned after filtering
+     * Append the getter method definition for the given property.
      *
-     * @return A String version of the class name, including package, that is safe to
-     *         insert into Java source code
-     */
-    private static String safeName(Class<?> type) {
-        return type.getName().replace('$', '.');
-    }
-
-    /**
-     * Append the generated declaration and definition for the given getter method, using
-     * the component specification in <var>spec</var>.
-     *
-     * @param getter      The setter method to append
-     * @param spec        The spec for the component type being generated
+     * @param forProperty The property whose getter method will be defined
+     * @param index       The index of the property in the overall spec
      * @param sb          The buffer to append to
      * @param useGenerics True if the source can use generics, in which case
      *                    ObjectProperties are properly parameterized
-     *
-     * @return True if the method was a valid getter, or false if it wasn't and no body
-     *         was appended
      */
-    private static boolean appendGetter(Method getter, List<PropertySpecification> spec,
-                                        StringBuilder sb, boolean useGenerics) {
-        PropertySpecification forProperty = findPropertyForGetter(getter, spec);
-        if (forProperty == null) {
-            return false;
-        }
-
+    private static void appendGetter(PropertyDeclaration forProperty, int index,
+                                     StringBuilder sb, boolean useGenerics) {
         // method signature
-        int idx = spec.indexOf(forProperty);
-        sb.append("\n\tpublic ").append(safeName(forProperty.getType())).append(" ")
-          .append(getter.getName()).append("() {\n\t\t");
+        sb.append("\n\tpublic ").append(forProperty.getType()).append(" ")
+          .append(forProperty.getGetterMethod()).append("() {\n\t\t");
 
         // implementation body, depending on if we use a shared instance variable or not
-        if (forProperty.isSharedInstance()) {
-            sb.append(PROPERTY_FIELD_PREFIX).append(idx).append(".get(")
+        if (forProperty.isShared()) {
+            sb.append(PROPERTY_FIELD_PREFIX).append(index).append(".get(")
               .append(INDEX_FIELD_NAME).append(", ").append(SHARED_FIELD_PREFIX)
-              .append(idx).append(");\n\t\treturn ").append(SHARED_FIELD_PREFIX)
-              .append(idx).append(";");
+              .append(index).append(");\n\t\treturn ").append(SHARED_FIELD_PREFIX)
+              .append(index).append(";");
         } else {
-            if (forProperty.getPropertyType().equals(ObjectProperty.class) &&
+            if (forProperty.getPropertyImplementation().equals(OBJECT_PROP_NAME) &&
                 !useGenerics) {
                 // special case where we allow ObjectProperty to have more permissive getters
                 // and setters to support any type under the sun, but that means we have
                 // to cast the object we get back
-                sb.append("return (").append(safeName(forProperty.getType())).append(") ")
-                  .append(PROPERTY_FIELD_PREFIX).append(idx).append(".get(")
+                sb.append("return (").append(forProperty.getType()).append(") ")
+                  .append(PROPERTY_FIELD_PREFIX).append(index).append(".get(")
                   .append(INDEX_FIELD_NAME).append(");");
             } else {
-                sb.append("return ").append(PROPERTY_FIELD_PREFIX).append(idx)
+                sb.append("return ").append(PROPERTY_FIELD_PREFIX).append(index)
                   .append(".get(").append(INDEX_FIELD_NAME).append(");");
             }
         }
 
         sb.append("\n\t}\n");
-        return true;
     }
 
     /**
-     * Append the generated declaration and definition for the given setter method, using
-     * the component specification in <var>spec</var>.
+     * Append the setter method definition given the property declarations that correspond
+     * to the method and its parameters.
      *
-     * @param setter The setter method to append
+     * @param params The properties mutated and that define the parameters of the setter
+     *               method
      * @param spec   The spec for the component type being generated
      * @param sb     The buffer to append to
-     *
-     * @return True if the method was a valid setter, or false if it wasn't and no body
-     *         was appended
      */
-    private static boolean appendSetter(Method setter, List<PropertySpecification> spec,
-                                        StringBuilder sb) {
-        List<PropertySpecification> params = findPropertiesForSetter(setter, spec);
-        if (params == null) {
-            return false;
-        }
+    private static void appendSetter(List<PropertyDeclaration> params,
+                                     ComponentSpecification spec, StringBuilder sb) {
+        // order by parameter index
+        Collections.sort(params, new Comparator<PropertyDeclaration>() {
+            @Override
+            public int compare(PropertyDeclaration o1, PropertyDeclaration o2) {
+                return o1.getSetterParameter() - o2.getSetterParameter();
+            }
+        });
 
-        boolean returnComponent = !setter.getReturnType().equals(void.class);
+        List<? extends PropertyDeclaration> properties = spec.getProperties();
+        String name = params.get(0).getSetterMethod();
+        boolean returnComponent = params.get(0).getSetterReturnsComponent();
 
         // complete method signature
         if (returnComponent) {
-            sb.append("\n\tpublic ").append(safeName(setter.getReturnType()));
+            sb.append("\n\tpublic ").append(spec.getType());
         } else {
             sb.append("\n\tpublic void");
         }
-        sb.append(' ').append(setter.getName()).append('(');
+        sb.append(' ').append(name).append('(');
 
         // with its possibly many parameters
         boolean first = true;
-        for (PropertySpecification p : params) {
+        for (PropertyDeclaration p : params) {
             if (first) {
                 first = false;
             } else {
                 sb.append(", ");
             }
-            sb.append(safeName(p.getType())).append(' ').append(SETTER_PARAM_PREFIX)
-              .append(spec.indexOf(p));
+            sb.append(p.getType()).append(' ').append(SETTER_PARAM_PREFIX)
+              .append(properties.indexOf(p));
         }
         sb.append(") {\n");
 
         // implement the body
-        for (PropertySpecification p : params) {
-            int idx = spec.indexOf(p);
+        for (PropertyDeclaration p : params) {
+            int idx = properties.indexOf(p);
             sb.append("\t\t").append(PROPERTY_FIELD_PREFIX).append(idx).append(".set(")
               .append(SETTER_PARAM_PREFIX).append(idx).append(", ")
               .append(INDEX_FIELD_NAME).append(");\n");
             sb.append("\t\treturn this;\n");
         }
         sb.append("\t}\n");
-        return true;
-    }
-
-    /**
-     * Get the property specification that matches the given getter method. The
-     * specification is selected from the provided list, which is assumed to be the
-     * complete spec.
-     *
-     * @param getter The getter method
-     * @param spec   The complete specification for a type
-     *
-     * @return The matching property, or null if no property matches (i.e. it's not a
-     *         valid getter method)
-     */
-    private static PropertySpecification findPropertyForGetter(Method getter,
-                                                               List<PropertySpecification> spec) {
-        for (PropertySpecification s : spec) {
-            if (s.getGetterMethod().equals(getter)) {
-                return s;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Get every property specification from the given setter method. The specifications
-     * will be selected from the provided list, which is assumed to be the complete spec.
-     * The returned list will be ordered by the setter parameter of each property in the
-     * provided method.
-     *
-     * @param setter The setter method
-     * @param spec   The complete specification for a type
-     *
-     * @return The properties attached to the setter, ordered by their parameter index, or
-     *         null if no properties match (i.e. it's not a valid setter method)
-     */
-    private static List<PropertySpecification> findPropertiesForSetter(Method setter,
-                                                                       List<PropertySpecification> spec) {
-        List<PropertySpecification> matches = new ArrayList<PropertySpecification>();
-        for (PropertySpecification s : spec) {
-            if (s.getSetterMethod().equals(setter)) {
-                matches.add(s);
-            }
-        }
-
-        if (matches.isEmpty()) {
-            return null;
-        } else {
-            Collections.sort(matches, new Comparator<PropertySpecification>() {
-                @Override
-                public int compare(PropertySpecification o1, PropertySpecification o2) {
-                    return o1.getSetterParameter() - o2.getSetterParameter();
-                }
-            });
-            return matches;
-        }
     }
 }

File src/main/java/com/lhkbob/entreri/impl/ComponentRepository.java

View file
             requiredTypes = new Class[0];
         }
 
-        List<PropertySpecification> spec = factory.getSpecification();
-
-        declaredProperties = new ArrayList<DeclaredPropertyStore<?>>();
-        decoratedProperties = new ArrayList<DecoratedPropertyStore<?>>(); // empty for now
-        for (PropertySpecification p : spec) {
-            DeclaredPropertyStore store = new DeclaredPropertyStore(p.getFactory(),
-                                                                    p.getName());
+        declaredProperties = new ArrayList<>();
+        decoratedProperties = new ArrayList<>(); // empty for now
+        for (PropertyDeclaration p : factory.getSpecification().getProperties()) {
+            DeclaredPropertyStore store = new DeclaredPropertyStore(
+                    p.getPropertyFactory(), p.getName());
             declaredProperties.add(store);
         }
 
                                                  : declaredProperties.get(0).property
                             .getCapacity());
         P prop = factory.create();
-        DecoratedPropertyStore<P> pstore = new DecoratedPropertyStore<P>(factory, prop);
+        DecoratedPropertyStore<P> pstore = new DecoratedPropertyStore<>(factory, prop);
 
         // Set values from factory to all component slots
         prop.setCapacity(size);
 
         public DecoratedPropertyStore(PropertyFactory<P> creator, P property) {
             super(creator);
-            this.property = new WeakReference<P>(property);
+            this.property = new WeakReference<>(property);
         }
 
         @Override

File src/main/java/com/lhkbob/entreri/impl/ComponentSpecification.java

View file
+package com.lhkbob.entreri.impl;
+
+import com.lhkbob.entreri.Component;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.TypeElement;
+import java.util.List;
+
+/**
+ *
+ */
+public interface ComponentSpecification {
+    public String getType();
+
+    public String getPackage();
+
+    public List<? extends PropertyDeclaration> getProperties();
+
+    public static final class Factory {
+        private Factory() {
+        }
+
+        public static ComponentSpecification fromClass(Class<? extends Component> type) {
+            return new ReflectionComponentSpecification(type);
+        }
+
+        public static ComponentSpecification fromTypeElement(TypeElement type,
+                                                             ProcessingEnvironment env) {
+            return new MirrorComponentSpecification(type, env.getTypeUtils(),
+                                                    env.getElementUtils(),
+                                                    env.getFiler());
+        }
+    }
+}

File src/main/java/com/lhkbob/entreri/impl/JaninoFactoryProvider.java

View file
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
-import java.util.List;
 
 /**
  * JaninoFactoryProvider is a fallback component provider that uses the Janino runtime
 public class JaninoFactoryProvider extends ComponentFactoryProvider {
     @Override
     public <T extends Component> Factory<T> getFactory(Class<T> componentType) {
-        return new JaninoFactory<T>(componentType);
+        return new JaninoFactory<>(componentType);
     }
 
     private static class JaninoFactory<T extends Component> implements Factory<T> {
         final Class<? extends AbstractComponent<T>> implType;
-        final List<PropertySpecification> specification;
+        final ComponentSpecification specification;
 
         final Constructor<? extends AbstractComponent<T>> ctor;
 
         @SuppressWarnings("unchecked")
         public JaninoFactory(Class<T> type) {
-            String implName = getImplementationClassName(type, true);
-            specification = PropertySpecification.getSpecification(type);
+            specification = ComponentSpecification.Factory.fromClass(type);
+            String implName = getImplementationClassName(specification, true);
 
             // make sure to not use generics since that is not supported by janino
-            String source = generateJavaCode(type, specification, false);
+            String source = generateJavaCode(specification, false);
             SimpleCompiler compiler = new SimpleCompiler();
             compiler.setParentClassLoader(getClass().getClassLoader());
 
             try {
                 compiler.cook(source);
-            } catch (CompileException e) {
-                throw new RuntimeException(
-                        "Unexpected runtime compilation failure for " + type +
-                        ", source:\n" + source, e);
-            } catch (NoClassDefFoundError e) {
+            } catch (CompileException | NoClassDefFoundError e) {
                 throw new RuntimeException(
                         "Unexpected runtime compilation failure for " + type +
                         ", source:\n" + source, e);
         public AbstractComponent<T> newInstance(ComponentRepository<T> forRepository) {
             try {
                 return ctor.newInstance(forRepository);
-            } catch (InstantiationException e) {
-                throw new RuntimeException(
-                        "Exception instantiating generated component impl", e);
-            } catch (IllegalAccessException e) {
-                throw new RuntimeException(
-                        "Exception instantiating generated component impl", e);
-            } catch (InvocationTargetException e) {
+            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                 throw new RuntimeException(
                         "Exception instantiating generated component impl", e);
             }
         }
 
         @Override
-        public List<PropertySpecification> getSpecification() {
+        public ComponentSpecification getSpecification() {
             return specification;
         }
     }

File src/main/java/com/lhkbob/entreri/impl/MirrorComponentSpecification.java

View file
+package com.lhkbob.entreri.impl;
+
+import com.lhkbob.entreri.*;
+import com.lhkbob.entreri.property.*;
+
+import javax.annotation.processing.Filer;
+import javax.lang.model.element.*;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+import javax.tools.FileObject;
+import javax.tools.StandardLocation;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ *
+ */
+public class MirrorComponentSpecification implements ComponentSpecification {
+    private final String typeName;
+    private final String packageName;
+    private final List<MirrorPropertyDeclaration> properties;
+
+    public MirrorComponentSpecification(TypeElement type, Types tu, Elements eu,
+                                        Filer io) {
+        TypeMirror baseComponentType = eu
+                .getTypeElement(Component.class.getCanonicalName()).asType();
+        TypeMirror ownerType = eu.getTypeElement(Owner.class.getCanonicalName()).asType();
+        TypeMirror ownableType = eu.getTypeElement(Ownable.class.getCanonicalName())
+                                   .asType();
+        TypeMirror objectType = eu.getTypeElement(Object.class.getCanonicalName())
+                                  .asType();
+
+        if (!tu.isAssignable(type.asType(), baseComponentType)) {
+            throw fail(type.asType(), "Class must extend Component");
+        }
+        if (!type.getKind().equals(ElementKind.INTERFACE)) {
+            throw fail(type.asType(), "Component definition must be an interface");
+        }
+
+        List<MirrorPropertyDeclaration> properties = new ArrayList<>();
+
+        // since this is an interface, we're only dealing with public methods
+        // so getMethods() returns everything we're interested in plus the methods
+        // declared in Component, which we'll have to exclude
+        List<? extends Element> methods = eu.getAllMembers(type);
+        Map<String, ExecutableElement> getters = new HashMap<>();
+        Map<String, ExecutableElement> setters = new HashMap<>();
+        Map<String, Integer> setterParameters = new HashMap<>();
+
+        for (int i = 0; i < methods.size(); i++) {
+            if (!methods.get(i).getKind().equals(ElementKind.METHOD)) {
+                // skip anything else, theoretically they can define inner static classes
+                // or enums, which is perfectly valid
+                continue;
+            }
+
+            // exclude methods defined in Component, Owner, and Ownable
+            ExecutableElement m = (ExecutableElement) methods.get(i);
+            String name = m.getSimpleName().toString();
+            TypeMirror declare = m.getEnclosingElement().asType();
+
+            if (tu.isSameType(declare, baseComponentType) ||
+                tu.isSameType(declare, ownableType) ||
+                tu.isSameType(declare, ownerType) ||
+                tu.isSameType(declare, objectType)) {
+                continue;
+            }
+
+            if (!tu.isAssignable(declare, baseComponentType)) {
+                throw fail(declare, name + ", method is not declared in Component");
+            }
+
+            if (name.startsWith("is")) {
+                processGetter(m, "is", getters);
+            } else if (name.startsWith("has")) {
+                processGetter(m, "has", getters);
+            } else if (name.startsWith("get")) {
+                processGetter(m, "get", getters);
+            } else if (name.startsWith("set")) {
+                processSetter(m, setters, setterParameters, tu);
+            } else {
+                throw fail(declare, name + " is an illegal property method");
+            }
+        }
+
+        for (String property : getters.keySet()) {
+            ExecutableElement getter = getters.get(property);
+            ExecutableElement setter = setters.remove(property);
+            Integer param = setterParameters.remove(property);
+
+            if (setter == null) {
+                throw fail(type.asType(), property + " has no matching setter");
+            } else if (!tu.isSameType(getter.getReturnType(),
+                                      setter.getParameters().get(param).asType())) {
+                throw fail(type.asType(), property + " has inconsistent type");
+            }
+
+            TypeMirror propertyType = getPropertyType(getter, tu, eu, io);
+            properties.add(new MirrorPropertyDeclaration(property, getter, setter, param,
+                                                         propertyType));
+        }
+
+        if (!setters.isEmpty()) {
+            throw fail(type.asType(), setters.keySet() + " have no matching getters");
+        }
+
+        // order the list of properties by their natural ordering
+        Collections.sort(properties);
+
+        String qualifiedName = type.getQualifiedName().toString();
+        String packageName = eu.getPackageOf(type).getQualifiedName().toString();
+        if (packageName.isEmpty()) {
+            typeName = qualifiedName;
+            this.packageName = "";
+        } else {
+            typeName = qualifiedName.substring(packageName.length() + 1);
+            this.packageName = packageName;
+        }
+
+        this.properties = Collections.unmodifiableList(properties);
+    }
+
+    @Override
+    public String getType() {
+        return typeName;
+    }
+
+    @Override
+    public String getPackage() {
+        return packageName;
+    }
+
+    @Override
+    public List<? extends PropertyDeclaration> getProperties() {
+        return properties;
+    }
+
+    private static IllegalComponentDefinitionException fail(TypeMirror type, String msg) {
+        return new IllegalComponentDefinitionException(type.toString(), msg);
+    }
+
+    private static class MirrorPropertyDeclaration implements PropertyDeclaration {
+        private final String name;
+
+        private final String setter;
+        private final int setterParameter;
+        private final boolean setterReturnsComponent;
+
+        private final String getter;
+        private final boolean isSharedInstance;
+
+        private final String type;
+        private final String propertyType;
+
+        public MirrorPropertyDeclaration(String name, ExecutableElement getter,
+                                         ExecutableElement setter, int parameter,
+                                         TypeMirror propertyType) {
+            this.name = name;
+            this.getter = getter.getSimpleName().toString();
+            this.setter = setter.getSimpleName().toString();
+            this.propertyType = propertyType.toString();
+            setterParameter = parameter;
+
+            type = getter.getReturnType().toString();
+            setterReturnsComponent = !setter.getReturnType().getKind()
+                                            .equals(TypeKind.VOID);
+
+            isSharedInstance = getter.getAnnotation(SharedInstance.class) != null;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getType() {
+            return type;
+        }
+
+        @Override
+        public String getPropertyImplementation() {
+            return propertyType;
+        }
+
+        @Override
+        public String getSetterMethod() {
+            return setter;
+        }
+
+        @Override
+        public String getGetterMethod() {
+            return getter;
+        }
+
+        @Override
+        public int getSetterParameter() {
+            return setterParameter;
+        }
+
+        @Override
+        public boolean getSetterReturnsComponent() {
+            return setterReturnsComponent;
+        }
+
+        @Override
+        public boolean isShared() {
+            return isSharedInstance;
+        }
+
+        @Override
+        public PropertyFactory<?> getPropertyFactory() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int compareTo(PropertyDeclaration o) {
+            return name.compareTo(o.getName());
+        }
+    }
+
+    private static void processSetter(ExecutableElement m,
+                                      Map<String, ExecutableElement> setters,
+                                      Map<String, Integer> parameters, Types tu) {
+        TypeMirror declaringClass = m.getEnclosingElement().asType();
+        if (!tu.isSameType(m.getReturnType(), m.getEnclosingElement().asType()) &&
+            !m.getReturnType().getKind().equals(TypeKind.VOID)) {
+            throw fail(declaringClass, m + " has invalid return type for setter");
+        }
+
+        List<? extends VariableElement> params = m.getParameters();
+        if (params.isEmpty()) {
+            throw fail(declaringClass, m + " must have at least one parameter");
+        }
+
+        if (params.size() == 1) {
+            String name = getNameFromParameter(params.get(0));
+            if (name != null) {
+                // verify absence of @Named on actual setter
+                if (m.getAnnotation(Named.class) != null) {
+                    throw fail(declaringClass,
+                               m + ", @Named cannot be on both parameter and method");
+                }
+            } else {
+                name = getName(m, "set");
+            }
+
+            if (setters.containsKey(name)) {
+                throw fail(declaringClass, name + " already declared on a setter");
+            }
+            setters.put(name, m);
+            parameters.put(name, 0);
+        } else {
+            // verify absence of @Named on actual setter
+            if (m.getAnnotation(Named.class) != null) {
+                throw fail(declaringClass, m +
+                                           ", @Named cannot be applied to setter method with multiple parameters");
+            }
+
+            int i = 0;
+            for (VariableElement p : params) {
+                String name = getNameFromParameter(p);
+                if (name == null) {
+                    throw fail(declaringClass, m +
+                                               ", @Named must be applied to each parameter for multi-parameter setter methods");
+                }
+
+                if (setters.containsKey(name)) {
+                    throw fail(declaringClass, name + " already declared on a setter");
+                }
+
+                setters.put(name, m);
+                parameters.put(name, i++);
+            }
+        }
+    }
+
+    private static void processGetter(ExecutableElement m, String prefix,
+                                      Map<String, ExecutableElement> getters) {
+        TypeMirror declaringClass = m.getEnclosingElement().asType();
+
+        String name = getName(m, prefix);
+        if (getters.containsKey(name)) {
+            throw fail(declaringClass, name + " already declared on a getter");
+        }
+        if (!m.getParameters().isEmpty()) {
+            throw fail(declaringClass, m + ", getter must not take arguments");
+        }
+        if (m.getReturnType().getKind().equals(TypeKind.VOID)) {
+            throw fail(declaringClass, m + ", getter must have non-void return type");
+        }
+
+        getters.put(name, m);
+    }
+
+    private static String getNameFromParameter(VariableElement parameter) {
+        Named n = parameter.getAnnotation(Named.class);
+        if (n != null) {
+            return n.value();
+        } else {
+            return null;
+        }
+    }
+
+    private static String getName(ExecutableElement m, String prefix) {
+        Named n = m.getAnnotation(Named.class);
+        if (n != null) {
+            return n.value();
+        } else {
+            String name = m.getSimpleName().toString();
+            return Character.toLowerCase(name.charAt(prefix.length())) +
+                   name.substring(prefix.length() + 1);
+        }
+    }
+
+    private static TypeMirror getPropertyType(ExecutableElement getter, Types tu,
+                                              Elements eu, Filer io) {
+        TypeMirror baseType = getter.getReturnType();
+
+        TypeMirror factory = getFactory(getter);
+        if (factory != null) {
+            // prefer getter specification to allow default overriding
+            return validateFactory(getter, factory, null, tu, eu);
+        } else {
+            // try to find a default property type
+            TypeElement mappedType;
+            switch (baseType.getKind()) {
+            case BOOLEAN:
+                mappedType = eu.getTypeElement(BooleanProperty.class.getCanonicalName());
+                break;
+            case BYTE:
+                mappedType = eu.getTypeElement(ByteProperty.class.getCanonicalName());
+                break;
+            case CHAR:
+                mappedType = eu.getTypeElement(CharProperty.class.getCanonicalName());
+                break;
+            case DOUBLE:
+                mappedType = eu.getTypeElement(DoubleProperty.class.getCanonicalName());
+                break;
+            case FLOAT:
+                mappedType = eu.getTypeElement(FloatProperty.class.getCanonicalName());
+                break;
+            case INT:
+                mappedType = eu.getTypeElement(IntProperty.class.getCanonicalName());
+                break;
+            case LONG:
+                mappedType = eu.getTypeElement(LongProperty.class.getCanonicalName());
+                break;
+            case SHORT:
+                mappedType = eu.getTypeElement(ShortProperty.class.getCanonicalName());
+                break;
+            default:
+                FileObject mapping;
+                try {
+                    //                    System.out.println(
+                    //                            "Searching for file " + TypePropertyMapping.MAPPING_DIR +
+                    //                            baseType.toString());
+                    mapping = io.getResource(StandardLocation.CLASS_PATH, "",
+                                             TypePropertyMapping.MAPPING_DIR +
+                                             baseType.toString());
+                    //                    System.out.println("Search successful: " +
+                    //                                       (mapping == null ? "null" : mapping.getName()));
+                } catch (IOException e) {
+                    // if an IO is thrown here, it means it couldn't find the file
+                    //                    System.out.println("IO Exception during search");
+                    mapping = null;
+                }
+
+                if (mapping != null) {
+                    try {
+                        String content = mapping.getCharContent(true).toString().trim();
+                        //                        System.out.println("Content of mapping file: " + content);
+                        mappedType = eu.getTypeElement(content);
+                    } catch (IOException e) {
+                        // if an IO is thrown here, however, it means errors accessing
+                        // the file, which we can't recover from
+                        throw new RuntimeException(e);
+                    }
+                } else {
+                    //                    System.out.println("Falling back to ObjectProperty");
+                    mappedType = eu
+                            .getTypeElement(ObjectProperty.class.getCanonicalName());
+                }
+            }
+
+            //            System.out.println("Mapped type for " + baseType + " is " + mappedType);
+            factory = getFactory(mappedType);
+            if (factory == null) {
+                throw fail(getter.getEnclosingElement().asType(),
+                           mappedType + " has no @Factory annotation");
+            } else {
+                return validateFactory(getter, factory, mappedType, tu, eu);
+            }
+        }
+    }
+
+    private static TypeMirror getFactory(Element e) {
+        try {
+            com.lhkbob.entreri.property.Factory f = e
+                    .getAnnotation(com.lhkbob.entreri.property.Factory.class);
+            if (f != null) {
+                f.value(); // will throw an exception
+            }
+            return null;
+        } catch (MirroredTypeException me) {
+            return me.getTypeMirror();
+        }
+    }
+
+    private static final EnumSet<TypeKind> PRIMITIVES = EnumSet
+            .of(TypeKind.BOOLEAN, TypeKind.BYTE, TypeKind.CHAR, TypeKind.DOUBLE,
+                TypeKind.FLOAT, TypeKind.INT, TypeKind.LONG, TypeKind.SHORT);
+
+    private static TypeMirror validateFactory(ExecutableElement getter,
+                                              TypeMirror factory,
+                                              TypeElement propertyType, Types tu,
+                                              Elements eu) {
+        TypeMirror declaringClass = getter.getEnclosingElement().asType();
+        boolean isShared = getter.getAnnotation(SharedInstance.class) != null;
+        TypeMirror baseType = getter.getReturnType();
+
+        TypeMirror createdType = null;
+        List<? extends ExecutableElement> factoryMethods = ElementFilter
+                .methodsIn(eu.getAllMembers((TypeElement) tu.asElement(factory)));
+        for (ExecutableElement m : factoryMethods) {
+            if (m.getSimpleName().contentEquals("create")) {
+                createdType = m.getReturnType();
+                break;
+            }
+        }
+        if (createdType == null) {
+            throw fail(declaringClass, factory + " is missing create() method");
+        }
+
+        if (propertyType == null) {
+            // rely on factory to determine property type
+            propertyType = (TypeElement) tu.asElement(createdType);
+        } else {
+            // make sure factory returns an assignable type
+            if (!tu.isAssignable(createdType, propertyType.asType())) {
+                throw fail(declaringClass, "Factory creates " + createdType +
+                                           ", which is incompatible with expected type " +
+                                           propertyType);
+            }
+        }
+
+        // verify contract of property
+        TypeMirror asType = propertyType.asType();
+        if (tu.isSameType(asType,
+                          eu.getTypeElement(ObjectProperty.class.getCanonicalName())
+                            .asType())) {
+            // special case for ObjectProperty to support more permissive assignments
+            // (which to record requires a similar special case in the code generation)
+            if (isShared) {
+                throw fail(declaringClass,
+                           propertyType + " can't be used with @SharedInstance");
+            } else if (PRIMITIVES.contains(asType.getKind())) {
+                throw fail(declaringClass,
+                           "ObjectProperty cannot be used with primitive types");
+            }
+            // else we know ObjectProperty is defined correctly because its part of the core library
+        } else {
+            TypeMirror intType = tu.getPrimitiveType(TypeKind.INT);
+            TypeMirror voidType = tu.getNoType(TypeKind.VOID);
+            List<? extends ExecutableElement> methods = ElementFilter
+                    .methodsIn(eu.getAllMembers(propertyType));
+
+            if (!findMethod(methods, tu, "get", baseType, intType)) {
+                throw fail(declaringClass,
+                           propertyType + " does not implement " + baseType + " get()");
+            }
+            // FIXME switch back to int, type method but then we have to update all the property defs
+            if (!findMethod(methods, tu, "set", voidType, baseType, intType)) {
+                throw fail(declaringClass,
+                           propertyType + " does not implement void set(" + baseType +
+                           ", int)");
+            }
+
+            if (isShared) {
+                // we could instantiate the declared type, but that crashes if the parameter
+                // type must be a primitive, so the erased type gives us a good enough check
+                TypeMirror share = tu.erasure(
+                        eu.getTypeElement(ShareableProperty.class.getCanonicalName())
+                          .asType());
+                if (!tu.isAssignable(asType, share)) {
+                    throw fail(declaringClass,
+                               propertyType + " can't be used with @SharedInstance");
+                }
+
+                // verify additional shareable property contract
+                if (!findMethod(methods, tu, "get", voidType, intType, baseType)) {
+                    throw fail(declaringClass,
+                               propertyType + " does not implement void get(int, " +
+                               baseType + ")");
+                }
+                if (!findMethod(methods, tu, "createShareableInstance", baseType)) {
+                    throw fail(declaringClass,
+                               propertyType + " does not implement " + baseType +
+                               " createShareableInstance()");
+                }
+            }
+        }
+
+        return tu.erasure(asType);
+    }
+
+    private static boolean findMethod(List<? extends ExecutableElement> methods, Types tu,
+                                      String name, TypeMirror returnType,
+                                      TypeMirror... params) {
+        //        System.out.print("Searching for method: " + returnType + " " + name +
+        //                         Arrays.toString(params));
+        for (ExecutableElement m : methods) {
+            if (m.getSimpleName().contentEquals(name) &&
+                tu.isSameType(returnType, m.getReturnType())) {
+                // now check parameters
+                List<? extends VariableElement> realParams = m.getParameters();
+                if (params.length == realParams.size()) {
+                    boolean found = true;
+                    for (int i = 0; i < params.length; i++) {
+                        if (!tu.isSameType(params[i], realParams.get(i).asType())) {
+                            found = false;
+                            break;
+                        }
+                    }
+
+                    if (found) {
+                        //                        System.out.println(" found!");
+                        return true;
+                    }
+                }
+            }
+        }
+
+        //        System.out.println(" not found :(");
+        return false;
+    }
+}

File src/main/java/com/lhkbob/entreri/impl/PropertyDeclaration.java

View file
+package com.lhkbob.entreri.impl;
+
+import com.lhkbob.entreri.property.PropertyFactory;
+
+/**
+ *
+ */
+public interface PropertyDeclaration extends Comparable<PropertyDeclaration> {
+    public String getName();
+
+    public String getType();
+
+    public String getPropertyImplementation();
+
+    public String getSetterMethod();
+
+    public String getGetterMethod();
+
+    public int getSetterParameter();
+
+    public boolean getSetterReturnsComponent();
+
+    public boolean isShared();
+
+    public PropertyFactory<?> getPropertyFactory();
+}

File src/main/java/com/lhkbob/entreri/impl/PropertySpecification.java

-/*
- * Entreri, an entity-component framework in Java
- *
- * Copyright (c) 2012, Michael Ludwig
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- *     Redistributions of source code must retain the above copyright notice,
- *         this list of conditions and the following disclaimer.
- *     Redistributions in binary form must reproduce the above copyright notice,
- *         this list of conditions and the following disclaimer in the
- *         documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.lhkbob.entreri.impl;
-
-import com.lhkbob.entreri.*;
-import com.lhkbob.entreri.property.*;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Constructor;
-import java.lang.reflect.Method;
-import java.util.*;
-
-/**
- * PropertySpecification instances hold the required information to implement the accessor
- * and mutator methods for a single property declared in a component type. Instances are
- * not created directly, but are gotten by {@link #getSpecification(Class)} that analyzes
- * and validates a component type.
- *
- * @author Michael Ludwig
- */
-@SuppressWarnings("unchecked")
-public final class PropertySpecification implements Comparable<PropertySpecification> {
-    private final String name;
-    private final PropertyFactory<?> factory;
-
-    private final Method setter;
-    private final int setterParameter;
-
-    private final Method getter;
-    private final boolean isSharedInstance;
-
-    private final Class<? extends Property> propertyType;
-
-    private PropertySpecification(String name, PropertyFactory<?> factory, Method getter,
-                                  Method setter, int setterParameter) {
-        this.name = name;
-        this.factory = factory;
-        this.getter = getter;
-        this.setter = setter;
-        this.setterParameter = setterParameter;
-        isSharedInstance = getter.getAnnotation(SharedInstance.class) != null;
-
-        propertyType = getCreatedType(
-                (Class<? extends PropertyFactory<?>>) factory.getClass());
-    }
-
-    /**
-     * @return The class type of returned by the getter, required by the setter, and
-     *         effective data type of the property.
-     */
-    public Class<?> getType() {
-        return getter.getReturnType();
-    }
-
-    /**
-     * @return The property implementation class that will store the property data, and is
-     *         the type created by the associated property factory
-     */
-    public Class<? extends Property> getPropertyType() {
-        return propertyType;
-    }
-
-    /**
-     * @return True if the property uses an internal shared instance
-     */
-    public boolean isSharedInstance() {
-        return isSharedInstance;
-    }
-
-    /**
-     * @return The name of the property
-     */
-    public String getName() {
-        return name;
-    }
-
-    /**
-     * @return The property factory instance that can create properties for systems that
-     *         use this component type
-     */
-    public PropertyFactory<?> getFactory() {
-        return factory;
-    }
-
-    /**
-     * @return The setter method that mutates the property
-     */
-    public Method getSetterMethod() {
-        return setter;
-    }
-
-    /**
-     * @return The specific parameter index of the setter's arguments that has the
-     *         property's value (will always be 0 for a single-argument setter)
-     */
-    public int getSetterParameter() {
-        return setterParameter;
-    }
-
-    /**
-     * @return The getter method that accesses the property
-     */
-    public Method getGetterMethod() {
-        return getter;
-    }
-
-    @Override
-    public int compareTo(PropertySpecification o) {
-        return name.compareTo(o.name);
-    }
-
-    /**
-     * Analyze the given component class and return the property specification. This will
-     * validate the class as well and throw an exception if it violates any of the rules
-     * specified in {@link com.lhkbob.entreri.Component}. Each PropertySpecification
-     * instance in the returned list will have a unique name. The list will be ordered by
-     * PropertySpecification's natural ordering and is immutable.
-     *
-     * @param componentDefinition The component type to analyze
-     *
-     * @return The ordered set of properties declared in the component
-     *
-     * @throws com.lhkbob.entreri.IllegalComponentDefinitionException
-     *          if the component class or any referenced PropertyFactories or Properties
-     *          are invalid
-     */
-    public static List<PropertySpecification> getSpecification(
-            Class<? extends Component> componentDefinition) {
-        if (!Component.class.isAssignableFrom(componentDefinition)) {
-            throw new IllegalArgumentException("Class must extend Component");
-        }
-        if (!componentDefinition.isInterface()) {
-            throw new IllegalComponentDefinitionException(componentDefinition,
-                                                          "Component definition must be an interface");
-        }
-
-        List<PropertySpecification> properties = new ArrayList<PropertySpecification>();
-
-        // since this is an interface, we're only dealing with public methods
-        // so getMethods() returns everything we're interested in plus the methods
-        // declared in Component, which we'll have to exclude
-        Method[] methods = componentDefinition.getMethods();
-        Map<String, Method> getters = new HashMap<String, Method>();
-        Map<String, Method> setters = new HashMap<String, Method>();
-        Map<String, Integer> setterParameters = new HashMap<String, Integer>();
-
-        for (int i = 0; i < methods.length; i++) {
-            // exclude methods defined in Component, Owner, and Ownable
-            Class<?> md = methods[i].getDeclaringClass();
-            if (md.equals(Component.class) || md.equals(Owner.class) ||
-                md.equals(Ownable.class)) {
-                continue;
-            }
-
-            if (!Component.class.isAssignableFrom(methods[i].getDeclaringClass())) {
-                throw new IllegalComponentDefinitionException(componentDefinition,
-                                                              "Method is defined in non-Component interface: " +
-                                                              md);
-            }
-
-            if (methods[i].getName().startsWith("is")) {
-                processGetter(methods[i], "is", getters);
-            } else if (methods[i].getName().startsWith("has")) {
-                processGetter(methods[i], "has", getters);
-            } else if (methods[i].getName().startsWith("get")) {
-                processGetter(methods[i], "get", getters);
-            } else if (methods[i].getName().startsWith("set")) {
-                processSetter(methods[i], setters, setterParameters);
-            } else {
-                throw new IllegalComponentDefinitionException(
-                        (Class<? extends Component>) md,
-                        "Illegal property method: " + methods[i]);
-            }
-        }
-
-        for (String property : getters.keySet()) {
-            Method getter = getters.get(property);
-            Method setter = setters.remove(property);
-            Integer param = setterParameters.remove(property);
-
-            if (setter == null) {
-                throw new IllegalComponentDefinitionException(componentDefinition,
-                                                              "No setter for property: " +
-                                                              property);
-            } else if (!setter.getParameterTypes()[param]
-                    .equals(getter.getReturnType())) {
-                throw new IllegalComponentDefinitionException(componentDefinition,
-                                                              "Mismatched type between getter and setter for: " +
-                                                              property);
-            }
-
-            properties.add(new PropertySpecification(property,
-                                                     createFactory(getters.get(property)),
-                                                     getter, setter, param));
-        }
-
-        if (!setters.isEmpty()) {
-            throw new IllegalComponentDefinitionException(componentDefinition,
-                                                          "No getter for properties: " +
-                                                          setters.keySet());
-        }
-
-        // order the list of properties by their natural ordering
-        Collections.sort(properties);
-        return Collections.unmodifiableList(properties);
-    }
-
-    /**
-     * Validate the method, which is assumed to be a bean mutator method starting with
-     * 'set'. . If the method is valid, it will be placed in the <var>setters</var> map by
-     * the determined name of the property. The method parameter is placed into
-     * <var>parameters</var> with the same key.
-     *
-     * @param m          The getter method
-     * @param setters    The name to mutator map that will be updated
-     * @param parameters The name to index map to be updated
-     *
-     * @throws IllegalComponentDefinitionException
-     *          if the method is invalid
-     */
-    private static void processSetter(Method m, Map<String, Method> setters,
-                                      Map<String, Integer> parameters) {
-        Class<? extends Component> cType = (Class<? extends Component>) m
-                .getDeclaringClass();
-        if (!m.getReturnType().equals(m.getDeclaringClass()) &&
-            !m.getReturnType().equals(void.class)) {
-            throw new IllegalComponentDefinitionException(cType,
-                                                          "Setter method must have a return type equal to its declaring class, or return void: " +
-                                                          m.getName());
-        }
-        if (m.getParameterTypes().length == 0) {
-            throw new IllegalComponentDefinitionException(cType,
-                                                          "Setter method must have at least 1 parameter: " +
-                                                          m.getName());
-        }
-
-        if (m.getParameterTypes().length == 1) {
-            String name = getNameFromParameter(m, 0);
-            if (name != null) {
-                // verify absence of @Named on actual setter
-                if (m.getAnnotation(Named.class) != null) {
-                    throw new IllegalComponentDefinitionException(cType,
-                                                                  "@Named cannot be on both parameter and method: " +
-                                                                  m.getName());
-                }
-            } else {
-                name = getName(m, "set");
-            }
-
-            if (setters.containsKey(name)) {
-                throw new IllegalComponentDefinitionException(cType,
-                                                              "Property name collision: " +
-                                                              name);
-            }
-            setters.put(name, m);
-            parameters.put(name, 0);
-        } else {
-            // verify absence of @Named on actual setter
-            if (m.getAnnotation(Named.class) != null) {
-                throw new IllegalComponentDefinitionException(cType,
-                                                              "@Named cannot be applied to setter method with multiple parameters: " +
-                                                              m.getName());
-            }
-
-            int numP = m.getGenericParameterTypes().length;
-            for (int i = 0; i < numP; i++) {
-                String name = getNameFromParameter(m, i);
-                if (name == null) {
-                    throw new IllegalComponentDefinitionException(cType,
-                                                                  "@Named must be applied to each parameter for setter methods with multiple parameters: " +
-                                                                  m.getName());
-                }
-
-                if (setters.containsKey(name)) {
-                    throw new IllegalComponentDefinitionException(cType,
-                                                                  "Property name collision: " +
-                                                                  name);
-                }
-
-                setters.put(name, m);
-                parameters.put(name, i);
-            }
-        }
-    }
-
-    /**
-     * Validate the method, which is assumed to be a bean accessor method starting with
-     * 'get', 'is', or 'has'. The correct prefix must be passed in through
-     * <var>prefix</var>. If the method is valid, it will be placed in the
-     * <var>getters</var> map by the determined name of the property.
-     *
-     * @param m       The getter method
-     * @param prefix  The prefix of the getter method
-     * @param getters The name to accessor map that will be updated
-     *
-     * @throws IllegalComponentDefinitionException
-     *          if the method is invalid
-     */
-    private static void processGetter(Method m, String prefix,
-                                      Map<String, Method> getters) {
-        Class<? extends Component> cType = (Class<? extends Component>) m
-                .getDeclaringClass();
-        String name = getName(m, prefix);
-        if (getters.containsKey(name)) {
-            throw new IllegalComponentDefinitionException(cType,
-                                                          "Property name collision: " +
-                                                          name);
-        }
-        if (m.getParameterTypes().length != 0) {
-            throw new IllegalComponentDefinitionException(cType,
-                                                          "Getter method cannot take arguments: " +
-                                                          m.getName());
-        }
-        if (m.getReturnType().equals(void.class)) {
-            throw new IllegalComponentDefinitionException(cType,
-                                                          "Getter method must have a non-void return type: " +
-                                                          m.getName());
-        }
-
-        getters.put(name, m);
-    }
-
-    /**
-     * Extract the property name from a {@link Named} annotation applied to the
-     * <var>p</var>th parameter. Null is returned if the parameter has not been annotated.
-     * This is intended for setter methods.
-     *
-     * @param m The method to inspect
-     * @param p The specific parameter whose name should be returned
-     *
-     * @return The name if it exists
-     */
-    private static String getNameFromParameter(Method m, int p) {
-        Annotation[] annots = m.getParameterAnnotations()[p];
-        for (int i = 0; i < annots.length; i++) {
-            if (annots[i] instanceof Named) {
-                return ((Named) annots[i]).value();
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Extract the bean name from the method given the prefix, which is assumed to be
-     * 'set', 'get', 'is', or 'has'. If the method is annotated with {@link Named}, that
-     * overrides the bean name.
-     *
-     * @param m      The bean method
-     * @param prefix The prefix the method name starts with
-     *
-     * @return The name for the property corresponding to the method
-     */
-    private static String getName(Method m, String prefix) {
-        Named n = m.getAnnotation(Named.class);
-        if (n != null) {
-            return n.value();
-        } else {
-            return Character.toLowerCase(m.getName().charAt(prefix.length())) +
-                   m.getName().substring(prefix.length() + 1);
-        }
-    }
-
-    /**
-     * Look up the Property class created by the factory class by inspecting its
-     * <code>create()</code> method.
-     *
-     * @param factory The property factory to inspect
-     *
-     * @return The Property type the factory creates
-     */
-    private static Class<? extends Property> getCreatedType(
-            Class<? extends PropertyFactory<?>> factory) {
-        try {
-            return (Class<? extends Property>) factory.getMethod("create")
-                                                      .getReturnType();
-        } catch (NoSuchMethodException e) {
-            throw new RuntimeException("Cannot inspect property factory " + factory, e);
-        }
-    }
-
-    /**
-     * Determine and instantiate a PropertyFactory that is capable of handling the data
-     * type of the getter method. The getter method is accessor bean method of one of the
-     * high-level properties defined in the component type.
-     * <p/>
-     * If the getter is annotated with {@link Factory}, that factory is used. Otherwise
-     * the return type is used with {@link TypePropertyMapping} to find the appropriate
-     * Property class. The {@link Factory} annotation on that Property is then used for
-     * the factory type.
-     * <p/>
-     * Validation is performed to ensure the factory creates properties of the expected
-     * type, and the property exposes the methods expected by the proxy code generation.
-     *
-     * @param getter The getter of the property
-     *
-     * @return The constructed property factory
-     *
-     * @throws IllegalComponentDefinitionException
-     *          if the property factory or getter has an invalid configuration
-     */
-    private static PropertyFactory<?> createFactory(Method getter) {
-        Class<?> baseType = getter.getReturnType();
-        Class<? extends Component> cType = (Class<? extends Component>) getter
-                .getDeclaringClass();
-
-        Class<? extends PropertyFactory<?>> factoryType;
-        if (getter.getAnnotation(Factory.class) != null) {
-            // prefer gett