From ea1938df8356b30472db6a6f1ae7d4c6d347be64 Mon Sep 17 00:00:00 2001 From: mdeboer Date: Thu, 4 Oct 2012 19:44:18 -0700 Subject: [PATCH 01/11] Added additional numeric converters to support primitive conversions for issue #58, with test cases --- .../converter/builtin/BuiltinConverters.java | 4 + .../converter/builtin/NumericConverters.java | 357 ++++++++++++++ .../converter/NumericConvertersTestCase.java | 440 ++++++++++++++---- .../test/primitives/PrimitivesTestCase.java | 42 ++ 4 files changed, 753 insertions(+), 90 deletions(-) diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java index 7faaa837..c7a8f6bc 100644 --- a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java @@ -83,6 +83,10 @@ public static void register(ConverterFactory converterFactory) { converterFactory.registerConverter(new NumericConverters.BigIntegerToIntegerConverter(false)); converterFactory.registerConverter(new NumericConverters.BigIntegerToLongConverter(false)); + converterFactory.registerConverter(new NumericConverters.IntegerToShortConverter(false)); + converterFactory.registerConverter(new NumericConverters.LongToIntegerConverter(false)); + converterFactory.registerConverter(new NumericConverters.LongToShortConverter(false)); + /* * Register additional common "immutable" types */ diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/NumericConverters.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/NumericConverters.java index 5ec261bf..ce811bf7 100644 --- a/core/src/main/java/ma/glasnost/orika/converter/builtin/NumericConverters.java +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/NumericConverters.java @@ -180,4 +180,361 @@ public BigInteger convertFrom(Integer source, } } + + /** + * Provides conversion between Integer and Short + * + * @author matt.deboer@gmail.com + */ + public static class IntegerToShortConverter extends + BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new IntegerToShortConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public IntegerToShortConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Short convertTo(Integer source, Type destinationType) { + if (!truncate && (source.compareTo((int) Short.MAX_VALUE) > 0 || source.compareTo((int) Short.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Short.class.getCanonicalName()); + } + return source.shortValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Integer convertFrom(Short source, Type destinationType) { + return source.intValue(); + } + } + + /** + * Provides conversion between Long and Short + * + * @author matt.deboer@gmail.com + */ + public static class LongToShortConverter extends + BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new LongToShortConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public LongToShortConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Short convertTo(Long source, Type destinationType) { + if (!truncate && (source.compareTo((long) Short.MAX_VALUE) > 0 || source.compareTo((long) Short.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Short.class.getCanonicalName()); + } + return source.shortValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Long convertFrom(Short source, Type destinationType) { + return source.longValue(); + } + } + + /** + * Provides conversion between Long and Integer + * + * @author matt.deboer@gmail.com + */ + public static class LongToIntegerConverter extends + BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new LongToIntegerConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public LongToIntegerConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Integer convertTo(Long source, Type destinationType) { + if (!truncate && (source.compareTo((long) Integer.MAX_VALUE) > 0 || source.compareTo((long) Integer.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Integer.class.getCanonicalName()); + } + return source.intValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Long convertFrom(Integer source, Type destinationType) { + return source.longValue(); + } + } + + /** + * Provides conversion between Long and Integer + * + * @author matt.deboer@gmail.com + */ + public static class DoubleToLongConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new LongToIntegerConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public DoubleToLongConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Long convertTo(Double source, Type destinationType) { + if (!truncate && (source.compareTo((double) Long.MAX_VALUE) > 0 || source.compareTo((double) Long.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Long.class.getCanonicalName()); + } + return source.longValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Double convertFrom(Long source, Type destinationType) { + return source.doubleValue(); + } + } + + /** + * Provides conversion between Integer and Integer + * + * @author matt.deboer@gmail.com + */ + public static class DoubleToIntegerConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new IntegerToIntegerConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public DoubleToIntegerConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Integer convertTo(Double source, Type destinationType) { + if (!truncate && (source.compareTo((double) Integer.MAX_VALUE) > 0 || source.compareTo((double) Integer.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Integer.class.getCanonicalName()); + } + return source.intValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Double convertFrom(Integer source, Type destinationType) { + return source.doubleValue(); + } + } + + /** + * Provides conversion between Short and Short + * + * @author matt.deboer@gmail.com + */ + public static class DoubleToShortConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new ShortToShortConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public DoubleToShortConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Short convertTo(Double source, Type destinationType) { + if (!truncate && (source.compareTo((double) Short.MAX_VALUE) > 0 || source.compareTo((double) Short.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Short.class.getCanonicalName()); + } + return source.shortValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Double convertFrom(Short source, Type destinationType) { + return source.doubleValue(); + } + } + + // ~ + + /** + * Provides conversion between Long and Integer + * + * @author matt.deboer@gmail.com + */ + public static class FloatToLongConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new LongToIntegerConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public FloatToLongConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Long convertTo(Float source, Type destinationType) { + if (!truncate && (source.compareTo((float) Long.MAX_VALUE) > 0 || source.compareTo((float) Long.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Long.class.getCanonicalName()); + } + return source.longValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Float convertFrom(Long source, Type destinationType) { + return source.floatValue(); + } + } + + /** + * Provides conversion between Integer and Integer + * + * @author matt.deboer@gmail.com + */ + public static class FloatToIntegerConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new IntegerToIntegerConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public FloatToIntegerConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Integer convertTo(Float source, Type destinationType) { + if (!truncate && (source.compareTo((float) Integer.MAX_VALUE) > 0 || source.compareTo((float) Integer.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Integer.class.getCanonicalName()); + } + return source.intValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Float convertFrom(Integer source, Type destinationType) { + return source.floatValue(); + } + } + + /** + * Provides conversion between Short and Short + * + * @author matt.deboer@gmail.com + */ + public static class FloatToShortConverter extends BidirectionalConverter { + + private final boolean truncate; + + /** + * Constructs a new ShortToShortConverter, with the configured truncation behavior. + * + * @param truncate specifies whether the converter should perform truncation; if false, + * an ArithmeticException is thrown for a value which is too large or too small to be + * accurately represented by the smaller of the two types + */ + public FloatToShortConverter(boolean truncate) { + this.truncate = truncate; + } + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertTo(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Short convertTo(Float source, Type destinationType) { + if (!truncate && (source.compareTo((float) Short.MAX_VALUE) > 0 || source.compareTo((float) Short.MIN_VALUE) < 0)) { + throw new ArithmeticException("Overflow: " + source + " cannot be represented by " + Short.class.getCanonicalName()); + } + return source.shortValue(); + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.converter.BidirectionalConverter#convertFrom(java.lang.Object, ma.glasnost.orika.metadata.Type) + */ + @Override + public Float convertFrom(Short source, Type destinationType) { + return source.floatValue(); + } + } + } diff --git a/core/src/test/java/ma/glasnost/orika/test/converter/NumericConvertersTestCase.java b/core/src/test/java/ma/glasnost/orika/test/converter/NumericConvertersTestCase.java index a5017ed5..d0ce4537 100644 --- a/core/src/test/java/ma/glasnost/orika/test/converter/NumericConvertersTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/converter/NumericConvertersTestCase.java @@ -11,13 +11,22 @@ import ma.glasnost.orika.converter.builtin.NumericConverters.BigDecimalToFloatConverter; import ma.glasnost.orika.converter.builtin.NumericConverters.BigIntegerToIntegerConverter; import ma.glasnost.orika.converter.builtin.NumericConverters.BigIntegerToLongConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.DoubleToIntegerConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.DoubleToLongConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.DoubleToShortConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.FloatToIntegerConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.FloatToLongConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.FloatToShortConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.IntegerToShortConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.LongToIntegerConverter; +import ma.glasnost.orika.converter.builtin.NumericConverters.LongToShortConverter; import ma.glasnost.orika.test.MappingUtil; import org.junit.Test; /** - * DateAndTimeConverters provides a set of individual converters - * for conversion between the below listed enumeration of commonly used data/time + * DateAndTimeConverters provides a set of individual converters for conversion + * between the below listed enumeration of commonly used data/time * representations: *
    *
  • java.util.Date @@ -27,94 +36,345 @@ *
