Repository: orika-mapper/orika
Pull request #52 (2015-01-01 11:19:22)

Add support for Java 8 & Guava Optional classes.
Note that Optional field mappings will need to make use of a converter (see the builtin GuavaOptionalConverter for details).


Patch:
diff --git a/core/pom.xml b/core/pom.xml
index 155bb68d..a3164d6a 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -48,6 +48,12 @@
 			<artifactId>java-sizeof</artifactId>
 		</dependency>
 
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+			<optional>true</optional>
+		</dependency>
+
 		<dependency>
 			<groupId>janino</groupId>
 			<artifactId>janino</artifactId>
diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/GuavaOptionalConverter.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/GuavaOptionalConverter.java
new file mode 100644
index 00000000..26ef5db9
--- /dev/null
+++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/GuavaOptionalConverter.java
@@ -0,0 +1,73 @@
+package ma.glasnost.orika.converter.builtin;
+
+import com.google.common.base.Optional;
+import ma.glasnost.orika.Converter;
+import ma.glasnost.orika.MapperFacade;
+import ma.glasnost.orika.MappingContext;
+import ma.glasnost.orika.metadata.Type;
+import ma.glasnost.orika.metadata.TypeFactory;
+
+/**
+ * <p>
+ *     Converter which will convert one {@linkplain Optional} field into another optional field.
+ * </p>
+ * <p>
+ *     For those that wish to support the Java 8 <code>java.util.Optional</code> class, the code is exactly the
+ *     same as this class apart from the static methods to construct the <code>Optional</code> classes.
+ * </p>
+ * @param <S> Type of the optional field to map from.
+ * @param <D> Type of the optional field to map to.
+ */
+public class GuavaOptionalConverter<S, D> implements Converter<Optional<S>, Optional<D>> {
+
+    private final Type<S> sourceType;
+    private final Type<D> destinationType;
+    private MapperFacade mapper;
+
+    /**
+     * Construct a new {@linkplain Optional} converter configured to convert an {@linkplain Optional} field
+     * to another <code>Optional</code> field.
+     * @param sourceType Type the source {@linkplain Optional} field contains.
+     * @param destinationType Type the destination {@linkplain Optional} field will contain.
+     */
+    public GuavaOptionalConverter(final Type<S> sourceType, final Type<D> destinationType) {
+        this.sourceType = sourceType;
+        this.destinationType = destinationType;
+    }
+
+    public boolean canConvert(final Type<?> sourceType, final Type<?> destinationType) {
+        final Type<?> sourceComponentType = sourceType.getComponentType();
+        final Type<?> destinationComponentType = destinationType.getComponentType();
+
+        return !(sourceComponentType == null || destinationComponentType == null)
+                && this.sourceType.isAssignableFrom(sourceComponentType.getRawType())
+                && this.destinationType.isAssignableFrom(destinationComponentType.getRawType());
+    }
+
+    public Optional<D> convert(final Optional<S> optionalSource, final Type<? extends Optional<D>> destinationType, final MappingContext mappingContext) {
+        if (!optionalSource.isPresent()) {
+            return Optional.absent();
+        }
+
+        final S source = optionalSource.get();
+
+        return Optional.fromNullable(mapper.map(source, sourceType, this.destinationType, mappingContext));
+    }
+
+    public void setMapperFacade(final MapperFacade mapper) {
+        this.mapper = mapper;
+    }
+
+    public Type<Optional<S>> getAType() {
+        return getOptionalTypeOf(sourceType);
+    }
+
+    public Type<Optional<D>> getBType() {
+        return getOptionalTypeOf(destinationType);
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> Type<Optional<T>> getOptionalTypeOf(Type<T> type) {
+        return (Type) TypeFactory.valueOf(Optional.class, type);
+    }
+}
diff --git a/core/src/main/java/ma/glasnost/orika/impl/generator/VariableRef.java b/core/src/main/java/ma/glasnost/orika/impl/generator/VariableRef.java
index 733e3b58..96df339b 100644
--- a/core/src/main/java/ma/glasnost/orika/impl/generator/VariableRef.java
+++ b/core/src/main/java/ma/glasnost/orika/impl/generator/VariableRef.java
@@ -26,6 +26,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import ma.glasnost.orika.Converter;
 import ma.glasnost.orika.Filter;
@@ -49,6 +50,20 @@ import ma.glasnost.orika.property.PropertyResolverStrategy;
  * 
  */
 public class VariableRef {
+
+    private static final Set<Class<?>> OPTIONAL_CLASSES = Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>());
+    static {
+        try {
+            OPTIONAL_CLASSES.add(Class.forName("java.util.Optional"));
+        } catch (ClassNotFoundException e) {
+            // No j.u.Optional...
+        }
+        try {
+            OPTIONAL_CLASSES.add(Class.forName("com.google.common.base.Optional"));
+        } catch (ClassNotFoundException e) {
+            // No Guava Optional...
+        }
+    }
     
     protected String name;
     private Property property;
@@ -133,7 +148,19 @@ public class VariableRef {
     public Type<?> type() {
         return type;
     }
-    
+
+    public boolean isOptional() {
+        final Class<?> rawType = rawType();
+
+        for (final Class<?> optionalClass : OPTIONAL_CLASSES) {
+            if (optionalClass.isAssignableFrom(rawType)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
     public boolean isPrimitive() {
         return type.isPrimitive();
     }
@@ -472,6 +499,10 @@ public class VariableRef {
     }
     
     public String isNull() {
+        if (isOptional()) {
+            return String.format("(%1$s == null || !%1$s.isPresent())", getter());
+        }
+
         return property != null ? isNull(property, name) : getter() + " == null";
     }
     
diff --git a/pom.xml b/pom.xml
index d02e94a3..f9dd437e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -282,6 +282,12 @@
 				<version>0.0.4</version>
 			</dependency>
 
+			<dependency>
+				<groupId>com.google.guava</groupId>
+				<artifactId>guava</artifactId>
+				<version>18.0</version>
+			</dependency>
+
 		</dependencies>
 	</dependencyManagement>
 


Test patch:
diff --git a/tests/pom.xml b/tests/pom.xml
index 7ea53f49..1b64001d 100644
--- a/tests/pom.xml
+++ b/tests/pom.xml
@@ -136,6 +136,10 @@
 			<artifactId>jaxb-impl</artifactId>
 		</dependency>
 
+		<dependency>
+			<groupId>com.google.guava</groupId>
+			<artifactId>guava</artifactId>
+		</dependency>
 
 	</dependencies>
 
diff --git a/tests/src/main/java/ma/glasnost/orika/test/optional/GuavaOptionalTestCase.java b/tests/src/main/java/ma/glasnost/orika/test/optional/GuavaOptionalTestCase.java
new file mode 100644
index 00000000..a6f4e152
--- /dev/null
+++ b/tests/src/main/java/ma/glasnost/orika/test/optional/GuavaOptionalTestCase.java
@@ -0,0 +1,66 @@
+package ma.glasnost.orika.test.optional;
+
+import com.google.common.base.Optional;
+import ma.glasnost.orika.MapperFacade;
+import ma.glasnost.orika.MapperFactory;
+import ma.glasnost.orika.converter.builtin.GuavaOptionalConverter;
+import ma.glasnost.orika.metadata.TypeFactory;
+import ma.glasnost.orika.test.MappingUtil;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class GuavaOptionalTestCase {
+
+    @Test
+    public void testMappingIgnoresEmptyOptionalInDestination() {
+        final String expected = "initial";
+
+        final Source source = new Source();
+        source.setS(Optional.of(expected));
+
+        final Destination actual = getMapperFacade().map(source, Destination.class);
+
+        assertEquals(expected, actual.getS().get());
+    }
+
+    @Test
+    public void testMappingMapEmptyToEmpty() {
+        final Destination actual = getMapperFacade().map(new Source(), Destination.class);
+
+        assertFalse(actual.getS().isPresent());
+    }
+
+    private MapperFacade getMapperFacade() {
+        final MapperFactory mapperFactory = MappingUtil.getMapperFactory(true);
+        mapperFactory.getConverterFactory()
+                .registerConverter(new GuavaOptionalConverter<String, String>(TypeFactory.valueOf(String.class), TypeFactory.valueOf(String.class)));
+        return mapperFactory.getMapperFacade();
+    }
+
+    public static class Source {
+        private Optional<String> s = Optional.absent();
+
+        public Optional<String> getS() {
+            return s;
+        }
+
+        public void setS(final Optional<String> s) {
+            this.s = s;
+        }
+    }
+
+    public static class Destination {
+        private Optional<String> s = Optional.absent();
+
+        public Optional<String> getS() {
+            return s;
+        }
+
+        public void setS(final Optional<String> s) {
+            this.s = s;
+        }
+    }
+
+}