Home > OS >  Java bean mapper expected capture but is provided object
Java bean mapper expected capture but is provided object

Time:06-17

Please note: even though I mention Dozer in this question, I do believe its really just a pure Java generics question at heart. There may be a Dozer-specific solution out there, but I think anyone with strong working knowledge of Java (11) generics/captures/erasures should be able to help me out!


Java 11 and Dozer here. Dozer is great for applying default bean mapping rules to field names, but anytime you have specialized, custom mapping logic you need to implement a Dozer CustomConverter and register it. That would be great, except the Dozer API for CustomConverter isn't genericized, is monolithic and leads to nasty code like this:

public class MyMonolithicConverter implements CustomConverter {

    @Override
    public Object convert(Object destination, Object source, Class<?> destinationClass, Class<?> sourceClass) {

        if (sourceClass.isAssignableFrom(Widget.class)) {
          Widget widget = (Widget)source;
          if (destinationClass.isAssignableFrom(Fizz.class)) {
            Fizz fizz = (Fizz)destination;
            // write code for mapping widget -> fizz here
          } else if (destinationClass.isAssignableFrom(Foo.class)) {
            // write code for mapping widget -> foo here
          }
          ... etc.
        } else if (sourceClass.isAssignableFrom(Baz.class)) {
          // write all the if-else-ifs and mappings for baz -> ??? here
        }

    }
}

So again: monolithic, not genericized and leads to large, complex nested if-else-if blocks. Eek.

I'm trying to make this a wee bit more palatable:

public abstract class BeanMapper<SOURCE,TARGET> {

    private Class<SOURCE> sourceClass;
    private Class<TARGET> targetClass;

    public abstract TARGET map(SOURCE source);

    public boolean matches(Class<?> otherSourceClass, Class<?> otherTargetClass) {
        return sourceClass.equals(otherSourceClass) && targetClass.equals(otherTargetClass);
    }

}

Then, an example of it in action:

public class SignUpRequestToAccountMapper extends BeanMapper<SignUpRequest, Account> {

    private PlaintextEncrypter encrypter;

    public SignUpRequestToAccountMapper(PlaintextEncrypter encrypter) {
        this.encrypter = encrypter;
    }

    @Override
    public Account map(SignUpRequest signUpRequest) {

        return Account.builder()
            .username(signUpRequest.getRequestedName())
            .email(signUpRequest.getEmailAddr())
            .givenName(signUpRequest.getFirstName())
            .surname(signUpRequest.getLastName()())
            .dob(DateUtils.toDate(signUpRequest.getBirthDate()))
            .passwordEnc(encrypter.saltPepperAndEncrypt(signUpRequest.getPasswordPlaintext()))
            .build();

    }
}

And now a way to invoke the correct source -> target mapper from inside my Dozer converter:

public class DozerConverter implements CustomConverter {

    private Set<BeanMapper> beanMappers;

    @Override
    public Object convert(Object destination, Object source, Class<?> destinationClass, Class<?> sourceClass) {

        BeanMapper<?,?> mapper = beanMappers.stream()
            .filter(beanMapper -> beanMapper.matches(sourceClass, destinationClass))
            .findFirst()
            .orElseThrow();

        // compiler error here:
        return mapper.map(source);

    }
}

I really like this design/API approach, however I get a compiler error on that mapper.map(source) line at the very end:

"Required type: capture of ?; Provided: Object"

What can I do to fix this compiler error? I'm not married to this API/approach, but I do like the simplicity it adds over the MyMonolithicConverter example above, which is the approach Dozer sort of forces on you. It is important to note that I am using Dozer elsewhere for simple bean mappings so I would prefer to use a CustomConverter impl and leverage Dozer for this instead of bringing in a whole other dependency/library for these custom/complex mappings. If Dozer offers a different solution I might be happy with that as well. Otherwise I just need to fix this capture issue. Thanks for any help here!

CodePudding user response:

The issue seems to come from the beanMappers. You have a set of mappers of various types. The compiler cannot infer what types the found mapper will have.

You can make the compiler believe you by casting the result and suppress the warning it gives you.

Casting to a <?,?> isn't going to happen, so I've added symbols for the convert method. At least it can then be assumed that when you get a BeanMapper<S,T>, map will indeed return a T upon an S source.

class DozerConverter {

  private Set<BeanMapper<Object,Object>> beanMappers;

  public <S,T> T convert(S source,
                         Class<?> destinationClass, 
                         Class<?> sourceClass) {

    @SuppressWarnings("unchecked")
    BeanMapper<S,T> mapper = (BeanMapper<S,T>) beanMappers.stream()
        .filter(beanMapper -> beanMapper.matches(sourceClass, destinationClass))
        .findFirst()
        .orElseThrow();

    return mapper.map(source);
  }

}

I'm afraid you're going to have to call it like so:

TARGET-TYPE target = dozerConverter.<SOURCE-TYPE,TARGET-TYPE>convert(...);
  • Related