* * @author matt.deboer@gmail.com - * + * */ public class NumericConvertersTestCase { - - - @Test - public void testBigDecimalToDoubleConverter() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigDecimalToDoubleConverter()); - MapperFacade mapper = factory.getMapperFacade(); - - BigDecimal bd = new BigDecimal("5423.51478"); - Double db = mapper.map(bd, Double.class); - Assert.assertEquals(bd.doubleValue(), db.doubleValue(), 0.00001d); - - BigDecimal reverse = mapper.map(db, BigDecimal.class); - Assert.assertEquals(bd.doubleValue(), reverse.doubleValue(), 0.00001d); - } - - @Test - public void testBigDecimalToFloatConverter() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigDecimalToFloatConverter()); - MapperFacade mapper = factory.getMapperFacade(); - - BigDecimal bd = new BigDecimal("5423.51"); - Float ft = mapper.map(bd, Float.class); - Assert.assertEquals(bd.floatValue(), ft.floatValue(), 0.01d); - - BigDecimal reverse = mapper.map(ft, BigDecimal.class); - Assert.assertEquals(bd.doubleValue(), reverse.doubleValue(), 0.01d); - } - - @Test - public void testBigIntegerToLongConverter() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigIntegerToLongConverter(false)); - MapperFacade mapper = factory.getMapperFacade(); - - BigInteger bi = new BigInteger(""+Long.MAX_VALUE); - Long lg = mapper.map(bi, Long.class); - Assert.assertEquals(bi.longValue(), lg.longValue()); - - BigInteger reverse = mapper.map(lg, BigInteger.class); - Assert.assertEquals(bi.longValue(), reverse.longValue()); - } - - @Test - public void testBigIntegerToIntegerConverter() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigIntegerToIntegerConverter(false)); - MapperFacade mapper = factory.getMapperFacade(); - - BigInteger bi = new BigInteger(""+Integer.MAX_VALUE); - Integer i = mapper.map(bi, Integer.class); - Assert.assertEquals(bi.longValue(), i.longValue()); - - BigInteger reverse = mapper.map(i, BigInteger.class); - Assert.assertEquals(bi.longValue(), reverse.longValue()); - } - - - @Test(expected=MappingException.class) - public void testBigIntegerToLongConverter_Overflow() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigIntegerToLongConverter(false)); - MapperFacade mapper = factory.getMapperFacade(); - - BigInteger bi = new BigInteger("1"+Long.MAX_VALUE); - Long lg = mapper.map(bi, Long.class); - Assert.assertEquals(bi.longValue(), lg.longValue()); - - BigInteger reverse = mapper.map(lg, BigInteger.class); - Assert.assertEquals(bi.longValue(), reverse.longValue()); - } - - @Test(expected=MappingException.class) - public void testBigIntegerToIntegerConverter_Overflow() { - MapperFactory factory = MappingUtil.getMapperFactory(); - factory.getConverterFactory().registerConverter(new BigIntegerToIntegerConverter(false)); - MapperFacade mapper = factory.getMapperFacade(); - - BigInteger bi = new BigInteger("1"+Long.MAX_VALUE); - Integer i = mapper.map(bi, Integer.class); - Assert.assertEquals(bi.longValue(), i.longValue()); - - BigInteger reverse = mapper.map(i, BigInteger.class); - Assert.assertEquals(bi.longValue(), reverse.longValue()); - } - + + @Test + public void testBigDecimalToDoubleConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigDecimalToDoubleConverter()); + MapperFacade mapper = factory.getMapperFacade(); + + BigDecimal bd = new BigDecimal("5423.51478"); + Double db = mapper.map(bd, Double.class); + Assert.assertEquals(bd.doubleValue(), db.doubleValue(), 0.00001d); + + BigDecimal reverse = mapper.map(db, BigDecimal.class); + Assert.assertEquals(bd.doubleValue(), reverse.doubleValue(), 0.00001d); + } + + @Test + public void testBigDecimalToFloatConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigDecimalToFloatConverter()); + MapperFacade mapper = factory.getMapperFacade(); + + BigDecimal bd = new BigDecimal("5423.51"); + Float ft = mapper.map(bd, Float.class); + Assert.assertEquals(bd.floatValue(), ft.floatValue(), 0.01d); + + BigDecimal reverse = mapper.map(ft, BigDecimal.class); + Assert.assertEquals(bd.doubleValue(), reverse.doubleValue(), 0.01d); + } + + @Test + public void testBigIntegerToLongConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigIntegerToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + BigInteger bi = new BigInteger("" + Long.MAX_VALUE); + Long lg = mapper.map(bi, Long.class); + Assert.assertEquals(bi.longValue(), lg.longValue()); + + BigInteger reverse = mapper.map(lg, BigInteger.class); + Assert.assertEquals(bi.longValue(), reverse.longValue()); + } + + @Test + public void testBigIntegerToIntegerConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigIntegerToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + BigInteger bi = new BigInteger("" + Integer.MAX_VALUE); + Integer i = mapper.map(bi, Integer.class); + Assert.assertEquals(bi.longValue(), i.longValue()); + + BigInteger reverse = mapper.map(i, BigInteger.class); + Assert.assertEquals(bi.longValue(), reverse.longValue()); + } + + @Test(expected = MappingException.class) + public void testBigIntegerToLongConverter_Overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigIntegerToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + BigInteger bi = new BigInteger("1" + Long.MAX_VALUE); + Long lg = mapper.map(bi, Long.class); + Assert.assertEquals(bi.longValue(), lg.longValue()); + + BigInteger reverse = mapper.map(lg, BigInteger.class); + Assert.assertEquals(bi.longValue(), reverse.longValue()); + } + + @Test(expected = MappingException.class) + public void testBigIntegerToIntegerConverter_Overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new BigIntegerToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + BigInteger bi = new BigInteger("1" + Long.MAX_VALUE); + Integer i = mapper.map(bi, Integer.class); + Assert.assertEquals(bi.longValue(), i.longValue()); + + BigInteger reverse = mapper.map(i, BigInteger.class); + Assert.assertEquals(bi.longValue(), reverse.longValue()); + } + + @Test + public void testLongToShortConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new LongToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Long value = (long) Short.MAX_VALUE; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.longValue(), result.longValue()); + + Long reverse = mapper.map(result, Long.class); + Assert.assertEquals(result.longValue(), reverse.longValue()); + } + + @Test + public void testLongToIntegerConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new LongToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Long value = (long) Integer.MAX_VALUE; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.longValue(), result.longValue()); + + Long reverse = mapper.map(result, Long.class); + Assert.assertEquals(result.longValue(), reverse.longValue()); + } + + @Test + public void testIntegerToShortConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new IntegerToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Integer value = (int) Short.MAX_VALUE; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.intValue(), result.intValue()); + + Integer reverse = mapper.map(result, Integer.class); + Assert.assertEquals(result.intValue(), reverse.intValue()); + } + + @Test + public void testDoubleToShortConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Short.MAX_VALUE; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test + public void testDoubleToIntegerConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Integer.MAX_VALUE; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test + public void testDoubleToLongConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Long.MAX_VALUE; + Long result = mapper.map(value, Long.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test + public void testFloatToShortConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = (float) Short.MAX_VALUE; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } + + @Test + public void testFloatToIntegerConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = (float) Integer.MAX_VALUE; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } + + @Test + public void testFloatToLongConverter() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = (float) Long.MAX_VALUE; + Long result = mapper.map(value, Long.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } + + // ~ overflow exceptions + + @Test(expected = MappingException.class) + public void testLongToShortConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new LongToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Long value = (long) Short.MAX_VALUE + 1; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.longValue(), result.longValue()); + + Long reverse = mapper.map(result, Long.class); + Assert.assertEquals(result.longValue(), reverse.longValue()); + } + + @Test(expected = MappingException.class) + public void testLongToIntegerConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new LongToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Long value = (long) Integer.MAX_VALUE + 1; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.longValue(), result.longValue()); + + Long reverse = mapper.map(result, Long.class); + Assert.assertEquals(result.longValue(), reverse.longValue()); + } + + @Test(expected = MappingException.class) + public void testIntegerToShortConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new IntegerToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Integer value = (int) Short.MAX_VALUE + 1; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.intValue(), result.intValue()); + + Integer reverse = mapper.map(result, Integer.class); + Assert.assertEquals(result.intValue(), reverse.intValue()); + } + + @Test(expected = MappingException.class) + public void testDoubleToShortConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Short.MAX_VALUE + 1; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test(expected = MappingException.class) + public void testDoubleToIntegerConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Integer.MAX_VALUE + 1; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test(expected = MappingException.class) + public void testDoubleToLongConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new DoubleToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Double value = (double) Long.MAX_VALUE + 10000.0; + Long result = mapper.map(value, Long.class); + Assert.assertEquals(value.doubleValue(), result.doubleValue()); + + Double reverse = mapper.map(result, Double.class); + Assert.assertEquals(result.doubleValue(), reverse.doubleValue()); + } + + @Test(expected = MappingException.class) + public void testFloatToShortConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToShortConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = ((float) Short.MAX_VALUE) * 1.1f; + Short result = mapper.map(value, Short.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } + + @Test(expected = MappingException.class) + public void testFloatToIntegerConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToIntegerConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = ((float) Integer.MAX_VALUE) * 1.1f; + Integer result = mapper.map(value, Integer.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } + + @Test(expected = MappingException.class) + public void testFloatToLongConverter_overflow() { + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(new FloatToLongConverter(false)); + MapperFacade mapper = factory.getMapperFacade(); + + Float value = ((float) Long.MAX_VALUE) * 1.1f; + Long result = mapper.map(value, Long.class); + Assert.assertEquals(value.floatValue(), result.floatValue()); + + Float reverse = mapper.map(result, Float.class); + Assert.assertEquals(result.floatValue(), reverse.floatValue()); + } } diff --git a/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java b/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java index 6884445e..2bb2ed00 100644 --- a/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java @@ -18,6 +18,7 @@ package ma.glasnost.orika.test.primitives; +import static org.junit.Assert.assertEquals; import ma.glasnost.orika.MapperFacade; import ma.glasnost.orika.MapperFactory; import ma.glasnost.orika.test.MappingUtil; @@ -114,6 +115,47 @@ public void testWrapperToWrapper() { } + + @Test + public void short_int() { + ShortHolder source = new ShortHolder(); + source.value = 2; + + MapperFactory factory = MappingUtil.getMapperFactory(); + MapperFacade mapper = factory.getMapperFacade(); + + IntHolder dest = mapper.map(source, IntHolder.class); + assertEquals(source.value, dest.value); + + ShortHolder mapBack = mapper.map(dest, ShortHolder.class); + assertEquals(source.value, mapBack.value); + } + + + public static class IntHolder { + public int value; + } + + public static class ShortHolder { + public short value; + } + + public static class LongHolder { + public long value; + } + + public static class FloatHolder { + public float value; + } + + public static class DoubleHolder { + public double value; + } + + public static class CharHolder { + public char value; + } + public static class PrimitiveAttributes { private int age; private short shortValue; From f0b4f6cab7fad3121e6bcde438312b4135103def Mon Sep 17 00:00:00 2001 From: mdeboer Date: Thu, 4 Oct 2012 20:06:56 -0700 Subject: [PATCH 02/11] Added additional numeric converters to BuiltinConverters --- .../orika/converter/builtin/BuiltinConverters.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java index c7a8f6bc..49d51bad 100644 --- a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java @@ -87,6 +87,14 @@ public static void register(ConverterFactory converterFactory) { converterFactory.registerConverter(new NumericConverters.LongToIntegerConverter(false)); converterFactory.registerConverter(new NumericConverters.LongToShortConverter(false)); + converterFactory.registerConverter(new NumericConverters.FloatToShortConverter(false)); + converterFactory.registerConverter(new NumericConverters.FloatToIntegerConverter(false)); + converterFactory.registerConverter(new NumericConverters.FloatToLongConverter(false)); + + converterFactory.registerConverter(new NumericConverters.DoubleToShortConverter(false)); + converterFactory.registerConverter(new NumericConverters.DoubleToIntegerConverter(false)); + converterFactory.registerConverter(new NumericConverters.DoubleToLongConverter(false)); + /* * Register additional common "immutable" types */ From 7d0c8a5823398cf4cee5ff4d0a0850917c3f54ae Mon Sep 17 00:00:00 2001 From: mdeboer Date: Thu, 4 Oct 2012 23:38:16 -0700 Subject: [PATCH 03/11] Added CloneableConverter as a resolution to issue #55 (regarding XMLGregorianCalendar); also moved types java.util.Date and java.sql.Date out of the immutable types known by ClassUtil.isImmutable() since these types are actually mutable; instead, they are included in a default CloneableConverter which is registered as part of BuiltinConverters.register() by default. Wrote unit test to confirm that users can override "cloneable" types to be treated as "immutable" types by registering their own PassThroughConverter --- .../converter/builtin/BuiltinConverters.java | 13 +- .../converter/builtin/CloneableConverter.java | 73 ++++++++ .../builtin/DateAndTimeConverters.java | 97 +++++++---- .../glasnost/orika/impl/util/ClassUtil.java | 3 +- .../converter/CloneableConverterTestCase.java | 161 ++++++++++++++++++ .../test/primitives/PrimitivesTestCase.java | 32 +++- 6 files changed, 341 insertions(+), 38 deletions(-) create mode 100644 core/src/main/java/ma/glasnost/orika/converter/builtin/CloneableConverter.java create mode 100644 core/src/test/java/ma/glasnost/orika/test/converter/CloneableConverterTestCase.java diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java index 49d51bad..60386717 100644 --- a/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/BuiltinConverters.java @@ -7,9 +7,13 @@ import java.net.InetSocketAddress; import java.net.URI; import java.net.URL; +import java.util.Calendar; +import java.util.Date; import java.util.Locale; import java.util.UUID; +import javax.xml.datatype.XMLGregorianCalendar; + import ma.glasnost.orika.converter.ConverterFactory; /** @@ -109,6 +113,13 @@ public static void register(ConverterFactory converterFactory) { Inet6Address.class, InetSocketAddress.class )); - + /* + * Register additional common "cloneable" types + */ + converterFactory.registerConverter(new CloneableConverter( + Date.class, + Calendar.class, + XMLGregorianCalendar.class + )); } } diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/CloneableConverter.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/CloneableConverter.java new file mode 100644 index 00000000..daae46ef --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/CloneableConverter.java @@ -0,0 +1,73 @@ +package ma.glasnost.orika.converter.builtin; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import ma.glasnost.orika.CustomConverter; +import ma.glasnost.orika.metadata.Type; +import ma.glasnost.orika.metadata.TypeFactory; + +/** + * CloneableConverter allows configuration of a number of specific types which + * should be cloned directly instead of creating a mapped copy.

