1. Michael Ludwig
  2. entreri

Commits

Michael Ludwig  committed d840e5a

Fix bugs in PropertySpecification, and almost finish Java source generation.

  • Participants
  • Parent commits 430e0ba
  • Branches default

Comments (0)

Files changed (9)

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

View file
 /**
  * AbstractComponent is the base class used for all generated proxy implementations of
  * component subtypes. It provides an implementation for all of the declared methods in
- * Component as well as equals() and hashCode().
+ * Component as well as equals() and hashCode(). It should not be subclassed or extended
+ * directly. As specified in {@link Component}, all component type definitions are
+ * sub-interfaces of Component.
  *
  * @param <T> The type of component the AbstractComponent is safely cast-able to
  */

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

View file
 package com.lhkbob.entreri;
 
-import java.util.List;
-import java.util.Set;
+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.*;
 
 /**
  *
  */
 abstract class ComponentFactoryProvider {
+    private static final Charset CHARSET = Charset.forName("UTF-8");
+    private static final String ABSTRACT_COMPONENT_NAME = AbstractComponent.class
+            .getSimpleName();
+    private static final String COMPONENT_REPO_NAME = ComponentRepository.class
+            .getSimpleName();
+    private static final String PROXY_PACKAGE_NAME = ComponentFactoryProvider.class
+            .getPackage().getName();
+
     public static interface Factory<T extends Component> {
         public AbstractComponent<T> newInstance(ComponentRepository<T> forRepository);
 
     public abstract <T extends Component> Factory<T> getFactory(Class<T> componentType);
 
     public static ComponentFactoryProvider getInstance() {
-        // FIXME impl
-        throw new UnsupportedOperationException();
+        return new ComponentFactoryProvider() {
+            @Override
+            public <T extends Component> Factory<T> getFactory(Class<T> componentType) {
+                List<PropertySpecification> spec = PropertySpecification
+                        .getSpecification(componentType);
+                // FIXME impl
+                throw new UnsupportedOperationException(
+                        generateJavaCode(componentType, spec));
+            }
+        };
+    }
+
+    private static String safeName(Class<?> type) {
+        return type.getName().replace('$', '.');
+    }
+
+    public static String getImplementationClassName(Class<? extends Component> type,
+                                                    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("[\\.$]", "");
+
+        // compute a unique hash on the original canonical class name to guarantee
+        // the uniqueness of the generated class as well
+        long hash;
+        try {
+            MessageDigest md = MessageDigest.getInstance("MD5");
+            md.update(type.getCanonicalName().getBytes(CHARSET));
+
+            ByteBuffer md5 = ByteBuffer.wrap(md.digest());
+            hash = Math.abs(md5.getLong() ^ md5.getLong());
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException("JVM does not support MD5", e);
+        }
+
+        StringBuilder sb = new StringBuilder().append(scrubbed).append("Impl")
+                                              .append(hash);
+        // FIXME is this worth it in the method signature?
+        if (includePackage) {
+            sb.append(PROXY_PACKAGE_NAME);
+        }
+        return sb.toString();
     }
 
     public static String generateJavaCode(Class<? extends Component> type,
                                           List<PropertySpecification> spec) {
-        String implName = type.getSimpleName() + "Impl";
+        String baseTypeName = safeName(type);
 
-        // FIXME what package do we put the generated classes in?
-        // FIXME if AbstractComponent remains package-private then we can't put them
-        // FIXME in the package of the incoming type
+        String implName = getImplementationClassName(type, false);
+
+        // place the implementation in the same package as the original type
         StringBuilder sb = new StringBuilder();
-        sb.append("package ").append(type.getPackage().getName()).append(";\n")
-          .append("public class ").append(implName)
-          .append(" extends com.lhkbob.entreri.AbstractComponent implements ")
-          .append(type.getName()).append("{\n");
-        // FIXME can't just use class.getName(), have to replace the $ with a .
+        sb.append("package ").append(PROXY_PACKAGE_NAME).append(";\n")
+          .append("public class ").append(implName).append(" extends ")
+                // FIXME I'm not sure if Janino supports generics
+                .append(ABSTRACT_COMPONENT_NAME).append('<').append(baseTypeName)
+                .append("> implements ").append(baseTypeName).append("{\n");
 
-        // FIXME handle shared instances needing members, and multi-param setters
-        int i = 0;
+        // add any shared instance field declarations
+        int property = 0;
         for (PropertySpecification s : spec) {
-            // FIXME nicer access to this
-            Class<?> pt = s.getGetterMethod().getReturnType();
-            // FIXME handle return this component setters as well
-            sb.append("\tpublic void ").append(s.getSetterMethod().getName()).append('(')
-              .append(pt.getName()).append(' ').append(s.getName()).append(") {\n")
-              .append("\t\towner.getProperty(").append(i).append(").set(")
-              .append(s.getName()).append(", getIndex());\n\t}\n\n");
-            sb.append("\tpublic ").append(pt.getName()).append(' ')
-              .append(s.getGetterMethod().getName()).append("() {\n")
-              .append("\t\treturn owner.getProperty(").append(i)
-              .append(").get(getIndex());\n\t}\n\n");
-            i++;
+            if (s.isSharedInstance()) {
+                sb.append("\tprivate final ").append(safeName(s.getPropertyType()))
+                  .append(" sharedInstance").append(property).append(";\n");
+            }
+            property++;
         }
+
+        // define the constructor, must invoke super and allocate shared instances
+        // FIXME make owner a magic constant and sharedInstance a magic constant
+        sb.append("\n\tpublic ").append(implName).append("(").append(COMPONENT_REPO_NAME)
+          .append('<').append(baseTypeName).append("> owner) {\n")
+          .append("\t\tsuper(owner);\n");
+        property = 0;
+        for (PropertySpecification s : spec) {
+            if (s.isSharedInstance()) {
+                // FIXME need to add getProperty() to ComponentRepository, and a createShareableInstance(),
+                // FIXME unless I want to create the objects via reflection ...
+                sb.append("sharedInstance").append(property).append(" = ")
+                  .append("owner.getProperty(").append(property)
+                  .append(").createShareableInstance();\n");
+            }
+            property++;
+        }
+        sb.append("\t}\n");
+
+        // implement all methods of the interface
+        for (Method m : type.getMethods()) {
+            // FIXME ugly
+            if (m.getDeclaringClass().equals(Component.class) ||
+                m.getDeclaringClass().equals(Owner.class) ||
+                m.getDeclaringClass().equals(Ownable.class) ||
+                m.getDeclaringClass().equals(Object.class) ||
+                m.getDeclaringClass().equals(AbstractComponent.class)) {
+                continue;
+            }
+            if (!appendGetter(m, spec, sb)) {
+                if (!appendSetter(m, spec, sb)) {
+                    throw new IllegalStateException(
+                            "Unexpected method during code generation: " + m);
+                }
+            }
+        }
+
         sb.append("}\n");
         return sb.toString();
     }
+
+    private static boolean appendGetter(Method getter, List<PropertySpecification> spec,
+                                        StringBuilder sb) {
+        PropertySpecification forProperty = findPropertyForGetter(getter, spec);
+        if (forProperty == null) {
+            return false;
+        }
+
+        // method signature
+        int idx = spec.indexOf(forProperty);
+        sb.append("\n\tpublic ").append(safeName(forProperty.getPropertyType()))
+          .append(" ").append(getter.getName()).append("() {\n\t\t");
+
+        // implementation body, depending on if we use a shared instance variable or not
+        if (forProperty.isSharedInstance()) {
+            sb.append("owner.getProperty(").append(idx).append(").get(getIndex(), ")
+              .append("sharedInstance").append(idx)
+              .append(");\n\t\treturn sharedInstance").append(idx).append(";");
+        } else {
+            sb.append("return owner.getProperty(").append(idx)
+              .append(").get(getIndex());");
+        }
+
+        sb.append("\n\t}\n");
+        return true;
+    }
+
+    private static boolean appendSetter(Method setter, List<PropertySpecification> spec,
+                                        StringBuilder sb) {
+        List<PropertySpecification> params = findPropertiesForSetter(setter, spec);
+        if (params == null) {
+            return false;
+        }
+
+        boolean returnComponent = !setter.getReturnType().equals(void.class);
+
+        // complete method signature
+        if (returnComponent) {
+            sb.append("\n\tpublic ").append(safeName(setter.getReturnType()));
+        } else {
+            sb.append("\n\tpublic void");
+        }
+        sb.append(' ').append(setter.getName()).append('(');
+
+        // with its possibly many parameters
+        boolean first = true;
+        for (PropertySpecification p : params) {
+            if (first) {
+                first = false;
+            } else {
+                sb.append(", ");
+            }
+            sb.append(safeName(p.getPropertyType())).append(" prop")
+              .append(spec.indexOf(p));
+        }
+        sb.append(") {\n");
+
+        // implement the body
+        for (PropertySpecification p : params) {
+            int idx = spec.indexOf(p);
+            sb.append("\t\towner.getProperty(").append(idx).append(").set(prop")
+              .append(idx).append(", getIndex());\n");
+        }
+
+        // return this component if we're not a void setter
+        if (returnComponent) {
+            sb.append("\t\treturn this;\n");
+        }
+        sb.append("\t}\n");
+        return true;
+    }
+
+    private static PropertySpecification findPropertyForGetter(Method getter,
+                                                               List<PropertySpecification> spec) {
+        for (PropertySpecification s : spec) {
+            if (s.getGetterMethod().equals(getter)) {
+                return s;
+            }
+        }
+        return null;
+    }
+
+    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/PropertySpecification.java

View file
         isSharedInstance = isShared(getter);
     }
 
+    public Class<?> getPropertyType() {
+        return getter.getReturnType();
+    }
+
     public boolean isSharedInstance() {
         return isSharedInstance;
     }
         }
 
         for (String property : getters.keySet()) {
-            if (!setters.containsKey(property)) {
+            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),
-                                                                   property),
-                                                     getters.get(property),
-                                                     setters.remove(property),
-                                                     setterParameters.remove(property)));
+                                                                   property), getter,
+                                                     setter, param));
         }
 
         if (!setters.isEmpty()) {
 
     private static void validateFactory(Method getter, boolean isShared,
                                         Class<? extends PropertyFactory<?>> factory,
-                                        Class<? extends Property> propertyType) {
+                                        Class<? extends Property> propertyType,
+                                        Class<? extends Component> forType) {
         Class<?> baseType = getter.getReturnType();
         Class<? extends Property> createdType;
         try {
         }
 
         // verify contract of property
+        // FIXME we need to handle parameterized property types a bit better
+        if (propertyType.equals(ObjectProperty.class)) {
+            return;
+        }
+
         try {
             Method g = propertyType.getMethod("get", int.class);
             if (!g.getReturnType().equals(baseType)) {
-                throw new RuntimeException(propertyType +
-                                           " does not implement required get() method for type " +
-                                           baseType);
+                throw new IllegalComponentDefinitionException(forType,
+                                                              "Does not implement required get() method for type " +
+                                                              baseType);
             }
             // FIXME switch back to int, type method but then we have to update all the property defs
             Method s = propertyType.getMethod("set", baseType, int.class);
             if (!s.getReturnType().equals(void.class)) {
-                throw new RuntimeException(propertyType +
-                                           " does not implement required set() method for type " +
-                                           baseType);
+                throw new IllegalComponentDefinitionException(forType,
+                                                              " does not implement required set() method for type " +
+                                                              baseType);
             }
         } catch (NoSuchMethodException e) {
-            throw new RuntimeException(propertyType +
-                                       " does not implement required get() or set() method for type " +
-                                       baseType, e);
+            throw new IllegalComponentDefinitionException(forType,
+                                                          " does not implement required get() or set() method for type " +
+                                                          baseType);
         }
 
         if (isShared) {
     private static PropertyFactory<?> createFactory(Method getter, String name) {
         boolean isShared = isShared(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 getter specification to allow default overriding
             factoryType = getter.getAnnotation(Factory.class).value();
-            validateFactory(getter, isShared, factoryType, null);
+            validateFactory(getter, isShared, factoryType, null, cType);
         } else {
             // try to find a default property type
             Class<? extends Property> mappedType = TypePropertyMapping
                     .getPropertyForType(baseType);
             if (mappedType.getAnnotation(Factory.class) == null) {
-                throw new IllegalComponentDefinitionException(
-                        (Class<? extends Component>) getter.getDeclaringClass(),
-                        "Cannot create PropertyFactory for " +
-                        mappedType +
-                        ", no @Factory annotation on type");
+                throw new IllegalComponentDefinitionException(cType,
+                                                              "Cannot create PropertyFactory for " +
+                                                              mappedType +
+                                                              ", no @Factory annotation on type");
             } else {
                 factoryType = mappedType.getAnnotation(Factory.class).value();
-                validateFactory(getter, isShared, factoryType, mappedType);
+                validateFactory(getter, isShared, factoryType, mappedType, cType);
             }
         }
 
 
         if (factory == null) {
             // unable to create a PropertyFactory
-            throw new IllegalComponentDefinitionException(
-                    (Class<? extends Component>) getter.getDeclaringClass(),
-                    "Unable to create PropertyFactory for " + name);
+            throw new IllegalComponentDefinitionException(cType,
+                                                          "Unable to create PropertyFactory for " +
+                                                          name);
         } else {
             return factory;
         }

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

View file
  */
 @Documented
 @Target(ElementType.METHOD)
-@Retention(RetentionPolicy.SOURCE)
+@Retention(RetentionPolicy.RUNTIME)
 public @interface SharedInstance {
 
 }

File src/test/java/com/lhkbob/entreri/PropertySpecificationTest.java

View file
         doInvalidComponentDefinitionTest(MissingGetterComponent.class);
         doInvalidComponentDefinitionTest(MismatchedNameComponent.class);
         doInvalidComponentDefinitionTest(MismatchedTypeComponent.class);
-        doValidComponentDefinitionTest(NonBeanComponent.class);
+        doInvalidComponentDefinitionTest(NonBeanComponent.class);
         doInvalidComponentDefinitionTest(InvalidPropertyComponent.class);
     }
 
         Assert.assertEquals(0f, floatProp.get(0), 0.0001f);
         Assert.assertEquals(FloatComponent.class.getMethod("getFloat"),
                             floatSpec.getGetterMethod());
-        Assert.assertEquals(FloatComponent.class.getMethod("setFloat", int.class),
+        Assert.assertEquals(FloatComponent.class.getMethod("setFloat", float.class),
                             floatSpec.getSetterMethod());
         Assert.assertEquals(0, floatSpec.getSetterParameter());
 

File src/test/java/com/lhkbob/entreri/component/ComplexComponent.java

View file
     public boolean isNamedParamGetter();
 
     @Named("foo-blah")
-    public ComplexComponent setNamedParamSetter(int foo);
+    public ComplexComponent setNamedParamSetter(boolean foo);
 
     @DefaultInt(14)
     @SharedInstance

File src/test/java/com/lhkbob/entreri/property/CustomProperty.java

View file
         @Override
         public void setDefaultValue(CustomProperty property, int index) {
             Bletch b = new Bletch();
-            b.value = (attributes.hasAttribute(IntProperty.DefaultInt.class) ? 0
-                                                                             : attributes
+            b.value = (!attributes.hasAttribute(IntProperty.DefaultInt.class) ? 0
+                                                                              : attributes
                                .getAttribute(IntProperty.DefaultInt.class).value());
             property.property.set(b, index);
         }

File src/test/java/com/lhkbob/entreri/property/entreri-mapping.properties

-com.lhkbob.entreri.property.CustomProperty.Bletch=com.lhkbob.property.CustomProperty
-com.lhkbob.entreri.property.NoFactoryProperty.Crass=com.lhkbob.property.NoFactoryProperty

File src/test/resources/com/lhkbob/entreri/property/entreri-mapping.properties

View file
+com.lhkbob.entreri.property.CustomProperty$Bletch=com.lhkbob.entreri.property.CustomProperty
+com.lhkbob.entreri.property.NoFactoryProperty$Crass=com.lhkbob.entreri.property.NoFactoryProperty