Chris311
Chris311

Reputation: 3992

Orika: Map from String to a List of SomeObjects

Consider the following situation:

public class A {
    private String stringA;

    public String getStringA() {
        return stringA;
    }

    public void setStringA(String stringA) {
        this.stringA = stringA;
    }

}

public class B {

    List<SomeObject> someObjects;

    public List<SomeObject> getSomeObjects() {
        if (someObjects == null) {
            someObjects = new ArrayList<SomeObject>();
        }
        return someObjects;
    }

}

public class SomeObject {
    private String stringSomeObject;

    public String getStringSomeObject() {
        return stringSomeObject;
    }

    public void setStringSomeObject(String stringSomeObject) {
        this.stringSomeObject = stringSomeObject;
    }

}

I want to map from A to B. Whilst mapping these, stringA needs to be mapped to stringSomeObject in SomeObject. I tried to write a Orika-Mapper for this:

public class MyMapper extends ConfigurableMapper {

    @Override
    protected void configure(MapperFactory factory) {
        ConverterFactory converterFactory = factory.getConverterFactory();
        converterFactory.registerConverter(new StringToSomeObjectConverter());

        factory.classMap(A.class, B.class) //
                .field("stringA", "someObjects") //
                .byDefault() //
                .register();
    }

}

It maps class A to B and whenever it encounters a conversion from String to List<SomeObject> it calls a custom-converter:

public class StringToSomeObjectConverter extends CustomConverter<String, List<SomeObject>> {

    private static final String BORROWER_PARTY_TYP_CODE = "147";

    @Override
    public List<SomeObject> convert(String source, Type<? extends List<SomeObject>> destinationType) {
        SomeObject someObject = new SomeObject();
        someObject.setStringSomeObject(source);
        return Arrays.asList(someObject);
    }

}

I wrote an unit-test to ensure that this works:

@Test
public void testMap() throws Exception {
    A a = new A();
    a.setStringA("a");

    B outcome = new MyMapper().map(a, B.class);

    assertThat(outcome.getSomeObjects.size(), is(1));
}

Sadly this test fails with:

java.lang.AssertionError: 
Expected: is <1>
   but: was <0>

It seems like the Converter is never executed so I tried to debug it. And indeed: The debugger never reaches the converter. Am I doing something wrong? It seems like. I know there are more methods which one could go with like: mapAToB e.g...

Ok I found a solut...nah! It's not a solution, it's just a workaround. I defined the stringA as List<String> as well and defined a converter extending CustomConverter<String, LoanContrReqERPCrteReqLoanContrBrrwrPty>.

Because this feels a little "hacky", I am still interested in a nice solution. (Though I am just thinking that this solution might be fine: Now the datastructure of both objects is more equal than before. The problem is, that object B is coming from an external service, I can't modify it.)

Upvotes: 1

Views: 4462

Answers (1)

Vlad Bochenin
Vlad Bochenin

Reputation: 3072

You mapping doesn't work because you don't have setter for someObjects.

When Orika tries to generate code for mapper, it checks all fieldMaps in classMap for sourceProperty is readable and destinationProperty is assignable. If this checks passed, generator puts field conversion into generated mapper. If check failed, Orika just skip this field conversion.

Few options you can use to solve a problem:

  • You can add setter for someObjects field in class B:

    public static class B {
    
        List<SomeObject> someObjects;
    
        public List<SomeObject> getSomeObjects() {
            if (someObjects == null) {
                someObjects = new ArrayList<SomeObject>();
            }
            return someObjects;
        }
    
        public void setSomeObjects(List<SomeObject> someObjects) {
            this.someObjects = someObjects;
        }
    }
    
  • Use custom mapper instead of converter:

        factory.classMap(A.class, B.class)
                .customize(
                        new CustomMapper<A, B>() {
                            @Override
                            public void mapAtoB(A a, B b, MappingContext context) {
                                SomeObject someObject = new SomeObject();
                                someObject.setStringSomeObject(a.getStringA());
                                b.getSomeObjects().add(someObject);
                            }
                        }
                )
                .byDefault()
                .register(); 
    

Orika will put invocation customMapper after resolving of field maps.
Generated mapper will looks like:

    b.setOtherField(a.getOtherField());
    if (customMapper != null) {
        customMapper.map(source, destination); <-- Your mapper invocation
    }
  • Use follow syntax for fields:

        factory.classMap(A.class, B.class)
                .field("stringA", "someObjects[0].stringSomeObject")
                .byDefault()
                .register();
    

Generated mapper will looks like:

    if (source.getStringA() != null) {
        if (((((java.util.List) destination.getSomeObjects()).size() <= 0 || ((List) destination.getSomeObjects()).get(0) == null))) {
            ((java.util.List) destination.getSomeObjects()).add(0, ((BoundMapperFacade) usedMapperFacades[0]).newObject(((String) source.getStringA()), mappingContext));
        }
    }

    if (!(((java.lang.String) source.getStringA()) == null)) {
        (((java.util.List) destination.getSomeObjects()).get(0)).setStringSomeObject(source.getStringA());
    } else if (!(((java.util.List) destination.getSomeObjects()) == null) && !((((java.util.List) destination.getSomeObjects()).size() <= 0 || ((List) destination.getSomeObjects()).get(0) == null))) {
        ( ((java.util.List) destination.getSomeObjects()).get(0)).setStringSomeObject(null);
    }

Also there was a bug in Orika to map from single property to property of collection using syntax .field("stringA", "elements{stringB}") ( Incorrect mapper code generated for mapping from a single property to property of collection element). Bug closed at 31 Dec 2016 here: Fix for bug

Upvotes: 2

Related Questions