+ * + * This allows you to declare your own set of types which should be cloned instead + * of mapped. + * + * @author matt.deboer@gmail.com + * + */ +public class CloneableConverter extends CustomConverter { + + private final Set> clonedTypes = new HashSet>(); + private final Method cloneMethod; + /** + * Constructs a new ClonableConverter configured to handle the provided + * list of types by cloning. + * + * @param types one or more types that should be treated as immutable + */ + public CloneableConverter(java.lang.reflect.Type...types) { + try { + cloneMethod = Object.class.getDeclaredMethod("clone"); + cloneMethod.setAccessible(true); + } catch (SecurityException e) { + throw new IllegalStateException(e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + for (java.lang.reflect.Type type: types) { + clonedTypes.add(TypeFactory.valueOf(type)); + } + } + + private boolean shouldClone(Type type) { + for (Type registeredType: clonedTypes) { + if (registeredType.isAssignableFrom(type)) { + return true; + } + } + return false; + } + + /* (non-Javadoc) + * @see ma.glasnost.orika.Converter#canConvert(ma.glasnost.orika.metadata.Type, ma.glasnost.orika.metadata.Type) + */ + public boolean canConvert(Type sourceType, Type destinationType) { + return shouldClone(sourceType) && sourceType.equals(destinationType); + } + + public Object convert(Object source, Type destinationType) { + try { + return cloneMethod.invoke(source); + } catch (IllegalArgumentException e) { + throw new IllegalStateException(e); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } catch (InvocationTargetException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/core/src/main/java/ma/glasnost/orika/converter/builtin/DateAndTimeConverters.java b/core/src/main/java/ma/glasnost/orika/converter/builtin/DateAndTimeConverters.java index d3513d2a..bbb3b0b1 100644 --- a/core/src/main/java/ma/glasnost/orika/converter/builtin/DateAndTimeConverters.java +++ b/core/src/main/java/ma/glasnost/orika/converter/builtin/DateAndTimeConverters.java @@ -55,10 +55,20 @@ public Date convertFrom(Calendar source, Type destinationType) { public static class DateToXmlGregorianCalendarConverter extends BidirectionalConverter { + private DatatypeFactory factory; + + { + try { + factory = DatatypeFactory.newInstance(); + } catch (DatatypeConfigurationException e) { + throw new IllegalStateException(e); + } + } + @Override public XMLGregorianCalendar convertTo(Date source, Type destinationType) { - return toXMLGregorianCalendar(source); + return toXMLGregorianCalendar(source, factory); } @Override @@ -76,10 +86,20 @@ public Date convertFrom(XMLGregorianCalendar source, public static class CalendarToXmlGregorianCalendarConverter extends BidirectionalConverter { + private DatatypeFactory factory; + + { + try { + factory = DatatypeFactory.newInstance(); + } catch (DatatypeConfigurationException e) { + throw new IllegalStateException(e); + } + } + @Override public XMLGregorianCalendar convertTo(Calendar source, Type destinationType) { - return toXMLGregorianCalendar(source); + return toXMLGregorianCalendar(source, factory); } @Override @@ -89,27 +109,6 @@ public Calendar convertFrom(XMLGregorianCalendar source, } } - /** - * Provides conversion between Long and XMLGregorianCalendar - * - * @author matt.deboer@gmail.com - */ - public static class LongToXmlGregorianCalendarConverter extends - BidirectionalConverter { - - @Override - public XMLGregorianCalendar convertTo(Long source, - Type destinationType) { - return toXMLGregorianCalendar(source); - } - - @Override - public Long convertFrom(XMLGregorianCalendar source, - Type destinationType) { - return toLong(source); - } - } - /** * Provides conversion between Long and Date * @@ -150,6 +149,37 @@ public Long convertFrom(Calendar source, Type destinationType) { } } + + /** + * Provides conversion between Long and Calendar + * + * @author matt.deboer@gmail.com + * + */ + public static class LongToXmlGregorianCalendarConverter extends + BidirectionalConverter { + + private DatatypeFactory factory; + + { + try { + factory = DatatypeFactory.newInstance(); + } catch (DatatypeConfigurationException e) { + throw new IllegalStateException(e); + } + } + + @Override + public XMLGregorianCalendar convertTo(Long source, Type destinationType) { + return toXMLGregorianCalendar(source, factory); + } + + @Override + public Long convertFrom(XMLGregorianCalendar source, Type destinationType) { + return toLong(source); + } + } + private static Date toDate(XMLGregorianCalendar source) { return source.toGregorianCalendar().getTime(); } @@ -177,24 +207,23 @@ private static Calendar toCalendar(Long source) { } private static XMLGregorianCalendar toXMLGregorianCalendar( - Calendar source) { - return toXMLGregorianCalendar(source.getTime()); + Calendar source, DatatypeFactory factory) { + return toXMLGregorianCalendar(source.getTime(), factory); } private static XMLGregorianCalendar toXMLGregorianCalendar( - Date source) { - GregorianCalendar c = new GregorianCalendar(); + Date source, DatatypeFactory factory) { + + GregorianCalendar c = new GregorianCalendar(); c.setTime(source); - try { - return DatatypeFactory.newInstance().newXMLGregorianCalendar(c); - } catch (DatatypeConfigurationException e) { - throw new IllegalStateException(e); - } + + return factory.newXMLGregorianCalendar(c); + } private static XMLGregorianCalendar toXMLGregorianCalendar( - Long source) { - return toXMLGregorianCalendar(new Date(source)); + Long source, DatatypeFactory factory) { + return toXMLGregorianCalendar(new Date(source), factory); } private static Long toLong(Date source) { diff --git a/core/src/main/java/ma/glasnost/orika/impl/util/ClassUtil.java b/core/src/main/java/ma/glasnost/orika/impl/util/ClassUtil.java index a22228fb..880b8264 100644 --- a/core/src/main/java/ma/glasnost/orika/impl/util/ClassUtil.java +++ b/core/src/main/java/ma/glasnost/orika/impl/util/ClassUtil.java @@ -22,7 +22,6 @@ import java.math.BigDecimal; import java.util.Arrays; import java.util.Collection; -import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.Set; @@ -47,7 +46,7 @@ private static Set> getWrapperTypes() { } private static Set> getImmutablesTypes() { - Set> immutables = new HashSet>(Arrays.>asList(String.class, BigDecimal.class, Date.class, java.sql.Date.class, + Set> immutables = new HashSet>(Arrays.>asList(String.class, BigDecimal.class, Byte.TYPE, Short.TYPE, Integer.TYPE, Long.TYPE, Boolean.TYPE, Character.TYPE, Float.TYPE, Double.TYPE )); immutables.addAll(getWrapperTypes()); return immutables; diff --git a/core/src/test/java/ma/glasnost/orika/test/converter/CloneableConverterTestCase.java b/core/src/test/java/ma/glasnost/orika/test/converter/CloneableConverterTestCase.java new file mode 100644 index 00000000..12d0e93c --- /dev/null +++ b/core/src/test/java/ma/glasnost/orika/test/converter/CloneableConverterTestCase.java @@ -0,0 +1,161 @@ +/* + * Orika - simpler, better and faster Java bean mapping + * + * Copyright (C) 2011 Orika authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ma.glasnost.orika.test.converter; + +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import javax.xml.datatype.DatatypeConfigurationException; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import junit.framework.Assert; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.converter.builtin.CloneableConverter; +import ma.glasnost.orika.converter.builtin.PassThroughConverter; +import ma.glasnost.orika.test.MappingUtil; + +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.junit.Test; + +public class CloneableConverterTestCase { + + @Test + public void cloneableConverter() throws DatatypeConfigurationException { + + CloneableConverter cc = new CloneableConverter(SampleCloneable.class); + + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(cc); + + GregorianCalendar cal = new GregorianCalendar(); + cal.add(Calendar.YEAR, 10); + XMLGregorianCalendar xmlCal = DatatypeFactory.newInstance().newXMLGregorianCalendar((GregorianCalendar)cal); + cal.add(Calendar.MONTH, 3); + + ClonableHolder source = new ClonableHolder(); + source.value = new SampleCloneable(); + source.value.id = 5L; + source.date = new Date(System.currentTimeMillis() + 100000); + source.timestamp = new Timestamp(System.currentTimeMillis() + 50000); + source.calendar = cal; + source.xmlCalendar = xmlCal; + + ClonableHolder dest = factory.getMapperFacade().map(source, ClonableHolder.class); + Assert.assertEquals(source.value, dest.value); + Assert.assertNotSame(source.value, dest.value); + Assert.assertEquals(source.date, dest.date); + Assert.assertNotSame(source.date, dest.date); + Assert.assertEquals(source.timestamp, dest.timestamp); + Assert.assertNotSame(source.timestamp, dest.timestamp); + Assert.assertEquals(source.calendar, dest.calendar); + Assert.assertNotSame(source.calendar, dest.calendar); + Assert.assertEquals(source.xmlCalendar, dest.xmlCalendar); + Assert.assertNotSame(source.xmlCalendar, dest.xmlCalendar); + } + + + /** + * This test method demonstrates that you can decide to treat one of the default cloneable types + * as immutable if desired by registering your own PassThroughConverter for that type + * + * @throws DatatypeConfigurationException + */ + @Test + public void overrideDefaultCloneableToImmutable() throws DatatypeConfigurationException { + + PassThroughConverter cc = new PassThroughConverter(Date.class, Calendar.class); + + MapperFactory factory = MappingUtil.getMapperFactory(); + factory.getConverterFactory().registerConverter(cc); + + GregorianCalendar cal = new GregorianCalendar(); + cal.add(Calendar.YEAR, 10); + XMLGregorianCalendar xmlCal = DatatypeFactory.newInstance().newXMLGregorianCalendar((GregorianCalendar)cal); + cal.add(Calendar.MONTH, 3); + + ClonableHolder source = new ClonableHolder(); + source.value = new SampleCloneable(); + source.value.id = 5L; + source.date = new Date(System.currentTimeMillis() + 100000); + source.calendar = cal; + source.xmlCalendar = xmlCal; + + ClonableHolder dest = factory.getMapperFacade().map(source, ClonableHolder.class); + Assert.assertEquals(source.value, dest.value); + Assert.assertNotSame(source.value, dest.value); + Assert.assertEquals(source.date, dest.date); + Assert.assertSame(source.date, dest.date); + Assert.assertEquals(source.calendar, dest.calendar); + Assert.assertSame(source.calendar, dest.calendar); + Assert.assertEquals(source.xmlCalendar, dest.xmlCalendar); + Assert.assertNotSame(source.xmlCalendar, dest.xmlCalendar); + + } + + + + public static class ClonableHolder { + public SampleCloneable value; + public Date date; + public java.sql.Timestamp timestamp; + public Calendar calendar; + public XMLGregorianCalendar xmlCalendar; + } + + + public static class SampleCloneable implements Cloneable { + private Long id; + + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Object clone() { + SampleCloneable clone; + try { + clone = (SampleCloneable) super.clone(); + clone.id = id; + } catch (CloneNotSupportedException e) { + throw new IllegalStateException(e); + } + + return clone; + } + + public boolean equals(Object that) { + return EqualsBuilder.reflectionEquals(this, that); + } + + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } + + } + + +} diff --git a/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java b/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java index 2bb2ed00..dd6d6284 100644 --- a/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/primitives/PrimitivesTestCase.java @@ -117,7 +117,7 @@ public void testWrapperToWrapper() { @Test - public void short_int() { + public void int_short() { ShortHolder source = new ShortHolder(); source.value = 2; @@ -131,6 +131,36 @@ public void short_int() { assertEquals(source.value, mapBack.value); } + @Test + public void long_short() { + ShortHolder source = new ShortHolder(); + source.value = 2; + + MapperFactory factory = MappingUtil.getMapperFactory(); + MapperFacade mapper = factory.getMapperFacade(); + + LongHolder dest = mapper.map(source, LongHolder.class); + assertEquals(source.value, dest.value); + + ShortHolder mapBack = mapper.map(dest, ShortHolder.class); + assertEquals(source.value, mapBack.value); + } + + @Test + public void long_int() { + IntHolder source = new IntHolder(); + source.value = 2; + + MapperFactory factory = MappingUtil.getMapperFactory(); + MapperFacade mapper = factory.getMapperFacade(); + + LongHolder dest = mapper.map(source, LongHolder.class); + assertEquals(source.value, dest.value); + + IntHolder mapBack = mapper.map(dest, IntHolder.class); + assertEquals(source.value, mapBack.value); + } + public static class IntHolder { public int value; From 610538662520ecf2366457066b13786a484a0d61 Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 00:11:36 -0700 Subject: [PATCH 04/11] Added new version of ScoringClassMapBuilder which is able to guess property matches (even nested ones) based on matching metrics; --- .../metadata/ScoringClassMapBuilder.java | 717 ++++++++++++++++++ .../ClassMapBuilderExtensibilityTestCase.java | 61 +- .../metadata/ScoringClassMapBuilderTest.java | 150 ++++ core/src/test/resources/logback-test.xml | 8 +- 4 files changed, 933 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java create mode 100644 core/src/test/java/ma/glasnost/orika/test/metadata/ScoringClassMapBuilderTest.java diff --git a/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java b/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java new file mode 100644 index 00000000..2cb87513 --- /dev/null +++ b/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java @@ -0,0 +1,717 @@ +/* + * Orika - simpler, better and faster Java bean mapping + * + * Copyright (C) 2011 Orika authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ma.glasnost.orika.metadata; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +import ma.glasnost.orika.DefaultFieldMapper; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.impl.util.ClassUtil; +import ma.glasnost.orika.property.PropertyResolverStrategy; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ScoringClassMapBuilder is an extension of the basic ClassMapBuilder that + * attempts to compute a best-fit matching of all properties (at every level + * of nesting) of one type to another, based on various metrics used to measure + * a given property match.

+ * + * Since this builder generates mappings based on scoring matches, it cannot always + * guess the correct mappings; be sure to test and double-check the mappings + * generated to assure they match expectations.

+ * + * @author matt.deboer@gmail.com + * + */ +public class ScoringClassMapBuilder extends ClassMapBuilder { + + private static final Logger LOGGER = LoggerFactory.getLogger(ScoringClassMapBuilder.class); + + private final PropertyMatchingWeights matchingWeights; + + /** + * PropertyMatchingHint is a class used to describe how different + * matching scenarios should be weighted when computing a match + * score for a set of properties. + * + * @author matt.deboer@gmail.com + * + */ + public static final class PropertyMatchingWeights { + + private static final double MIN_WEIGHT = 0.0; + private static final double MAX_WEIGHT = 1.0; + + private double nestedDepth = MAX_WEIGHT / 2.0; + private double unmatchedWords = MAX_WEIGHT / 2.0; + private double editDistance = MAX_WEIGHT / 2.0; + private double containsName = MAX_WEIGHT / 2.0; + private double typeMatch = MAX_WEIGHT / 2.0; + private double commonWordCount = MAX_WEIGHT / 2.0; + private double minimumScore = MAX_WEIGHT / 2.0; + + /** + * @return the weight associated with the number of words found in common + * between two property expressions + */ + public double commonWordCount() { + return commonWordCount; + } + /** + * Set the weight associated with the number of words found in common + * between two property expressions + * + * @param weight the weight associated with the number of words found in common + * between two property expressions + */ + public PropertyMatchingWeights commonWordCount(double weight) { + validateWeight(weight); + this.commonWordCount = weight; + return this; + } + /** + * @return the weight associated with one property containing the + * entire name of another property + */ + public double containsName() { + return containsName; + } + /** + * Set the weight associated with one property containing the + * entire name of another property. + * + * @param weight the weight associated with one property containing the + * entire name of another property + */ + public PropertyMatchingWeights containsName(double weight) { + validateWeight(weight); + this.containsName = weight; + return this; + } + /** + * @return the weight associated with one property matching the type of the other + */ + public double typeMatch() { + return typeMatch; + } + /** + * Set the weight associated with one property matching the type of the other + * + * @param weight the weight associated with one property matching the type of the other + */ + public PropertyMatchingWeights typeMatch(double weight) { + validateWeight(weight); + this.typeMatch = weight; + return this; + } + /** + * @return the weight modifier associated with a property word's edit distance based on + * it's nesting depth + */ + public double nestedDepth() { + return nestedDepth; + } + /** + * Set the weight modifier associated with a property word's edit distance based on + * it's nesting depth; higher values here causes the matching to be more focused toward + * the final name of a nested property, lower values focus on the entire name more evenly + * + * @param weight the weight modifier associated with a property word's edit distance based on + * it's nesting depth + */ + public PropertyMatchingWeights nestedDepth(double weight) { + validateWeight(weight); + this.nestedDepth = weight; + return this; + } + + /** + * @return the weight associated with the number of unmatched words between two property expressions + */ + public double unmatchedWords() { + return unmatchedWords; + } + + /** + * Set the weight associated with the number of unmatched words between two property expressions + * + * @param weight the weight associated with the number of unmatched words between two property expressions + */ + public PropertyMatchingWeights unmatchedWords(double weight) { + validateWeight(weight); + this.unmatchedWords = weight; + return this; + } + /** + * @return the weight associated with the edit distance between words in two property expressions + */ + public double editDistance() { + return editDistance; + } + /** + * Set the weight associated with the edit distance between words in two property expressions + * + * @param weight the weight associated with the edit distance between words in two property expressions + */ + public PropertyMatchingWeights editDistance(double weight) { + validateWeight(weight); + this.editDistance = weight; + return this; + } + /** + * @return the weight applied to the minimum score needed to accept a given match + */ + public double minimumScore() { + return minimumScore; + } + + /** + * Set the weight applied to the minimum score needed to accept a given match; setting higher + * values makes the matching more restrictive, lower scores make matching more lenient. + * + * @param weight the weight applied to the minimum score needed to accept a given match + */ + public PropertyMatchingWeights minimumScore(double weight) { + validateWeight(weight); + this.minimumScore = weight; + return this; + } + private void validateWeight(double weight) { + if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) { + throw new IllegalArgumentException("weights should be between " + MIN_WEIGHT + " and " + MAX_WEIGHT); + } + } + } + + + /** + * Constructs a new instance of ScoringClassMapBuilder, using the provided PropertyMatchingWeights + * to adjust the overall scoring of how properties are matched. + * + * @param aType + * @param bType + * @param propertyResolver + * @param defaults + */ + protected ScoringClassMapBuilder(Type aType, Type bType, MapperFactory mapperFactory, PropertyResolverStrategy propertyResolver, + DefaultFieldMapper[] defaults, PropertyMatchingWeights matchingWeights) { + super(aType, bType, mapperFactory, propertyResolver, defaults); + this.matchingWeights = matchingWeights; + } + + /** + * Gets all of the property expressions for a given type, including all nested properties. + * If the type of a property is not immutable and has any nested properties, it will not + * be included. (Note that the 'class' property is explicitly excluded.) + * + * @param type the type for which to gather properties + * @return the map of nested properties keyed by expression name + */ + protected Map getPropertyExpressions(Type type) { + + PropertyResolverStrategy propertyResolver = getPropertyResolver(); + + Map properties = new HashMap(); + LinkedHashMap toProcess = new LinkedHashMap(propertyResolver.getProperties(type)); + + while (!toProcess.isEmpty()) { + + Entry entry = toProcess.entrySet().iterator().next(); + if (!entry.getKey().equals("class")) { + + if (!ClassUtil.isImmutable(entry.getValue().getType())) { + Map props = propertyResolver.getProperties(entry.getValue().getType()); + if (!props.isEmpty()) { + for (Entry property : props.entrySet()) { + if (!property.getKey().equals("class")) { + String expression = entry.getKey() + "." + property.getKey(); + toProcess.put(expression, resolveProperty(type, expression)); + } + } + } else { + properties.put(entry.getKey(), resolveProperty(type, entry.getKey())); + } + } else { + properties.put(entry.getKey(), resolveProperty(type, entry.getKey())); + } + } + toProcess.remove(entry.getKey()); + } + return properties; + } + + /* + * (non-Javadoc) + * + * @see ma.glasnost.orika.metadata.ClassMapBuilder#byDefault(ma.glasnost. + * orika.DefaultFieldMapper[]) + */ + public ClassMapBuilder byDefault(DefaultFieldMapper... withDefaults) { + + DefaultFieldMapper[] defaults; + if (withDefaults.length == 0) { + defaults = getDefaultFieldMappers(); + } else { + defaults = withDefaults; + } + /* + * For our custom 'byDefault' method, we're going to try and match + * fields by their Levenshtein distance + */ + TreeSet matchScores = new TreeSet(); + + Map propertiesForA = getPropertyExpressions(getAType()); + Map propertiesForB = getPropertyExpressions(getBType()); + + for (final Entry propertyA : propertiesForA.entrySet()) { + if (!propertyA.getValue().getName().equals("class")) { + for (final Entry propertyB : propertiesForB.entrySet()) { + if (!propertyB.getValue().getName().equals("class")) { + FieldMatchScore matchScore = new FieldMatchScore(propertyA.getValue(), propertyB.getValue(), matchingWeights); + matchScores.add(matchScore); + } + } + } + } + + Set unmatchedFields = new HashSet(this.getPropertiesForTypeA()); + unmatchedFields.remove("class"); + + for (FieldMatchScore score : matchScores) { + + if (!this.getMappedPropertiesForTypeA().contains(score.propertyA.getExpression()) + && !this.getMappedPropertiesForTypeB().contains(score.propertyB.getExpression())) { + if (LOGGER.isTraceEnabled()) { + LOGGER.trace("\n" + score.toString()); + } + if (score.meetsMinimumScore()) { + fieldMap(score.propertyA.getExpression(), score.propertyB.getExpression()).add(); + unmatchedFields.remove(score.propertyA); + } + } + } + + /* + * Apply any default field mappers to the unmapped fields + */ + for (String propertyNameA : unmatchedFields) { + Property prop = resolvePropertyForA(propertyNameA); + for (DefaultFieldMapper defaulter : defaults) { + String suggestion = defaulter.suggestMappedField(propertyNameA, prop.getType()); + if (suggestion != null && getPropertiesForTypeB().contains(suggestion)) { + if (!getMappedPropertiesForTypeB().contains(suggestion)) { + fieldMap(propertyNameA, suggestion).add(); + } + } + } + } + + return this; + } + + public static class Factory extends ClassMapBuilderFactory { + + private PropertyMatchingWeights matchingWeights; + + public Factory() { + matchingWeights = new PropertyMatchingWeights(); + } + + public Factory(PropertyMatchingWeights matchingWeights) { + this.matchingWeights = matchingWeights; + } + + /* + * (non-Javadoc) + * + * @see + * ma.glasnost.orika.metadata.ClassMapBuilderFactory#newClassMapBuilder + * (ma.glasnost.orika.metadata.Type, ma.glasnost.orika.metadata.Type, + * ma.glasnost.orika.property.PropertyResolverStrategy, + * ma.glasnost.orika.DefaultFieldMapper[]) + */ + @Override + protected ClassMapBuilder newClassMapBuilder(Type aType, Type bType, MapperFactory mapperFactory, + PropertyResolverStrategy propertyResolver, DefaultFieldMapper[] defaults) { + + return new ScoringClassMapBuilder(aType, bType, mapperFactory, propertyResolver, defaults, matchingWeights); + } + + } + + + + /** + * FieldMatchScore is used to score the match of a pair of property expressions + * + * @author matt.deboer@gmail.com + * + */ + public static class FieldMatchScore implements Comparable { + + private static final List IGNORED_WORDS = Arrays.asList("with","this","that","an","a","of","the"); + /* + * TODO: static for now; should probably be computed + */ + private static final double MAX_POSSIBLE_SCORE = 50.0; + + private final PropertyMatchingWeights matchingWeights; + + private boolean contains; + private boolean containsIgnoreCase; + private boolean typeMatch; + private Property propertyA; + private Property propertyB; + private int hashCode; + private double commonWordCount; + private double avgWordCount; + private double wordMatchScore; + private double score; + private double typeMatchScore; + private double commonWordsScore; + private double containsScore; + + public FieldMatchScore(Property propertyA, Property propertyB, PropertyMatchingWeights matchingWeights) { + + this.matchingWeights = matchingWeights; + this.propertyA = propertyA; + this.propertyB = propertyB; + + String propertyALower = propertyA.getName().toLowerCase(); + String propertyBLower = propertyB.getName().toLowerCase(); + + List aWords = splitIntoLowerCaseWords(propertyA.getExpression()); + List bWords = splitIntoLowerCaseWords(propertyB.getExpression()); + + aWords.removeAll(IGNORED_WORDS); + bWords.removeAll(IGNORED_WORDS); + + Set commonWords = intersection(aWords,bWords); + + this.avgWordCount = (aWords.size() + bWords.size()) / 2.0; + + this.commonWordCount = commonWords.size(); + this.wordMatchScore = computeWordMatchScore(aWords, bWords); + + this.contains = propertyA.getName().contains(propertyB.getName()) || propertyB.getName().contains(propertyA.getName()); + this.containsIgnoreCase = contains || propertyALower.contains(propertyBLower) || propertyBLower.contains(propertyALower); + + + this.typeMatch = propertyA.getType().isAssignableFrom(propertyB.getType()) + || propertyB.getType().isAssignableFrom(propertyA.getType()); + + computeOverallScore(); + + this.hashCode = computeHashCode(); + } + + public String toString() { + return + "[" + propertyA.getExpression() + ", " + propertyB.getExpression() + "] {\n" + + " wordMatchScore: " + wordMatchScore + "\n" + + " commonWordScore: " + commonWordsScore + "\n" + + " containsScore: " + containsScore + "\n" + + " typeMatchScore: " + typeMatchScore + "\n" + + " ------------------- \n" + + " total: " + score + "\n" + + "}"; + } + + private Set intersection(Collection setA, Collection setB) { + Set intersection = flatten(setA); + Set temp = flatten(setB); + intersection.retainAll(temp); + return intersection; + } + + private Set flatten(Collection arrays) { + Set set = new LinkedHashSet(); + for (T[] array: arrays) { + for (T item: array) { + set.add(item); + } + } + return set; + } + + public boolean meetsMinimumScore() { + double normalizedScore = ((MAX_POSSIBLE_SCORE / 2.0)* this.matchingWeights.minimumScore()); + return this.score >= normalizedScore; + } + + /** + * Compute the match score between two properties, broken up into arrays of + * words at each property divider level. + * + * @param aWords + * @param bWords + * @return + */ + double computeWordMatchScore(List aWords, List bWords) { + + Set aWordsRemaining = new LinkedHashSet(flatten(aWords)); + Set bWordsRemaining = new LinkedHashSet(flatten(bWords)); + + TreeSet orderedPairs = new TreeSet(); + double aDepth = 0; + for (String[] aWordArray : aWords) { + ++aDepth; + for (String aWord : aWordArray) { + double bDepth = 0; + for (String[] bWordArray: bWords) { + for (String bWord : bWordArray) { + ++bDepth; + orderedPairs.add(new WordPair(aWord, bWord, (aDepth/aWords.size()), (bDepth/bWords.size()), matchingWeights)); + } + } + } + } + + double score = 0.0d; + for (WordPair w: orderedPairs) { + if (aWordsRemaining.contains(w.aWord) && bWordsRemaining.contains(w.bWord)) { + score += w.score; + aWordsRemaining.remove(w.aWord); + bWordsRemaining.remove(w.bWord); + } + } + + double remains = (aWordsRemaining.size() + bWordsRemaining.size()) / 2.0; + double initial = (aWords.size() + bWords.size()) / 2.0; + double unmatchedWordsCount = (remains - initial) * (matchingWeights.unmatchedWords()); + + return score + unmatchedWordsCount; + } + + private void computeOverallScore() { + + this.containsScore = this.matchingWeights.containsName() * (this.containsIgnoreCase ? 10 : 0); + if (this.commonWordCount == 0) { + this.commonWordsScore = 0.0; + } else { + this.commonWordsScore = (this.matchingWeights.commonWordCount()) * (Math.pow(2 * this.commonWordCount, 2.0)*((avgWordCount + commonWordCount)/avgWordCount)); + } + this.typeMatchScore = (this.matchingWeights.typeMatch()) * (this.typeMatch ? 1.0 : 0); + this.score = this.wordMatchScore + commonWordsScore + containsScore + typeMatchScore; + } + + /** + * WordPair is used to rank a match of a given set of words based on + * word depth and levenshtein distance between the words + * + */ + private static class WordPair implements Comparable{ + private String aWord; + private String bWord; + private double score; + + private WordPair(String aWord, String bWord, double aWordDepth, double bWordDepth, PropertyMatchingWeights matchingWeights) { + this.aWord = aWord; + this.bWord = bWord; + double aDepth = (1.0 + aWordDepth) * (matchingWeights.nestedDepth); + double bDepth = (1.0 + bWordDepth) * (matchingWeights.nestedDepth); + double editDistance = getLevenshteinDistance(aWord, bWord); + double distanceWeight = matchingWeights.editDistance * (1.0 / (editDistance + 1.0)); + double wordLength = Math.max(aWord.length(), bWord.length()); + double wordLengthWeight = matchingWeights.editDistance * Math.sqrt(wordLength); + this.score = aDepth + bDepth + distanceWeight + wordLengthWeight; + } + /* (non-Javadoc) + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(WordPair o) { + double score = this.score - o.score; + if (score < 0) { + return 1; + } else if (score > 0) { + return -1; + } else { + return 0; + } + } + + public String toString() { + return "[" + aWord + "],[" + bWord + "] = " + score; + } + } + + /** + * Computes the levenshtein distance of 2 strings + * + * @param s + * @param t + * @return + */ + private static int getLevenshteinDistance(String s, String t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + int lengthOfS = s.length(); + int lengthOfT = t.length(); + + if (lengthOfS == 0) { + return lengthOfT; + } else if (lengthOfT == 0) { + return lengthOfS; + } + + if (lengthOfS > lengthOfT) { + // swap the input strings to consume less memory + String tmp = s; + s = t; + t = tmp; + lengthOfS = lengthOfT; + lengthOfT = t.length(); + } + + int previousCosts[] = new int[lengthOfS + 1]; + int costs[] = new int[lengthOfS + 1]; + int swap[]; + + int indexOfS; + int indexOfT; + + char charAtIndexOfT; // jth character of t + int cost; + + for (indexOfS = 0; indexOfS <= lengthOfS; indexOfS++) { + previousCosts[indexOfS] = indexOfS; + } + + for (indexOfT = 1; indexOfT <= lengthOfT; indexOfT++) { + charAtIndexOfT = t.charAt(indexOfT - 1); + costs[0] = indexOfT; + + for (indexOfS = 1; indexOfS <= lengthOfS; indexOfS++) { + cost = s.charAt(indexOfS - 1) == charAtIndexOfT ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally + // left and up +cost + costs[indexOfS] = Math.min(Math.min(costs[indexOfS - 1] + 1, previousCosts[indexOfS] + 1), previousCosts[indexOfS - 1] + + cost); + } + + // copy current distance counts to 'previous row' distance + // counts + swap = previousCosts; + previousCosts = costs; + costs = swap; + } + + // previousCosts now has the most recent cost counts + return previousCosts[lengthOfS]; + } + + /** + * Pattern is used to split a string into words on camel-case word boundaries + */ + private static final String WORD_SPLITTER = String.format("%s|%s|%s", + "(?<=[A-Z])(?=[A-Z][a-z])", "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])"); + + /** + * Splits a given property expression into arrays of lower-case words; + * result is returned as a set of String[], which represent a property + * component split on word boundaries. + * + * @param s + * @return + */ + private static List splitIntoLowerCaseWords(String s) { + List results = new ArrayList(); + for (String property: s.split("[.]")) { + String[] words = property.split(WORD_SPLITTER); + for (int i=0; i < words.length; ++i) { + words[i] = words[i].toLowerCase(); + } + results.add(words); + } + return results; + } + /* + * (non-Javadoc) + * + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(FieldMatchScore that) { + /* + * Higher scores are better, and should be ordered first ("lower") + */ + if (this.score < that.score) { + return 1; + } else if (this.score > that.score) { + return -1; + } else { + return 0; + } + } + + private int computeHashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((propertyA == null) ? 0 : propertyA.hashCode()); + result = prime * result + ((propertyB == null) ? 0 : propertyB.hashCode()); + return result; + } + + public int hashCode() { + return hashCode; + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FieldMatchScore other = (FieldMatchScore) obj; + if (propertyA == null) { + if (other.propertyA != null) + return false; + } else if (!propertyA.equals(other.propertyA)) + return false; + if (propertyB == null) { + if (other.propertyB != null) + return false; + } else if (!propertyB.equals(other.propertyB)) + return false; + return true; + } + + } + +} diff --git a/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java b/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java index 3026a3c0..fe07f392 100644 --- a/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java @@ -93,7 +93,66 @@ public FieldMatchScore(Property propertyA, Property propertyB) { || propertyB.getType() .isAssignableFrom(propertyA.getType()); } - + + public static int getLevenshteinDistance(String s, String t) { + if (s == null || t == null) { + throw new IllegalArgumentException("Strings must not be null"); + } + int n = s.length(); + int m = t.length(); + + if (n == 0) { + return m; + } else if (m == 0) { + return n; + } + + if (n > m) { + // swap the input strings to consume less memory + String tmp = s; + s = t; + t = tmp; + n = m; + m = t.length(); + } + + int p[] = new int[n+1]; //'previous' cost array, horizontally + int d[] = new int[n+1]; // cost array, horizontally + int _d[]; //placeholder to assist in swapping p and d + + // indexes into strings s and t + int i; // iterates through s + int j; // iterates through t + + char t_j; // jth character of t + + int cost; // cost + + for (i = 0; i<=n; i++) { + p[i] = i; + } + + for (j = 1; j<=m; j++) { + t_j = t.charAt(j-1); + d[0] = j; + + for (i=1; i<=n; i++) { + cost = s.charAt(i-1)==t_j ? 0 : 1; + // minimum of cell to the left+1, to the top+1, diagonally left and up +cost + d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1), p[i-1]+cost); + } + + // copy current distance counts to 'previous row' distance counts + _d = p; + p = d; + d = _d; + } + + // our last action in the above loop was to switch d and p, so p now + // actually has the most recent cost counts + return p[n]; + } + /* * (non-Javadoc) * diff --git a/core/src/test/java/ma/glasnost/orika/test/metadata/ScoringClassMapBuilderTest.java b/core/src/test/java/ma/glasnost/orika/test/metadata/ScoringClassMapBuilderTest.java new file mode 100644 index 00000000..89eb8064 --- /dev/null +++ b/core/src/test/java/ma/glasnost/orika/test/metadata/ScoringClassMapBuilderTest.java @@ -0,0 +1,150 @@ +/* + * Orika - simpler, better and faster Java bean mapping + * + * Copyright (C) 2011 Orika authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ma.glasnost.orika.test.metadata; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import junit.framework.Assert; +import ma.glasnost.orika.MapperFactory; +import ma.glasnost.orika.impl.DefaultMapperFactory; +import ma.glasnost.orika.metadata.ClassMap; +import ma.glasnost.orika.metadata.FieldMap; +import ma.glasnost.orika.metadata.ScoringClassMapBuilder; + +import org.junit.Test; + +/** + * @author matt.deboer@gmail.com + * + */ +public class ScoringClassMapBuilderTest { + public static class Name { + public String first; + public String middle; + public String last; + } + + public static class Source { + public String lastName; + public Integer age; + public PostalAddress postalAddress; + public String firstName; + public String stateOfBirth; + public String eyeColor; + public String driversLicenseNumber; + } + + public static class Destination { + public Name name; + public Integer currentAge; + public String streetAddress; + public String birthState; + public String countryCode; + public String favoriteColor; + public String id; + } + + public static class PostalAddress { + public String street; + public String city; + public String state; + public String postalCode; + public Country country; + } + + public static class Country { + public String name; + public String alphaCode; + public int numericCode; + } + + @Test + public void testClassMapBuilderExtension() { + + MapperFactory factory = new DefaultMapperFactory.Builder().classMapBuilderFactory(new ScoringClassMapBuilder.Factory()).build(); + + ClassMap map = factory.classMap(Source.class, Destination.class).byDefault().toClassMap(); + Map mapping = new HashMap(); + for (FieldMap f : map.getFieldsMapping()) { + mapping.put(f.getSource().getExpression(), f.getDestination().getExpression()); + } + + /* + * Check that properties we expect were mapped + */ + Assert.assertEquals("name.first", mapping.get("firstName")); + Assert.assertEquals("name.last", mapping.get("lastName")); + Assert.assertEquals("streetAddress", mapping.get("postalAddress.street")); + Assert.assertEquals("countryCode", mapping.get("postalAddress.country.alphaCode")); + Assert.assertEquals("currentAge", mapping.get("age")); + Assert.assertEquals("birthState", mapping.get("stateOfBirth")); + + /* + * Check that properties that we don't expect aren't mapped by accident + */ + Assert.assertFalse(mapping.containsKey("driversLicenseNumber")); + Assert.assertFalse(mapping.containsKey("eyeColor")); + + + } + + @SuppressWarnings("unchecked") + @Test + public void testSplittingWords() throws Throwable { + Map> tests = new HashMap>() { + private static final long serialVersionUID = 1L; + { + put("lowercase", Arrays.asList(new String[]{"lowercase"})); + put("Class", Arrays.asList(new String[]{"class"})); + put("MyClass", Arrays.asList(new String[]{"my", "class"})); + put("HTML", Arrays.asList(new String[]{"html"})); + put("PDFLoader", Arrays.asList(new String[]{"pdf", "loader"})); + put("AString", Arrays.asList(new String[]{"a", "string"})); + put("SimpleXMLParser", Arrays.asList(new String[]{"Simple", "xml", "parser"})); + put("GL11Version", Arrays.asList(new String[]{"gl", "11", "version"})); + put("99Bottles", Arrays.asList(new String[]{"99", "bottles"})); + put("May5", Arrays.asList(new String[]{"may", "5"})); + put("BFG9000", Arrays.asList(new String[]{"bfg", "9000"})); + put("SimpleXMLParser", Arrays.asList(new String[]{"simple", "xml", "parser"})); + put("postalAddress.country", Arrays.asList(new String[]{"postal", "address"}, new String[]{"country"})); + put("aVeryLongWord.name.first", Arrays.asList(new String[]{"a", "very", "long", "word"}, new String[]{"name"}, new String[]{"first"})); + } + }; + + Method splitIntoWords = ScoringClassMapBuilder.FieldMatchScore.class.getDeclaredMethod("splitIntoLowerCaseWords", String.class); + splitIntoWords.setAccessible(true); + + for (Entry> test : tests.entrySet()) { + + List testValue = test.getValue(); + List result = (List)splitIntoWords.invoke(null, test.getKey()); + Assert.assertEquals(testValue.size(), result.size()); + for (int i=0, len = testValue.size(); i < len; ++i) { + Assert.assertTrue("Expected <"+Arrays.toString(testValue.get(i)) + ">, found <" + Arrays.toString(result.get(i))+">", + Arrays.deepEquals(testValue.get(i), result.get(i))); + } + } + + } + +} diff --git a/core/src/test/resources/logback-test.xml b/core/src/test/resources/logback-test.xml index 8f8855b5..8c3f18ca 100644 --- a/core/src/test/resources/logback-test.xml +++ b/core/src/test/resources/logback-test.xml @@ -15,8 +15,12 @@ - --> - --> + + + + + + \ No newline at end of file From 4c1b95b29e8748ec503cd19bb79e5d4d824b668b Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 00:17:19 -0700 Subject: [PATCH 05/11] Restored original ClassMapBuilderExtensibilityTestCase --- .../ClassMapBuilderExtensibilityTestCase.java | 619 ++++++++---------- 1 file changed, 280 insertions(+), 339 deletions(-) diff --git a/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java b/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java index fe07f392..61409771 100644 --- a/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/extensibility/ClassMapBuilderExtensibilityTestCase.java @@ -57,363 +57,304 @@ */ public class ClassMapBuilderExtensibilityTestCase { - /** - * FieldMatchScore is used to rank "closeness" of a given field mapping - * - * @author matt.deboer@gmail.com - * - */ - private static class FieldMatchScore implements Comparable { - - private boolean contains; - private boolean containsIgnoreCase; - private boolean typeMatch; - private int distance; - private int distanceIgnoreCase; - private Property propertyA; - private Property propertyB; - - public FieldMatchScore(Property propertyA, Property propertyB) { - String propertyALower = propertyA.getName().toLowerCase(); - String propertyBLower = propertyB.getName().toLowerCase(); - - this.contains = propertyA.getName().contains(propertyB.getName()) - || propertyB.getName().contains(propertyA.getName()); - this.containsIgnoreCase = contains - || propertyALower.contains(propertyBLower) - || propertyBLower.contains(propertyALower); - this.distance = StringUtils.getLevenshteinDistance( - propertyA.getName(), propertyB.getName()); - this.distanceIgnoreCase = StringUtils.getLevenshteinDistance( - propertyALower, propertyBLower); - this.propertyA = propertyA; - this.propertyB = propertyB; - this.typeMatch = propertyA.getType().isAssignableFrom( - propertyB.getType()) - || propertyB.getType() - .isAssignableFrom(propertyA.getType()); - } - - public static int getLevenshteinDistance(String s, String t) { - if (s == null || t == null) { - throw new IllegalArgumentException("Strings must not be null"); + /** + * FieldMatchScore is used to rank "closeness" of a given field mapping + * + * @author matt.deboer@gmail.com + * + */ + private static class FieldMatchScore implements Comparable { + + private boolean contains; + private boolean containsIgnoreCase; + private boolean typeMatch; + private int distance; + private int distanceIgnoreCase; + private Property propertyA; + private Property propertyB; + + public FieldMatchScore(Property propertyA, Property propertyB) { + String propertyALower = propertyA.getName().toLowerCase(); + String propertyBLower = propertyB.getName().toLowerCase(); + + this.contains = propertyA.getName().contains(propertyB.getName()) + || propertyB.getName().contains(propertyA.getName()); + this.containsIgnoreCase = contains + || propertyALower.contains(propertyBLower) + || propertyBLower.contains(propertyALower); + this.distance = StringUtils.getLevenshteinDistance( + propertyA.getName(), propertyB.getName()); + this.distanceIgnoreCase = StringUtils.getLevenshteinDistance( + propertyALower, propertyBLower); + this.propertyA = propertyA; + this.propertyB = propertyB; + this.typeMatch = propertyA.getType().isAssignableFrom( + propertyB.getType()) + || propertyB.getType() + .isAssignableFrom(propertyA.getType()); + } + + /* + * (non-Javadoc) + * + * @see java.lang.Comparable#compareTo(java.lang.Object) + */ + public int compareTo(FieldMatchScore that) { + if (this.containsIgnoreCase && !that.containsIgnoreCase) { + return -1; + } else if (!this.containsIgnoreCase && that.containsIgnoreCase) { + return 1; + } + + if (this.contains && !that.contains) { + return -1; + } else if (!this.contains && that.contains) { + return 1; + } + + if (this.distanceIgnoreCase < that.distanceIgnoreCase) { + return -1; + } else if (this.distanceIgnoreCase > that.distanceIgnoreCase) { + return 1; + } + + if (this.distance < that.distance) { + return -1; + } else if (this.distance > that.distance) { + return 1; } - int n = s.length(); - int m = t.length(); - if (n == 0) { - return m; - } else if (m == 0) { - return n; + if (this.typeMatch && !that.typeMatch) { + return -1; + } else if (!this.typeMatch && that.typeMatch) { + return 1; } - if (n > m) { - // swap the input strings to consume less memory - String tmp = s; - s = t; - t = tmp; - n = m; - m = t.length(); + if (this.propertyA.getName().length() > that.propertyA.getName() + .length()) { + return -1; + } else if (this.propertyA.getName().length() > that.propertyA + .getName().length()) { + return 1; } - int p[] = new int[n+1]; //'previous' cost array, horizontally - int d[] = new int[n+1]; // cost array, horizontally - int _d[]; //placeholder to assist in swapping p and d + int propACompare = this.propertyA.getName().compareTo( + that.propertyA.getName()); + if (propACompare < 0) { + return -1; + } else if (propACompare > 0) { + return 1; + } - // indexes into strings s and t - int i; // iterates through s - int j; // iterates through t + return this.propertyB.getName().compareTo(that.propertyB.getName()); + } - char t_j; // jth character of t + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } - int cost; // cost + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } - for (i = 0; i<=n; i++) { - p[i] = i; + } + + public static class ScoringClassMapBuilder extends + ClassMapBuilder { + + public static class Factory extends ClassMapBuilderFactory { + + /* + * (non-Javadoc) + * + * @see + * ma.glasnost.orika.metadata.ClassMapBuilderFactory#newClassMapBuilder + * (ma.glasnost.orika.metadata.Type, + * ma.glasnost.orika.metadata.Type, + * ma.glasnost.orika.property.PropertyResolverStrategy, + * ma.glasnost.orika.DefaultFieldMapper[]) + */ + @Override + protected ClassMapBuilder newClassMapBuilder( + Type aType, Type bType, + MapperFactory mapperFactory, + PropertyResolverStrategy propertyResolver, + DefaultFieldMapper[] defaults) { + + return new ScoringClassMapBuilder(aType, bType, + mapperFactory, propertyResolver, defaults); } - for (j = 1; j<=m; j++) { - t_j = t.charAt(j-1); - d[0] = j; + } + + /** + * @param aType + * @param bType + * @param propertyResolver + * @param defaults + */ + protected ScoringClassMapBuilder(Type aType, Type bType, + MapperFactory mapperFactory, PropertyResolverStrategy propertyResolver, + DefaultFieldMapper[] defaults) { + super(aType, bType, mapperFactory, propertyResolver, defaults); + } - for (i=1; i<=n; i++) { - cost = s.charAt(i-1)==t_j ? 0 : 1; - // minimum of cell to the left+1, to the top+1, diagonally left and up +cost - d[i] = Math.min(Math.min(d[i-1]+1, p[i]+1), p[i-1]+cost); + public Map getPropertyExpressions(Type type) { + + PropertyResolverStrategy propertyResolver = getPropertyResolver(); + + Map properties = new HashMap(); + LinkedHashMap toProcess = new LinkedHashMap( + propertyResolver.getProperties(type)); + + while (!toProcess.isEmpty()) { + + Entry entry = toProcess.entrySet().iterator().next(); + if (!entry.getKey().equals("class")) { + + if (!ClassUtil.isImmutable(entry.getValue().getType())) { + Map props = propertyResolver + .getProperties(entry.getValue().getType()); + if (!props.isEmpty()) { + for (Entry property : props + .entrySet()) { + if (!property.getKey().equals("class")) { + String expression = entry.getKey() + "." + property.getKey(); + toProcess.put(expression, resolveProperty(type, expression)); + } + } + } else { + properties.put( + entry.getKey(), resolveProperty(type, entry.getKey())); + } + } else { + properties.put( + entry.getKey(), resolveProperty(type, entry.getKey())); + } } + toProcess.remove(entry.getKey()); + } + return properties; + } - // copy current distance counts to 'previous row' distance counts - _d = p; - p = d; - d = _d; + /* + * (non-Javadoc) + * + * @see + * ma.glasnost.orika.metadata.ClassMapBuilder#byDefault(ma.glasnost. + * orika.DefaultFieldMapper[]) + */ + public ClassMapBuilder byDefault( + DefaultFieldMapper... withDefaults) { + + DefaultFieldMapper[] defaults; + if (withDefaults.length == 0) { + defaults = getDefaultFieldMappers(); + } else { + defaults = withDefaults; + } + /* + * For our custom 'byDefault' method, we're going to try and match + * fields by their Levenshtein distance + */ + TreeSet matchScores = new TreeSet(); + + Map propertiesForA = getPropertyExpressions(getAType()); + Map propertiesForB = getPropertyExpressions(getBType()); + + for (final Entry propertyA : propertiesForA.entrySet()) { + if (!propertyA.getValue().getName().equals("class")) { + for (final Entry propertyB : propertiesForB + .entrySet()) { + if (!propertyB.getValue().getName().equals("class")) { + matchScores.add(new FieldMatchScore(propertyA + .getValue(), propertyB.getValue())); + } + } + } } - // our last action in the above loop was to switch d and p, so p now - // actually has the most recent cost counts - return p[n]; + Set unmatchedFields = new HashSet( + this.getPropertiesForTypeA()); + unmatchedFields.remove("class"); + + for (FieldMatchScore score : matchScores) { + + if (!this.getMappedPropertiesForTypeA().contains( + score.propertyA.getExpression()) + && !this.getMappedPropertiesForTypeB().contains( + score.propertyB.getExpression())) { + + fieldMap(score.propertyA.getExpression(), + score.propertyB.getExpression()).add(); + unmatchedFields.remove(score.propertyA); + } + } + + /* + * Apply any default field mappers to the unmapped fields + */ + for (String propertyNameA : unmatchedFields) { + Property prop = resolvePropertyForA(propertyNameA); + for (DefaultFieldMapper defaulter : defaults) { + String suggestion = defaulter.suggestMappedField( + propertyNameA, prop.getType()); + if (suggestion != null + && getPropertiesForTypeB().contains(suggestion)) { + if (!getMappedPropertiesForTypeB().contains(suggestion)) { + fieldMap(propertyNameA, suggestion).add(); + } + } + } + } + + return this; } + + } + + public static class Name { + public String first; + public String middle; + public String last; + } + + public static class Source { + public String lastName; + public Integer age; + public String postalAddress; + public String firstName; + public String stateOfBirth; + } + + public static class Destination { + public Name name; + public Integer currentAge; + public String address; + public String birthState; + } + + @Test + public void testClassMapBuilderExtension() { + + MapperFactory factory = new DefaultMapperFactory.Builder() + .classMapBuilderFactory(new ScoringClassMapBuilder.Factory()) + .build(); + + ClassMap map = factory.classMap(Source.class, Destination.class).byDefault().toClassMap(); + Map mapping = new HashMap(); + for (FieldMap f: map.getFieldsMapping()) { + mapping.put(f.getSource().getExpression(), f.getDestination().getExpression()); + } + + Assert.assertEquals("name.first", mapping.get("firstName")); + Assert.assertEquals("name.last", mapping.get("lastName")); + Assert.assertEquals("address", mapping.get("postalAddress")); + Assert.assertEquals("currentAge", mapping.get("age")); + Assert.assertEquals("birthState", mapping.get("stateOfBirth")); - /* - * (non-Javadoc) - * - * @see java.lang.Comparable#compareTo(java.lang.Object) - */ - public int compareTo(FieldMatchScore that) { - if (this.containsIgnoreCase && !that.containsIgnoreCase) { - return -1; - } else if (!this.containsIgnoreCase && that.containsIgnoreCase) { - return 1; - } - - if (this.contains && !that.contains) { - return -1; - } else if (!this.contains && that.contains) { - return 1; - } - - if (this.distanceIgnoreCase < that.distanceIgnoreCase) { - return -1; - } else if (this.distanceIgnoreCase > that.distanceIgnoreCase) { - return 1; - } - - if (this.distance < that.distance) { - return -1; - } else if (this.distance > that.distance) { - return 1; - } - - if (this.typeMatch && !that.typeMatch) { - return -1; - } else if (!this.typeMatch && that.typeMatch) { - return 1; - } - - if (this.propertyA.getName().length() > that.propertyA.getName() - .length()) { - return -1; - } else if (this.propertyA.getName().length() > that.propertyA - .getName().length()) { - return 1; - } - - int propACompare = this.propertyA.getName().compareTo( - that.propertyA.getName()); - if (propACompare < 0) { - return -1; - } else if (propACompare > 0) { - return 1; - } - - return this.propertyB.getName().compareTo(that.propertyB.getName()); - } - - @Override - public int hashCode() { - return HashCodeBuilder.reflectionHashCode(this); - } - - @Override - public boolean equals(Object obj) { - return EqualsBuilder.reflectionEquals(this, obj); - } - - } - - public static class ScoringClassMapBuilder extends - ClassMapBuilder { - - public static class Factory extends ClassMapBuilderFactory { - - /* - * (non-Javadoc) - * - * @see - * ma.glasnost.orika.metadata.ClassMapBuilderFactory#newClassMapBuilder - * (ma.glasnost.orika.metadata.Type, - * ma.glasnost.orika.metadata.Type, - * ma.glasnost.orika.property.PropertyResolverStrategy, - * ma.glasnost.orika.DefaultFieldMapper[]) - */ - @Override - protected ClassMapBuilder newClassMapBuilder( - Type aType, Type bType, - MapperFactory mapperFactory, - PropertyResolverStrategy propertyResolver, - DefaultFieldMapper[] defaults) { - - return new ScoringClassMapBuilder(aType, bType, - mapperFactory, propertyResolver, defaults); - } - - } - - /** - * @param aType - * @param bType - * @param propertyResolver - * @param defaults - */ - protected ScoringClassMapBuilder(Type aType, Type bType, - MapperFactory mapperFactory, PropertyResolverStrategy propertyResolver, - DefaultFieldMapper[] defaults) { - super(aType, bType, mapperFactory, propertyResolver, defaults); - } - - public Map getPropertyExpressions(Type type) { - - PropertyResolverStrategy propertyResolver = getPropertyResolver(); - - Map properties = new HashMap(); - LinkedHashMap toProcess = new LinkedHashMap( - propertyResolver.getProperties(type)); - - while (!toProcess.isEmpty()) { - - Entry entry = toProcess.entrySet().iterator().next(); - if (!entry.getKey().equals("class")) { - - if (!ClassUtil.isImmutable(entry.getValue().getType())) { - Map props = propertyResolver - .getProperties(entry.getValue().getType()); - if (!props.isEmpty()) { - for (Entry property : props - .entrySet()) { - if (!property.getKey().equals("class")) { - String expression = entry.getKey() + "." + property.getKey(); - toProcess.put(expression, resolveProperty(type, expression)); - } - } - } else { - properties.put( - entry.getKey(), resolveProperty(type, entry.getKey())); - } - } else { - properties.put( - entry.getKey(), resolveProperty(type, entry.getKey())); - } - } - toProcess.remove(entry.getKey()); - } - return properties; - } - - /* - * (non-Javadoc) - * - * @see - * ma.glasnost.orika.metadata.ClassMapBuilder#byDefault(ma.glasnost. - * orika.DefaultFieldMapper[]) - */ - public ClassMapBuilder byDefault( - DefaultFieldMapper... withDefaults) { - - DefaultFieldMapper[] defaults; - if (withDefaults.length == 0) { - defaults = getDefaultFieldMappers(); - } else { - defaults = withDefaults; - } - /* - * For our custom 'byDefault' method, we're going to try and match - * fields by their Levenshtein distance - */ - TreeSet matchScores = new TreeSet(); - - Map propertiesForA = getPropertyExpressions(getAType()); - Map propertiesForB = getPropertyExpressions(getBType()); - - for (final Entry propertyA : propertiesForA.entrySet()) { - if (!propertyA.getValue().getName().equals("class")) { - for (final Entry propertyB : propertiesForB - .entrySet()) { - if (!propertyB.getValue().getName().equals("class")) { - matchScores.add(new FieldMatchScore(propertyA - .getValue(), propertyB.getValue())); - } - } - } - } - - Set unmatchedFields = new HashSet( - this.getPropertiesForTypeA()); - unmatchedFields.remove("class"); - - for (FieldMatchScore score : matchScores) { - - if (!this.getMappedPropertiesForTypeA().contains( - score.propertyA.getExpression()) - && !this.getMappedPropertiesForTypeB().contains( - score.propertyB.getExpression())) { - - fieldMap(score.propertyA.getExpression(), - score.propertyB.getExpression()).add(); - unmatchedFields.remove(score.propertyA); - } - } - - /* - * Apply any default field mappers to the unmapped fields - */ - for (String propertyNameA : unmatchedFields) { - Property prop = resolvePropertyForA(propertyNameA); - for (DefaultFieldMapper defaulter : defaults) { - String suggestion = defaulter.suggestMappedField( - propertyNameA, prop.getType()); - if (suggestion != null - && getPropertiesForTypeB().contains(suggestion)) { - if (!getMappedPropertiesForTypeB().contains(suggestion)) { - fieldMap(propertyNameA, suggestion).add(); - } - } - } - } - - return this; - } - - } - - public static class Name { - public String first; - public String middle; - public String last; - } - - public static class Source { - public String lastName; - public Integer age; - public String postalAddress; - public String firstName; - public String stateOfBirth; - } - - public static class Destination { - public Name name; - public Integer currentAge; - public String address; - public String birthState; - } - - @Test - public void testClassMapBuilderExtension() { - - MapperFactory factory = new DefaultMapperFactory.Builder() - .classMapBuilderFactory(new ScoringClassMapBuilder.Factory()) - .build(); - - ClassMap map = factory.classMap(Source.class, Destination.class).byDefault().toClassMap(); - Map mapping = new HashMap(); - for (FieldMap f: map.getFieldsMapping()) { - mapping.put(f.getSource().getExpression(), f.getDestination().getExpression()); - } - - Assert.assertEquals("name.first", mapping.get("firstName")); - Assert.assertEquals("name.last", mapping.get("lastName")); - Assert.assertEquals("address", mapping.get("postalAddress")); - Assert.assertEquals("currentAge", mapping.get("age")); - Assert.assertEquals("birthState", mapping.get("stateOfBirth")); - - } + } } From 85ac28a3016bfe04e3f9f4b425c8e18c10d4505e Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 00:32:56 -0700 Subject: [PATCH 06/11] updated JavaDocs --- .../ma/glasnost/orika/metadata/ScoringClassMapBuilder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java b/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java index 2cb87513..6feb7a2d 100644 --- a/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java +++ b/core/src/main/java/ma/glasnost/orika/metadata/ScoringClassMapBuilder.java @@ -48,6 +48,11 @@ * guess the correct mappings; be sure to test and double-check the mappings * generated to assure they match expectations.

* + * Note: levenshtein distance implementation is pulled from code found in + * Apache Commons Lang org.apache.commons.lang.StringUtils, which is based on + * the implementation provided by Chas Emerick + *
http://www.merriampark.com/ldjava.htm + * * @author matt.deboer@gmail.com * */ From 190c9505b70edb2869b2dc705acf1aece97b981b Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 14:13:58 -0700 Subject: [PATCH 07/11] Added fix for issue #59, with test case --- .../impl/generator/CodeSourceBuilder.java | 8 +-- .../orika/impl/generator/VariableRef.java | 36 +++++++++++-- .../test/collection/CollectionTestCase.java | 52 +++++++++++++++++++ 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/ma/glasnost/orika/impl/generator/CodeSourceBuilder.java b/core/src/main/java/ma/glasnost/orika/impl/generator/CodeSourceBuilder.java index 4e348c05..74a5793e 100644 --- a/core/src/main/java/ma/glasnost/orika/impl/generator/CodeSourceBuilder.java +++ b/core/src/main/java/ma/glasnost/orika/impl/generator/CodeSourceBuilder.java @@ -172,12 +172,13 @@ public CodeSourceBuilder fromArrayOrCollectionToCollection(VariableRef dest, Var throw new MappingException("cannot determine runtime type of destination collection " + dc.getName() + "." + d.name()); } + // Start check if source property ! = null + ifNotNull(s).then(); + if (d.isAssignable()) { statement("if (%s == null) %s", d, d.assign(d.newInstance(src.size()))); } - // Start check if source property ! = null - ifNotNull(s).then(); if (s.isArray()) { if (s.elementType().isPrimitive()) newLine().append("%s.addAll(asList(%s));", d, s); @@ -536,8 +537,7 @@ public CodeSourceBuilder assureInstanceExists(VariableRef propertyRef, VariableR if (!ClassUtil.isConcrete(ref.type())) { throw new MappingException("Abstract types are unsupported for nested properties. \n" + ref.name()); } -// statement("if(%s == null) %s", ref, -// ref.assign("(%s)mapperFacade.newObject(source, %s, mappingContext)", ref.typeName(), usedType(ref))); + statement("if(%s == null) %s", ref, ref.assign("(%s)%s(source, mappingContext)", ref.typeName(), usedMapperFacadeNewObjectCall(ref,source))); } 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 3f74d567..d1e92f86 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 @@ -338,9 +338,9 @@ public String isNull() { return getter() + " == null"; } - public String notNull() { - return getter() + " != null"; - } +// public String notNull() { +// return getter() + " != null"; +// } public String ifNotNull() { return "if ( " + notNull() + ") "; @@ -486,4 +486,34 @@ public String ifPathNotNull() { } return path.toString(); } + + /** + * @return a nested-property safe null check for this property + */ + public String notNull() { + StringBuilder path = new StringBuilder(); + path.append("("); + if (property() != null && property().hasPath()) { + boolean first = true; + + String expression = "source"; + + for (final Property p : property().getPath()) { + if (!first) { + path.append(" && "); + } else { + first = false; + } + expression = getGetter(p, expression); + path.append(format("%s != null", expression)); + } + } + if (path.length() > 1) { + path.append(" && "); + } + path.append(format("%s != null", getter())); + path.append(")"); + + return path.toString(); + } } diff --git a/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java b/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java index 2c17e9a2..1c88407f 100644 --- a/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java @@ -52,6 +52,17 @@ public void testStringToStringWithGetterOnlyCollection() { Assert.assertEquals(3, destination.getTags().size()); } + @Test + public void testStringToStringWithGetterOnlyCollection_nullCollection() { + Source source = new Source(); + //source.setTags(Arrays.asList("soa", "java", "rest")); + + Destination destination = MappingUtil.getMapperFactory(true).getMapperFacade().map(source, Destination.class); + + Assert.assertNull(destination.getNames()); + } + + static public class A { private Set tags; @@ -89,4 +100,45 @@ public List getTags() { } } + + public static class Name { + public String first; + public String last; + } + + public static class Source { + private List names; + + /** + * @return the names + */ + public List getNames() { + return names; + } + + /** + * @param names the names to set + */ + public void setNames(List names) { + this.names = names; + } + } + + public static class Destination { + private List names; + + /** + * @return the names + */ + public List getNames() { + return names; + } + + /** + * @param names the names to set + */ + public void setNames(List names) { + this.names = names; + } + } } From a32f3b4c7893676480a2bf859e4cfece8a06bb51 Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 14:17:33 -0700 Subject: [PATCH 08/11] Added test case for collection to array also --- .../test/collection/CollectionTestCase.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java b/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java index 1c88407f..56f025fe 100644 --- a/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java +++ b/core/src/test/java/ma/glasnost/orika/test/collection/CollectionTestCase.java @@ -53,15 +53,23 @@ public void testStringToStringWithGetterOnlyCollection() { } @Test - public void testStringToStringWithGetterOnlyCollection_nullCollection() { + public void nullSourceCollection_toCollection() { Source source = new Source(); - //source.setTags(Arrays.asList("soa", "java", "rest")); Destination destination = MappingUtil.getMapperFactory(true).getMapperFacade().map(source, Destination.class); Assert.assertNull(destination.getNames()); } + @Test + public void nullSourceCollection_toArray() { + Source source = new Source(); + + Destination2 destination = MappingUtil.getMapperFactory(true).getMapperFacade().map(source, Destination2.class); + + Assert.assertNull(destination.getNames()); + } + static public class A { private Set tags; @@ -141,4 +149,22 @@ public void setNames(List names) { this.names = names; } } + + public static class Destination2 { + private Name[] names; + + /** + * @return the names + */ + public Name[] getNames() { + return names; + } + + /** + * @param names the names to set + */ + public void setNames(Name[] names) { + this.names = names; + } + } } From 4b3cd332f433602da324cc1c9257e53889c9b7dd Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 17:11:34 -0700 Subject: [PATCH 09/11] change to README.md to trigger new Travis CI build.. : ) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f7aad3b..a2e883ec 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Orika ![Build status](https://secure.travis-ci.org/elaatifi/orika.png) ----------------------------------------------------------------------- -*NEW* We are pleased to announce the release of Orika *1.2.2*! _This version is available on Maven central repository_ +*NEW* We are pleased to announce the release of Orika *1.3.0*! _This version is available on Maven central repository_ From ecca5f495b58b96cdb9cd18e12dcfd18ed912a17 Mon Sep 17 00:00:00 2001 From: mdeboer Date: Mon, 8 Oct 2012 19:23:50 -0700 Subject: [PATCH 10/11] excluded old/alternate version of javassist to avoid multiple javassist versions on classpath for tests --- pom.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pom.xml b/pom.xml index 4e8a5196..b2841448 100644 --- a/pom.xml +++ b/pom.xml @@ -140,6 +140,12 @@ hibernate-entitymanager 3.6.10.Final test + + + javassist + javassist + + From e1415f51ac0bea0c776585514ce43611f9f35ccb Mon Sep 17 00:00:00 2001 From: mdeboer Date: Tue, 9 Oct 2012 11:55:12 -0700 Subject: [PATCH 11/11] removed openjdk6 from build environments, since it appears to be unstable on travisCI (can take 30+ minutes to initialize the environment) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index e194c02a..967a17f5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: java jdk: - oraclejdk7 - - openjdk6 +# - openjdk6 script: mvn -q clean install # whitelist branches: