From f5b4096362414d5819b18547505984c776d09dc7 Mon Sep 17 00:00:00 2001 From: elaatifi Date: Thu, 10 Nov 2011 15:21:25 +0000 Subject: [PATCH] Add ObjectFactory generator SUpport constructor class mapping git-svn-id: https://orika.googlecode.com/svn/trunk@238 f4a4e40d-1d15-de9b-c3e8-c5ea35f5a4c4 --- core/pom.xml | 5 + .../java/ma/glasnost/orika/MapperFactory.java | 5 + .../PickingConstructorStrategy.java | 10 + .../SimplePickingConstructorStrategy.java | 20 ++ .../orika/impl/CodeSourceBuilder.java | 182 +++++++++++---- .../orika/impl/DefaultMapperFactory.java | 219 +++++++++++------- .../orika/impl/GeneratedObjectFactory.java | 13 ++ .../orika/impl/ObjectFactoryGenerator.java | 197 ++++++++++++++++ .../ma/glasnost/orika/metadata/ClassMap.java | 36 ++- .../orika/metadata/ClassMapBuilder.java | 34 ++- .../ConstructorMappingTestCase.java | 120 ++++++++++ pom.xml | 8 + 12 files changed, 694 insertions(+), 155 deletions(-) create mode 100644 core/src/main/java/ma/glasnost/orika/constructor/PickingConstructorStrategy.java create mode 100644 core/src/main/java/ma/glasnost/orika/constructor/SimplePickingConstructorStrategy.java create mode 100644 core/src/main/java/ma/glasnost/orika/impl/GeneratedObjectFactory.java create mode 100644 core/src/main/java/ma/glasnost/orika/impl/ObjectFactoryGenerator.java create mode 100644 core/src/test/java/ma/glasnost/orika/test/constructor/ConstructorMappingTestCase.java diff --git a/core/pom.xml b/core/pom.xml index 04f5dec8..b477677c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -27,6 +27,11 @@ javassist + + com.thoughtworks.paranamer + paranamer + + junit junit diff --git a/core/src/main/java/ma/glasnost/orika/MapperFactory.java b/core/src/main/java/ma/glasnost/orika/MapperFactory.java index fd3c5215..36cd6b34 100644 --- a/core/src/main/java/ma/glasnost/orika/MapperFactory.java +++ b/core/src/main/java/ma/glasnost/orika/MapperFactory.java @@ -56,4 +56,9 @@ public interface MapperFactory { void build(); Set> lookupUsedClassMap(MapperKey mapperKey); + + ClassMap getClassMap(MapperKey mapperKey); + + Set> lookupMappedClasses(Class clazz); + } diff --git a/core/src/main/java/ma/glasnost/orika/constructor/PickingConstructorStrategy.java b/core/src/main/java/ma/glasnost/orika/constructor/PickingConstructorStrategy.java new file mode 100644 index 00000000..5c84b04d --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/constructor/PickingConstructorStrategy.java @@ -0,0 +1,10 @@ +package ma.glasnost.orika.constructor; + +import java.lang.reflect.Constructor; + +import ma.glasnost.orika.metadata.ClassMap; + +public interface PickingConstructorStrategy { + + Constructor pick(ClassMap classMap, Class sourceClass); +} diff --git a/core/src/main/java/ma/glasnost/orika/constructor/SimplePickingConstructorStrategy.java b/core/src/main/java/ma/glasnost/orika/constructor/SimplePickingConstructorStrategy.java new file mode 100644 index 00000000..ad1b670e --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/constructor/SimplePickingConstructorStrategy.java @@ -0,0 +1,20 @@ +package ma.glasnost.orika.constructor; + +import java.lang.reflect.Constructor; + +import ma.glasnost.orika.metadata.ClassMap; + +public class SimplePickingConstructorStrategy implements PickingConstructorStrategy { + + @SuppressWarnings({ "unchecked" }) + public Constructor pick(ClassMap classMap, Class sourceClass) { + boolean aToB = classMap.getBType().equals(sourceClass); + // String[] argumentNames = aToB ? classMap.getConstructorB() : + // classMap.getConstructorA(); + Class targetClass = aToB ? classMap.getBType() : classMap.getAType(); + + // TODO to specify + return (Constructor) targetClass.getConstructors()[0]; + + } +} diff --git a/core/src/main/java/ma/glasnost/orika/impl/CodeSourceBuilder.java b/core/src/main/java/ma/glasnost/orika/impl/CodeSourceBuilder.java index b51a13c8..c1c4805d 100644 --- a/core/src/main/java/ma/glasnost/orika/impl/CodeSourceBuilder.java +++ b/core/src/main/java/ma/glasnost/orika/impl/CodeSourceBuilder.java @@ -32,11 +32,11 @@ public class CodeSourceBuilder { private int currentIndent = 1; public CodeSourceBuilder(int indent) { - this.currentIndent = indent; + this.currentIndent = indent; } public CodeSourceBuilder assertType(String var, Class clazz) { - newLine(); + newLine(); append("if(!(" + var + " instanceof ").append(clazz.getName()).append(")) "); begin(); append("throw new IllegalStateException(\"[" + var + "] is not an instance of " + clazz.getName() + " \");"); @@ -46,18 +46,16 @@ public CodeSourceBuilder assertType(String var, Class clazz) { public CodeSourceBuilder convert(Property destination, Property source) { - final String getter = getGetter(source); + final String getter = getGetter(source); final String setter = getSetter(destination); final Class destinationClass = destination.getType(); - return newLine() - .append("destination.%s((%s)mapperFacade.convert(source.%s, %s.class));", setter, destinationClass.getName(), getter, - destinationClass.getName()) - .newLine(); + return newLine().append("destination.%s((%s)mapperFacade.convert(source.%s, %s.class));", setter, destinationClass.getName(), + getter, destinationClass.getName()).newLine(); } public CodeSourceBuilder set(Property d, Property s) { - final String getter = getGetter(s); + final String getter = getGetter(s); final String setter = getSetter(d); return newLine().append("destination.%s(source.%s);", setter, getter); } @@ -65,8 +63,8 @@ public CodeSourceBuilder set(Property d, Property s) { public CodeSourceBuilder setCollection(Property dp, Property sp, Property ip, Class dc) { final Class destinationElementClass = dp.getParameterizedType(); - if (destinationElementClass==null) { - throw new MappingException("cannot determine runtime type of destination collection " + dc.getName() + "." + dp.getName()); + if (destinationElementClass == null) { + throw new MappingException("cannot determine runtime type of destination collection " + dc.getName() + "." + dp.getName()); } String destinationCollection = "List"; @@ -82,30 +80,30 @@ public CodeSourceBuilder setCollection(Property dp, Property sp, Property ip, Cl final String sourceGetter = getGetter(sp); final String destinationGetter = getGetter(dp); final String destinationSetter = getSetter(dp); - boolean destinationHasSetter = false; + boolean destinationHasSetter = false; try { - destinationHasSetter = (dc.getMethod(destinationSetter, dp.getType())!=null); - - } catch (Exception e) { - /* ignored: no destination setter available */ - } + destinationHasSetter = (dc.getMethod(destinationSetter, dp.getType()) != null); + + } catch (Exception e) { + /* ignored: no destination setter available */ + } - if (destinationHasSetter) { - newLine() - .append("if (destination.%s == null) ", destinationGetter) - .begin() - .append("destination.%s(%s);", destinationSetter, newStatement) - .end(); - } + if (destinationHasSetter) { + newLine().append("if (destination.%s == null) ", destinationGetter) + .begin() + .append("destination.%s(%s);", destinationSetter, newStatement) + .end(); + } newLine().append("destination.%s.clear();", destinationGetter); newLine().append("destination.%s.addAll(mapperFacade.mapAs%s(source.%s, %s.class, mappingContext));", destinationGetter, destinationCollection, sourceGetter, destinationElementClass.getName()); if (ip != null) { if (ip.isCollection()) { - newLine().append("for (java.util.Iterator orikaIterator = destination.%s.iterator(); orikaIterator.hasNext();) ", getGetter(dp)); - begin().append("%s orikaCollectionItem = (%s) orikaIterator.next();", dp.getParameterizedType().getName(), dp - .getParameterizedType().getName()); + newLine().append("for (java.util.Iterator orikaIterator = destination.%s.iterator(); orikaIterator.hasNext();) ", + getGetter(dp)); + begin().append("%s orikaCollectionItem = (%s) orikaIterator.next();", dp.getParameterizedType().getName(), + dp.getParameterizedType().getName()); newLine().append("if (orikaCollectionItem.%s == null) ", getGetter(ip)); begin(); if (ip.isSet()) { @@ -123,9 +121,10 @@ public CodeSourceBuilder setCollection(Property dp, Property sp, Property ip, Cl } else if (ip.isArray()) { // TODO To implement } else { - newLine().append("for (java.util.Iterator orikaIterator = destination.%s.iterator(); orikaIterator.hasNext();)", getGetter(dp)); - begin().append("%s orikaCollectionItem = (%s) orikaIterator.next();", dp.getParameterizedType().getName(), dp - .getParameterizedType().getName()); + newLine().append("for (java.util.Iterator orikaIterator = destination.%s.iterator(); orikaIterator.hasNext();)", + getGetter(dp)); + begin().append("%s orikaCollectionItem = (%s) orikaIterator.next();", dp.getParameterizedType().getName(), + dp.getParameterizedType().getName()); newLine().append("orikaCollectionItem.%s(destination);", getSetter(ip)); end(); } @@ -135,11 +134,11 @@ public CodeSourceBuilder setCollection(Property dp, Property sp, Property ip, Cl } public CodeSourceBuilder newLine() { - out.append("\n"); - for(int i = 0; i < currentIndent; ++i) { - out.append("\t"); - } - return this; + out.append("\n"); + for (int i = 0; i < currentIndent; ++i) { + out.append("\t"); + } + return this; } public CodeSourceBuilder append(String str, Object... args) { @@ -159,7 +158,7 @@ public CodeSourceBuilder then() { } public CodeSourceBuilder begin() { - return then(); + return then(); } public CodeSourceBuilder end() { @@ -186,7 +185,8 @@ public CodeSourceBuilder setWrapper(Property dp, Property sp) { final String getter = getGetter(sp); final String setter = getSetter(dp); - newLine().append("destination.%s(%s.valueOf((%s) source.%s));", setter, dp.getType().getName(), getPrimitiveType(dp.getType()), getter); + newLine().append("destination.%s(%s.valueOf((%s) source.%s));", setter, dp.getType().getName(), getPrimitiveType(dp.getType()), + getter); return this; } @@ -205,31 +205,31 @@ public CodeSourceBuilder setArray(Property dp, Property sp) { final String getter = getGetter(sp); final String setter = getSetter(dp); - newLine().append("%s[] %s = new %s[source.%s.%s];", paramType, dp.getName(), paramType, getter, getSizeCode).append( - "mapperFacade.mapAsArray((Object[])%s, (%s)source.%s, %s.class, mappingContext);", dp.getName(), castSource, getter, - paramType).append("destination.%s(%s);", setter, dp.getName()); + newLine().append("%s[] %s = new %s[source.%s.%s];", paramType, dp.getName(), paramType, getter, getSizeCode) + .append("mapperFacade.mapAsArray((Object[])%s, (%s)source.%s, %s.class, mappingContext);", dp.getName(), castSource, + getter, paramType) + .append("destination.%s(%s);", setter, dp.getName()); return this; } public CodeSourceBuilder setToEnumeration(Property dp, Property sp) { - final String getter = getGetter(sp); + final String getter = getGetter(sp); final String setter = getSetter(dp); - newLine().append("destination.%s((%s)Enum.valueOf(%s.class,\"\"+source.%s));", - setter, dp.getType().getName(), dp.getType().getName(), getter); + newLine().append("destination.%s((%s)Enum.valueOf(%s.class,\"\"+source.%s));", setter, dp.getType().getName(), + dp.getType().getName(), getter); return this; } - public CodeSourceBuilder setObject(Property dp, Property sp, Property ip) { final String sourceGetter = getGetter(sp); final String destinationGetter = getGetter(dp); final String destinationSetter = getSetter(dp); newLine().append("if (destination.%s == null) ", destinationGetter); - begin().append("destination.%s((%s)mapperFacade.map(source.%s, %s.class, mappingContext));", destinationSetter, dp.getType().getName(), - sourceGetter, dp.getType().getName()); + begin().append("destination.%s((%s)mapperFacade.map(source.%s, %s.class, mappingContext));", destinationSetter, + dp.getType().getName(), sourceGetter, dp.getType().getName()); elze(); append("mapperFacade.map(source.%s, destination.%s, mappingContext);", sourceGetter, destinationGetter); end(); @@ -256,11 +256,9 @@ public CodeSourceBuilder setObject(Property dp, Property sp, Property ip) { return this; } - - public CodeSourceBuilder ifSourceNotNull(Property sp) { - newLine(); + newLine(); if (sp.hasPath()) { final StringBuilder sb = new StringBuilder("source"); int i = 0; @@ -293,8 +291,8 @@ public CodeSourceBuilder ifSourceNotNull(Property sp) { * @return CodeSourceBuilder */ public CodeSourceBuilder ifDestinationNull(Property property) { - - if (!property.hasPath()) { + + if (!property.hasPath()) { return this; } @@ -316,6 +314,11 @@ public CodeSourceBuilder ifDestinationNull(Property property) { return this; } + public CodeSourceBuilder ifSourceInstanceOf(Class sourceClass) { + append("if(s instanceof %s)", sourceClass.getName()); + return this; + } + private String getLongGetter(NestedProperty property) { final StringBuilder sb = new StringBuilder(); for (final Property p : property.getPath()) { @@ -352,4 +355,83 @@ private String getPrimitiveType(Class clazz) { } return type; } + + public CodeSourceBuilder declareVar(Class clazz, String var) { + append("\n%s %s = %s;", clazz.getName(), var, clazz.isPrimitive() ? getDefaultPrimtiveValue(clazz) : "null"); + return this; + } + + public CodeSourceBuilder assignImmutableVar(String var, Property sp) { + append("%s = source.%s;", var, getGetter(sp)); + return this; + } + + public CodeSourceBuilder assignObjectVar(String var, Property sp, Class targetClass) { + append("%s = (%s) mapperFacade.map(source.%s, %s.class);", var, targetClass.getName(), getGetter(sp), targetClass.getName()); + return this; + } + + public CodeSourceBuilder assignCollectionVar(String var, Property sp, Property dp) { + final Class destinationElementClass = dp.getParameterizedType(); + String destinationCollection = "List"; + String newStatement = "new java.util.ArrayList()"; + if (List.class.isAssignableFrom(dp.getType())) { + destinationCollection = "List"; + newStatement = "new java.util.ArrayList()"; + } else if (Set.class.isAssignableFrom(dp.getType())) { + destinationCollection = "Set"; + newStatement = "new java.util.HashSet()"; + } + + final String sourceGetter = getGetter(sp); + final String destinationGetter = getGetter(dp); + final String destinationSetter = getSetter(dp); + + append("if (destination.%s == null) {\n", destinationGetter); + append("destination.%s(%s);\n", destinationSetter, newStatement); + append("}\n"); + append("destination.%s.clear();\n", destinationGetter); + append("destination.%s.addAll(mapperFacade.mapAs%s(source.%s, %s.class, mappingContext));", destinationGetter, + destinationCollection, sourceGetter, destinationElementClass.getName()); + return this; + } + + public CodeSourceBuilder assignArrayVar(String var, Property sp, Class targetClass) { + String getter = getGetter(sp); + final String getSizeCode = sp.getType().isArray() ? "length" : "size()"; + final String castSource = sp.getType().isArray() ? "Object[]" : ""; + append("%s[] %s = new %s[source.%s.%s];", targetClass, var, targetClass.getName(), getter, getSizeCode).append( + "mapperFacade.mapAsArray((Object[])%s, (%s)source.%s, %s.class, mappingContext);", var, castSource, getter, + targetClass.getName()); + return this; + } + + public CodeSourceBuilder assignPrimtiveToWrapperVar(String var, Property sp, Class targetClass) { + final String getter = getGetter(sp); + + append("%s = %s.valueOf((%s) source.%s));\n", var, targetClass.getName(), getPrimitiveType(targetClass), getter); + return this; + } + + public CodeSourceBuilder assignWrapperToPrimitiveVar(String var, Property sp, Class targetClass) { + String getter = getGetter(sp); + append("%s = source.%s.%sValue();\n", var, getter, getPrimitiveType(targetClass)); + return this; + } + + public CodeSourceBuilder assignConvertedVar(String var, Property source, Class targetClass) { + final String getter = getGetter(source); + append("%s = ((%s)mapperFacade.convert(source.%s, %s.class)); \n", var, targetClass.getName(), getter, targetClass.getName()); + return this; + + } + + private String getDefaultPrimtiveValue(Class clazz) { + if (Boolean.TYPE.equals(clazz)) + return "false"; + else if (Character.TYPE.equals(clazz)) + return "'\0'"; + else + return "0"; + } } diff --git a/core/src/main/java/ma/glasnost/orika/impl/DefaultMapperFactory.java b/core/src/main/java/ma/glasnost/orika/impl/DefaultMapperFactory.java index 73c2e0f1..e311eade 100644 --- a/core/src/main/java/ma/glasnost/orika/impl/DefaultMapperFactory.java +++ b/core/src/main/java/ma/glasnost/orika/impl/DefaultMapperFactory.java @@ -61,7 +61,9 @@ public class DefaultMapperFactory implements MapperFactory { private final MapperFacade mapperFacade; private final MapperGenerator mapperGenerator; - private final Set> classMaps; + private final ObjectFactoryGenerator objectFactoryGenerator; + + private final Map> classMapRegistry; private final Map mappersRegistry; private final Map> convertersRegistry; private final Map, ObjectFactory> objectFactoryRegistry; @@ -72,11 +74,13 @@ public class DefaultMapperFactory implements MapperFactory { private final Map>> usedMapperMetadataRegistry; - private DefaultMapperFactory(Set> classMaps, UnenhanceStrategy delegateStrategy, - SuperTypeResolverStrategy superTypeStrategy) { + private DefaultMapperFactory(Set> classMaps, UnenhanceStrategy delegateStrategy, + SuperTypeResolverStrategy superTypeStrategy) { this.mapperGenerator = new MapperGenerator(this); - this.classMaps = Collections.synchronizedSet(new HashSet>()); + this.objectFactoryGenerator = new ObjectFactoryGenerator(this); + + this.classMapRegistry = new ConcurrentHashMap>(); this.mappersRegistry = new ConcurrentHashMap(); this.convertersRegistry = new ConcurrentHashMap>(); this.aToBRegistry = new ConcurrentHashMap, Set>>(); @@ -84,7 +88,7 @@ private DefaultMapperFactory(Set> classMaps, UnenhanceStrategy de this.usedMapperMetadataRegistry = new ConcurrentHashMap>>(); this.objectFactoryRegistry = new ConcurrentHashMap, ObjectFactory>(); this.mappingHints = new CopyOnWriteArrayList(); - this.unenhanceStrategy = buildUnenhanceStrategy(delegateStrategy,superTypeStrategy); + this.unenhanceStrategy = buildUnenhanceStrategy(delegateStrategy, superTypeStrategy); this.mapperFacade = new MapperFacadeImpl(this, unenhanceStrategy); if (classMaps != null) { @@ -95,103 +99,115 @@ private DefaultMapperFactory(Set> classMaps, UnenhanceStrategy de } /** - * Construct an instance of DefaultMapperFactory with a default UnenhanceStrategy + * Construct an instance of DefaultMapperFactory with a default + * UnenhanceStrategy */ public DefaultMapperFactory() { - this(null,null,null); + this(null, null, null); } /** - * Constructs an instance of DefaultMapperFactory using the specified UnenhanceStrategy - * @param unenhanceStrategy used to provide custom unenhancement of mapped objects before processing + * Constructs an instance of DefaultMapperFactory using the specified + * UnenhanceStrategy + * + * @param unenhanceStrategy + * used to provide custom unenhancement of mapped objects before + * processing */ public DefaultMapperFactory(UnenhanceStrategy unenhanceStrategy) { - this(null,unenhanceStrategy,null); + this(null, unenhanceStrategy, null); } /** - * Constructs an instance of DefaultMapperFactory using the specified UnenhanceStrategy, - * with the possibility to override the default unenhancement behavior. - * - * @param unenhanceStrategy used to provide custom unenhancement of mapped objects before processing - * @param superTypeStrategy similar to the unenhance strategy, but used when a recommended type - * is not usable (in case it is inaccessible, or similar situation) + * Constructs an instance of DefaultMapperFactory using the specified + * UnenhanceStrategy, with the possibility to override the default + * unenhancement behavior. + * + * @param unenhanceStrategy + * used to provide custom unenhancement of mapped objects before + * processing + * @param superTypeStrategy + * similar to the unenhance strategy, but used when a recommended + * type is not usable (in case it is inaccessible, or similar + * situation) */ public DefaultMapperFactory(UnenhanceStrategy unenhanceStrategy, SuperTypeResolverStrategy superTypeStrategy) { - this(null,unenhanceStrategy,superTypeStrategy); + this(null, unenhanceStrategy, superTypeStrategy); } - /** - * Generates the UnenhanceStrategy to be used for this MapperFactory, applying - * the passed delegateStrategy if not null. + * Generates the UnenhanceStrategy to be used for this MapperFactory, + * applying the passed delegateStrategy if not null. * * @param unenhanceStrategy - * @param overrideDefaultUnenhanceBehavior true if the passed UnenhanceStrategy should take - * full responsibility for un-enhancement; false if the default behavior should be applied as - * a fail-safe after consulting the passed strategy. + * @param overrideDefaultUnenhanceBehavior + * true if the passed UnenhanceStrategy should take full + * responsibility for un-enhancement; false if the default + * behavior should be applied as a fail-safe after consulting the + * passed strategy. * * @return */ - protected UnenhanceStrategy buildUnenhanceStrategy(UnenhanceStrategy unenhanceStrategy, - SuperTypeResolverStrategy superTypeStrategy) { + protected UnenhanceStrategy buildUnenhanceStrategy(UnenhanceStrategy unenhanceStrategy, SuperTypeResolverStrategy superTypeStrategy) { + + BaseUnenhancer unenhancer = new BaseUnenhancer(); + + if (unenhanceStrategy != null) { + unenhancer.addUnenhanceStrategy(unenhanceStrategy); + } else { + // TODO: this delegate strategy may no longer be needed... + try { + Class.forName("org.hibernate.proxy.HibernateProxy"); + unenhancer.addUnenhanceStrategy(new HibernateUnenhanceStrategy()); + } catch (final Throwable e) { + // TODO add warning + } + + } + + /* + * If the passed strategy wants complete control, they can have it + */ + if (superTypeStrategy != null) { + unenhancer.addSuperTypeResolverStrategy(superTypeStrategy); + } else { + + /* + * This strategy attempts to lookup super-type that has a registered + * mapper or converter whenever it is offered a class that is not + * currently mapped + */ + final SuperTypeResolverStrategy registeredMappersStrategy = new DefaultSuperTypeResolverStrategy() { + + @Override + public boolean isAcceptable(Class proposedClass) { + return aToBRegistry.containsKey(proposedClass) || mappedConverters.containsKey(proposedClass); + } + }; + + unenhancer.addSuperTypeResolverStrategy(registeredMappersStrategy); + } + + /* + * This strategy produces super-types whenever the proposed class type + * is not accessible to the (javassist) byte-code generator and/or the + * current thread context class laoder; it is added last as a fail-safe + * in case a suggested type cannot be used. It is automatically + * included, as there's no case when skipping it would be desired.... + */ + final SuperTypeResolverStrategy inaccessibleTypeStrategy = new DefaultSuperTypeResolverStrategy() { + + @Override + public boolean isAcceptable(Class proposedClass) { + return mapperGenerator.isTypeAccessible(proposedClass) && !java.lang.reflect.Proxy.class.equals(proposedClass); + } + + }; + + unenhancer.addSuperTypeResolverStrategy(inaccessibleTypeStrategy); + + return unenhancer; - BaseUnenhancer unenhancer = new BaseUnenhancer(); - - - if (unenhanceStrategy!=null) { - unenhancer.addUnenhanceStrategy(unenhanceStrategy); - } else { - // TODO: this delegate strategy may no longer be needed... - try { - Class.forName("org.hibernate.proxy.HibernateProxy"); - unenhancer.addUnenhanceStrategy(new HibernateUnenhanceStrategy()); - } catch (final Throwable e) { - // TODO add warning - } - - } - - /* - * If the passed strategy wants complete control, they can have it - */ - if (superTypeStrategy!=null) { - unenhancer.addSuperTypeResolverStrategy(superTypeStrategy); - } else { - - /* - * This strategy attempts to lookup super-type that has a registered mapper or converter - * whenever it is offered a class that is not currently mapped - */ - final SuperTypeResolverStrategy registeredMappersStrategy = new DefaultSuperTypeResolverStrategy() { - - public boolean isAcceptable(Class proposedClass) { - return aToBRegistry.containsKey(proposedClass) || - mappedConverters.containsKey(proposedClass); - } - }; - - unenhancer.addSuperTypeResolverStrategy(registeredMappersStrategy); - } - - /* - * This strategy produces super-types whenever the proposed class type is not accessible to - * the (javassist) byte-code generator and/or the current thread context class laoder; - * it is added last as a fail-safe in case a suggested type cannot be used. It is automatically - * included, as there's no case when skipping it would be desired.... - */ - final SuperTypeResolverStrategy inaccessibleTypeStrategy = new DefaultSuperTypeResolverStrategy() { - - public boolean isAcceptable(Class proposedClass) { - return mapperGenerator.isTypeAccessible(proposedClass) && !java.lang.reflect.Proxy.class.equals(proposedClass); - } - - }; - - unenhancer.addSuperTypeResolverStrategy(inaccessibleTypeStrategy); - - return unenhancer; - } public GeneratedMapperBase lookupMapper(MapperKey mapperKey) { @@ -257,19 +273,21 @@ public Class lookupConcreteDestinationClass(Class sourceC return concreteClass; } - public void registerClassMap(ClassMap classMap) { - classMaps.add(classMap); + @SuppressWarnings("unchecked") + public void registerClassMap(ClassMap classMap) { + classMapRegistry.put(new MapperKey(classMap.getAType(), classMap.getBType()), (ClassMap) classMap); } public void build() { buildClassMapRegistry(); - for (final ClassMap classMap : classMaps) { + for (final ClassMap classMap : classMapRegistry.values()) { buildMapper(classMap); } - for (final ClassMap classMap : classMaps) { + for (final ClassMap classMap : classMapRegistry.values()) { + buildObjectFactories(classMap); initializeUsedMappers(classMap); } @@ -287,10 +305,9 @@ private void buildClassMapRegistry() { // prepare a map for classmap (stored as set) Map> classMapsDictionnary = new HashMap>(); - @SuppressWarnings({ "rawtypes", "unchecked" }) - Set> set = (Set) classMaps; + Set> classMaps = new HashSet>(classMapRegistry.values()); - for (final ClassMap classMap : set) { + for (final ClassMap classMap : classMaps) { classMapsDictionnary.put(new MapperKey(classMap.getAType(), classMap.getBType()), classMap); } @@ -311,6 +328,21 @@ private void buildClassMapRegistry() { } + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void buildObjectFactories(ClassMap classMap) { + Class aType = classMap.getAType(); + Class bType = classMap.getBType(); + if (classMap.getConstructorA() != null && lookupObjectFactory(aType) == null) { + GeneratedObjectFactory objectFactory = objectFactoryGenerator.build(aType); + registerObjectFactory(objectFactory, aType); + } + + if (classMap.getConstructorB() != null && lookupObjectFactory(bType) == null) { + GeneratedObjectFactory objectFactory = objectFactoryGenerator.build(bType); + registerObjectFactory(objectFactory, bType); + } + } + @SuppressWarnings("unchecked") private void initializeUsedMappers(ClassMap classMap) { Mapper mapper = lookupMapper(new MapperKey(classMap.getAType(), classMap.getBType())); @@ -360,4 +392,15 @@ private void register(Class sourceClass, Class destinationClass) { } destinationSet.add(destinationClass); } + + @SuppressWarnings("unchecked") + public ClassMap getClassMap(MapperKey mapperKey) { + return (ClassMap) classMapRegistry.get(mapperKey); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Set> lookupMappedClasses(Class clazz) { + return (Set) aToBRegistry.get(clazz); + } + } diff --git a/core/src/main/java/ma/glasnost/orika/impl/GeneratedObjectFactory.java b/core/src/main/java/ma/glasnost/orika/impl/GeneratedObjectFactory.java new file mode 100644 index 00000000..3f48efbb --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/impl/GeneratedObjectFactory.java @@ -0,0 +1,13 @@ +package ma.glasnost.orika.impl; + +import ma.glasnost.orika.MapperFacade; +import ma.glasnost.orika.ObjectFactory; + +public abstract class GeneratedObjectFactory implements ObjectFactory { + protected MapperFacade mapperFacade; + + public void setMapperFacade(MapperFacade mapperFacade) { + this.mapperFacade = mapperFacade; + } + +} diff --git a/core/src/main/java/ma/glasnost/orika/impl/ObjectFactoryGenerator.java b/core/src/main/java/ma/glasnost/orika/impl/ObjectFactoryGenerator.java new file mode 100644 index 00000000..880f168a --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/impl/ObjectFactoryGenerator.java @@ -0,0 +1,197 @@ +package ma.glasnost.orika.impl; + +import static ma.glasnost.orika.impl.Specifications.aCollection; +import static ma.glasnost.orika.impl.Specifications.aPrimitiveToWrapper; +import static ma.glasnost.orika.impl.Specifications.aWrapperToPrimitive; +import static ma.glasnost.orika.impl.Specifications.anArray; +import static ma.glasnost.orika.impl.Specifications.immutable; + +import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javassist.CannotCompileException; +import javassist.ClassClassPath; +import javassist.ClassPool; +import javassist.CtClass; +import javassist.CtNewMethod; +import ma.glasnost.orika.Converter; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.MappingException; +import ma.glasnost.orika.constructor.PickingConstructorStrategy; +import ma.glasnost.orika.constructor.SimplePickingConstructorStrategy; +import ma.glasnost.orika.metadata.ClassMap; +import ma.glasnost.orika.metadata.FieldMap; +import ma.glasnost.orika.metadata.MapperKey; +import ma.glasnost.orika.metadata.Property; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.thoughtworks.paranamer.AdaptiveParanamer; +import com.thoughtworks.paranamer.AnnotationParanamer; +import com.thoughtworks.paranamer.BytecodeReadingParanamer; +import com.thoughtworks.paranamer.CachingParanamer; +import com.thoughtworks.paranamer.Paranamer; + +public class ObjectFactoryGenerator { + + private final static Logger LOG = LoggerFactory.getLogger(ObjectFactoryGenerator.class); + + private final PickingConstructorStrategy pickConstructorStrategy; + private final MapperFactory mapperFactory; + private final ClassPool classPool; + private final Paranamer paranamer; + + public ObjectFactoryGenerator(MapperFactory mapperFactory) { + this.mapperFactory = mapperFactory; + this.classPool = ClassPool.getDefault(); + this.paranamer = new CachingParanamer(new AdaptiveParanamer(new BytecodeReadingParanamer(), new AnnotationParanamer())); + + classPool.insertClassPath(new ClassClassPath(this.getClass())); + + pickConstructorStrategy = new SimplePickingConstructorStrategy(); + } + + public GeneratedObjectFactory build(Class clazz) { + + final CtClass objectFactoryClass = classPool.makeClass(clazz.getSimpleName() + "ObjectFactory" + System.identityHashCode(clazz)); + CtClass abstractObjectFactoryClass = null; + try { + + abstractObjectFactoryClass = classPool.getCtClass(GeneratedObjectFactory.class.getName()); + objectFactoryClass.setSuperclass(abstractObjectFactoryClass); + + addCreateMethod(objectFactoryClass, clazz); + + GeneratedObjectFactory objectFactory = (GeneratedObjectFactory) objectFactoryClass.toClass().newInstance(); + objectFactory.setMapperFacade(mapperFactory.getMapperFacade()); + + return objectFactory; + + } catch (final Exception e) { + throw new MappingException(e); + } finally { + // reduce memory consumption + if (abstractObjectFactoryClass != null) { + abstractObjectFactoryClass.detach(); + } + if (objectFactoryClass != null) { + objectFactoryClass.detach(); + } + } + } + + private void addCreateMethod(CtClass objectFactoryClass, Class clazz) throws CannotCompileException { + final CodeSourceBuilder out = new CodeSourceBuilder(1); + out.append("public Object create(Object s) {"); + out.append("if(s == null) throw new %s(\"[s] must be not null\");", IllegalArgumentException.class.getName()); + + Set> sourceClasses = mapperFactory.lookupMappedClasses(clazz); + + if (sourceClasses != null && !sourceClasses.isEmpty()) { + for (Class sourceClass : sourceClasses) { + addSourceClassConstructor(out, clazz, sourceClass); + } + } + out.append("throw new %s(\"[s] is an unsupported source class : \"+s.getClass().getName());", + IllegalArgumentException.class.getName()); + out.append("\n}"); + + try { + objectFactoryClass.addMethod(CtNewMethod.make(out.toString(), objectFactoryClass)); + } catch (final CannotCompileException e) { + LOG.error("An exception occured while compiling: " + out.toString(), e); + throw e; + } + } + + private void addSourceClassConstructor(CodeSourceBuilder out, Class clazz, Class sourceClass) { + List properties = new ArrayList(); + ClassMap classMap = mapperFactory.getClassMap(new MapperKey(clazz, sourceClass)); + boolean aToB = classMap.getBType().equals(clazz); + + try { + Constructor constructor = pickConstructorStrategy.pick(classMap, clazz); + + String[] parameters = paranamer.lookupParameterNames(constructor); + Class[] constructorArguments = constructor.getParameterTypes(); + + // TODO need optimizations + for (String param : parameters) { + for (FieldMap fieldMap : classMap.getFieldsMapping()) { + if (!aToB) + fieldMap = fieldMap.flip(); + if (param.equals(fieldMap.getDestination().getName())) { + properties.add(fieldMap); + break; + } + } + } + + if (parameters.length != properties.size()) { + throw new MappingException("Can not find all constructor's parameters"); + } + + out.ifSourceInstanceOf(sourceClass).then(); + out.append("%s source = (%s) s;", sourceClass.getName(), sourceClass.getName()); + int argIndex = 0; + for (FieldMap fieldMap : properties) { + + final Property sp = fieldMap.getSource(), dp = fieldMap.getDestination(); + String var = "arg" + argIndex; + Class targetClass = constructorArguments[argIndex]; + out.declareVar(targetClass, var); + argIndex++; + + if (generateConverterCode(out, var, sp, dp)) { + continue; + } + try { + + if (fieldMap.is(immutable())) { + out.ifSourceNotNull(sp).assignImmutableVar(var, sp); + } else if (fieldMap.is(anArray())) { + out.ifSourceNotNull(sp).assignArrayVar(var, sp, targetClass); + } else if (fieldMap.is(aCollection())) { + out.ifSourceNotNull(sp).assignCollectionVar(var, sp, dp); + } else if (fieldMap.is(aWrapperToPrimitive())) { + out.ifSourceNotNull(sp).assignWrapperToPrimitiveVar(var, sp, targetClass); + } else if (fieldMap.is(aPrimitiveToWrapper())) { + out.ifSourceNotNull(sp).assignPrimtiveToWrapperVar(var, sp, targetClass); + } else { /**/ + out.ifSourceNotNull(sp).then().assignObjectVar(var, sp, targetClass).end(); + } + + } catch (final Exception e) { + } + } + + out.append("return new %s", clazz.getName()).append("("); + for (int i = 0; i < properties.size(); i++) { + out.append("arg%d", i); + if (i < properties.size() - 1) { + out.append(","); + } + } + out.append(");").end(); + + System.out.println(out); + } catch (Exception e) { + LOG.warn("Can not find " + clazz.getName() + "constructor's parameters name"); + /* SKIP */ + } + } + + private boolean generateConverterCode(final CodeSourceBuilder code, String var, Property sp, Property dp) { + final Class destinationClass = dp.getType(); + final Converter converter = mapperFactory.lookupConverter(sp.getType(), destinationClass); + if (converter != null) { + code.ifSourceNotNull(sp).then().assignConvertedVar(var, sp, destinationClass).end(); + return true; + } else { + return false; + } + } +} diff --git a/core/src/main/java/ma/glasnost/orika/metadata/ClassMap.java b/core/src/main/java/ma/glasnost/orika/metadata/ClassMap.java index dc04109f..65bb99ee 100644 --- a/core/src/main/java/ma/glasnost/orika/metadata/ClassMap.java +++ b/core/src/main/java/ma/glasnost/orika/metadata/ClassMap.java @@ -25,14 +25,18 @@ public class ClassMap { - final private Class aType; - final private Class bType; - final private Set fieldsMapping; - final private Set usedMappers; + private final Class aType; + private final Class bType; + private final Set fieldsMapping; + private final Set usedMappers; - final private Mapper customizedMapper; + private final Mapper customizedMapper; - public ClassMap(Class aType, Class bType, Set fieldsMapping, Mapper customizedMapper, Set usedMappers) { + private final String[] constructorA; + private final String[] constructorB; + + public ClassMap(Class aType, Class bType, Set fieldsMapping, Mapper customizedMapper, Set usedMappers, + String[] constructorA, String[] constructorB) { this.aType = aType; this.bType = bType; @@ -40,6 +44,18 @@ public ClassMap(Class aType, Class bType, Set fieldsMapping, Map this.fieldsMapping = Collections.unmodifiableSet(fieldsMapping); this.usedMappers = Collections.unmodifiableSet(usedMappers); + + if (constructorA != null) { + this.constructorA = constructorA.clone(); + } else { + this.constructorA = null; + } + + if (constructorB != null) { + this.constructorB = constructorB.clone(); + } else { + this.constructorB = null; + } } public void addFieldMap(FieldMap fieldMap) { @@ -77,6 +93,14 @@ public String getMapperClassName() { return "Orika" + bType.getSimpleName() + getATypeName() + "Mapper" + System.identityHashCode(this); } + public String[] getConstructorA() { + return constructorA; + } + + public String[] getConstructorB() { + return constructorB; + } + @Override public int hashCode() { int result = 31; diff --git a/core/src/main/java/ma/glasnost/orika/metadata/ClassMapBuilder.java b/core/src/main/java/ma/glasnost/orika/metadata/ClassMapBuilder.java index 5c9e7d25..c2af8522 100644 --- a/core/src/main/java/ma/glasnost/orika/metadata/ClassMapBuilder.java +++ b/core/src/main/java/ma/glasnost/orika/metadata/ClassMapBuilder.java @@ -37,6 +37,8 @@ public final class ClassMapBuilder { final private Set fieldsMapping; final private Set usedMappers; private Mapper customizedMapper; + private String[] constructorA; + private String[] constructorB; private ClassMapBuilder(Class aType, Class bType) { @@ -100,20 +102,20 @@ public ClassMapBuilder use(Class aParentClass, Class bParentClass) { return this; } - public ClassMapBuilder byDefault(MappingHint...mappingHints) { + public ClassMapBuilder byDefault(MappingHint... mappingHints) { for (final String propertyName : aProperties.keySet()) { if (!propertiesCache.contains(propertyName)) { - if(bProperties.containsKey(propertyName)) { - fieldMap(propertyName).add(); + if (bProperties.containsKey(propertyName)) { + fieldMap(propertyName).add(); } else { - Property prop = aProperties.get(propertyName); - for (MappingHint hint: mappingHints) { - String suggestion = hint.suggestMappedField(propertyName,prop.getType()); - if (suggestion!=null && bProperties.containsKey(suggestion)) { - fieldMap(propertyName,suggestion).add(); - } - } + Property prop = aProperties.get(propertyName); + for (MappingHint hint : mappingHints) { + String suggestion = hint.suggestMappedField(propertyName, prop.getType()); + if (suggestion != null && bProperties.containsKey(suggestion)) { + fieldMap(propertyName, suggestion).add(); + } + } } } } @@ -122,7 +124,7 @@ public ClassMapBuilder byDefault(MappingHint...mappingHints) { } public ClassMap toClassMap() { - return new ClassMap(aType, bType, fieldsMapping, customizedMapper, usedMappers); + return new ClassMap(aType, bType, fieldsMapping, customizedMapper, usedMappers, constructorA, constructorB); } public static ClassMapBuilder map(Class aType, Class bType) { @@ -176,4 +178,14 @@ void addFieldMap(FieldMap fieldMap) { propertiesCache.add(fieldMap.getSource().getExpression()); } + public ClassMapBuilder constructorA(String... args) { + this.constructorA = args.clone(); + return this; + } + + public ClassMapBuilder constructorB(String... args) { + this.constructorB = args.clone(); + return this; + } + } diff --git a/core/src/test/java/ma/glasnost/orika/test/constructor/ConstructorMappingTestCase.java b/core/src/test/java/ma/glasnost/orika/test/constructor/ConstructorMappingTestCase.java new file mode 100644 index 00000000..c6b6229f --- /dev/null +++ b/core/src/test/java/ma/glasnost/orika/test/constructor/ConstructorMappingTestCase.java @@ -0,0 +1,120 @@ +package ma.glasnost.orika.test.constructor; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import junit.framework.Assert; +import ma.glasnost.orika.Converter; +import ma.glasnost.orika.ConverterException; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.metadata.ClassMapBuilder; +import ma.glasnost.orika.test.MappingUtil; + +import org.junit.Test; + +public class ConstructorMappingTestCase { + + @Test + public void testSimpleCase() throws Throwable { + final SimpleDateFormat df = new SimpleDateFormat("dd/MM/yyyy"); + MapperFactory factory = MappingUtil.getMapperFactory(); + + factory.registerClassMap(ClassMapBuilder.map(PersonVO.class, Person.class) + .constructorA() + .field("dateOfBirth", "date") + .byDefault() + .toClassMap()); + factory.registerConverter(new Converter() { + public String convert(Date source) throws ConverterException { + return df.format(source); + } + }, Date.class, String.class); + + factory.build(); + + Person person = new Person(); + person.setFirstName("Abdelkrim"); + person.setLastName("EL KHETTABI"); + person.setDate(df.parse("01/01/1980")); + person.setAge(31L); + + PersonVO vo = factory.getMapperFacade().map(person, PersonVO.class); + + Assert.assertEquals(person.getFirstName(), vo.getFirstName()); + Assert.assertEquals(person.getLastName(), vo.getLastName()); + Assert.assertTrue(person.getAge() == vo.getAge()); + Assert.assertEquals("01/01/1980", vo.getDateOfBirth()); + } + + public static class Person { + private String firstName; + private String lastName; + + private Long age; + private Date date; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Long getAge() { + return age; + } + + public void setAge(Long age) { + this.age = age; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + } + + public static class PersonVO { + private final String firstName; + private final String lastName; + + private final long age; + private final String dateOfBirth; + + public PersonVO(String firstName, String lastName, long age, String dateOfBirth) { + this.firstName = firstName; + this.lastName = lastName; + this.age = age; + this.dateOfBirth = dateOfBirth; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public long getAge() { + return age; + } + + public String getDateOfBirth() { + return dateOfBirth; + } + } +} diff --git a/pom.xml b/pom.xml index e9737a08..a9a7b11e 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,8 @@ 1.8 1.3.154 1.5.8 + 2.3 + @@ -36,6 +38,12 @@ ${javassist.version} + + com.thoughtworks.paranamer + paranamer + ${paranamer.version} + + junit junit