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(...);