orika-mapper / orika

Simpler, better and faster Java bean mapping framework
http://orika-mapper.github.io/orika-docs/
Apache License 2.0
1.29k stars 269 forks source link

Strange behaviour when mapping shared object instance multiple times with inheritance #321

Open Chr3is opened 5 years ago

Chr3is commented 5 years ago

There's a strange behaviour while mapping an object instance which was extracted from a list into different objects (with inheritance). In some cases the fields are mapped correctly. This was tested with the lastest Orika version (1.5.4).

Bug1:

Output:

OrikaTest.DestContainer(a=OrikaTest.A(f1=null), b=[OrikaTest.B(super=OrikaTest.A(f1=foo), f2=bar), OrikaTest.B(super=OrikaTest.A(f1=hello), f2=world)])

Bug2:

Output:

OrikaTest.DestContainer(a=OrikaTest.A(f1=foo), b=[OrikaTest.B(super=OrikaTest.A(f1=null), f2=null), OrikaTest.B(super=OrikaTest.A(f1=hello), f2=null)])

Expected: OrikaTest.DestContainer(a=OrikaTest.A(f1=foo), b=[OrikaTest.B(super=OrikaTest.A(f1=foo), f2=bar), OrikaTest.B(super=OrikaTest.A(f1=hello), f2=world)])

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.junit.Assert;
import org.junit.Test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.impl.ConfigurableMapper;
import ma.glasnost.orika.impl.DefaultMapperFactory;

public class OrikaTest {

    @Data
    @AllArgsConstructor
    public static class Container {
        private List<Source> source;

        public Source getFirst() {
            return source.get(0);
        }
    }

    @Data
    @AllArgsConstructor
    public static class Source {
        private SourceAttributes attributes;
    }

    @Data
    @AllArgsConstructor
    public static class SourceAttributes {
        private String f1;
        private String f2;
    }

    @Data
    public static class DestContainer {
        private A a;
        private List<B> b = new ArrayList<>();
    }

    @Data
    public static class A {
        private String f1;
    }

    @EqualsAndHashCode(callSuper = true)
    @Data
    @ToString(callSuper = true)
    public static class B extends A {
        private String f2;
    }

    public static class ExampleMapper extends ConfigurableMapper {

        private final int bug;

        public ExampleMapper(int bug) {
            super(false);
            this.bug = bug;
            init();
        }

        @Override
        protected void configureFactoryBuilder(DefaultMapperFactory.Builder factoryBuilder) {
            factoryBuilder.useAutoMapping(false);
        }

        @Override
        protected void configure(MapperFactory factory) {
            if (bug == 1) {
                factory.classMap(Container.class, DestContainer.class)
                        // switching these fields causes
                        // OrikaTest.DestContainer(a=OrikaTest.A(f1=null), b=[OrikaTest.B(super=OrikaTest.A(f1=foo), f2=bar), OrikaTest.B(super=OrikaTest.A(f1=hello), f2=world)])
                        .field("source", "b")
                        .field("first", "a")
                        .register();
            } else {
                factory.classMap(Container.class, DestContainer.class)
                        .field("first", "a")
                        .field("source", "b")
                        .register();

            }

            factory.classMap(Source.class, A.class)
                    .field("attributes", "")
                    .register();

            if (bug != 2) {
            // removing this causes
            // OrikaTest.DestContainer(a=OrikaTest.A(f1=foo), b=[OrikaTest.B(super=OrikaTest.A(f1=null), f2=null), OrikaTest.B(super=OrikaTest.A(f1=hello), f2=null)])
                factory.classMap(Source.class, B.class)
                        .field("attributes", "")
                        .register();
            }

            factory.classMap(SourceAttributes.class, A.class)
                    .byDefault()
                    .register();

            factory.classMap(SourceAttributes.class, B.class)
                    .byDefault()
                    .register();
        }
    }

    private Container container = new Container(Arrays.asList(new Source(new SourceAttributes("foo", "bar")), new Source(new SourceAttributes("hello", "world"))));

    @Test
    public void testOkMapping() {
        ExampleMapper mapper = new ExampleMapper(0);
        DestContainer destContainer = mapper.map(container, DestContainer.class);
        doAssertions(destContainer);

    }
    @Test
    public void testBug1() {
        ExampleMapper mapper = new ExampleMapper(1);
        DestContainer destContainer = mapper.map(container, DestContainer.class);
        doAssertions(destContainer);

    }
    @Test
    public void testBug2() {
        ExampleMapper mapper = new ExampleMapper(2);
        DestContainer destContainer = mapper.map(container, DestContainer.class);
        doAssertions(destContainer);

    }

    private void doAssertions(DestContainer destContainer) {
        System.out.println(destContainer);
        Assert.assertNotNull(destContainer.getA());
        Assert.assertNotNull(destContainer.getA().getF1());
        Assert.assertNotNull(destContainer.getB());
        Assert.assertNotNull(destContainer.getB().get(0).getF1());
        Assert.assertNotNull(destContainer.getB().get(0).getF2());
        Assert.assertNotNull(destContainer.getB().get(1).getF1());
        Assert.assertNotNull(destContainer.getB().get(1).getF2());
    }

}
Chr3is commented 5 years ago

A possible solution is to use the BoundMapperFacade with the "containsCycle=false" option.