CDI Mapper: get rid of the proxy layer!


In a recent post (https://rmannibucau.wordpress.com/2015/12/01/write-your-own-cdi-extension-for-bean-mapping/) I explained how to implement a simple mapper with CDI integration. We can actually make it simpler leveraging on CDI a bit more.

The @FieldMapping* annotations will stay the same:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface FieldMappings {
    @Nonbinding
    FieldMapping[] value() default {};
}

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface FieldMappings {
    @Nonbinding
    FieldMapping[] value() default {};
}

but we will introduce a Mapper interface:

@FunctionalInterface
public interface Mapper<FROM, TO> {
    TO map(FROM from);

    default Collection<TO> mapCollection(Collection<FROM> from) {
        return from.stream().map(this::map).collect(toList());
    }
}

Note: see how easy it is to handle collection relying on java8 default methods and stream API!

So all our API is ready. Before seeing how to implement it let check how to use it:

@Inject
@FieldMapping(target = "id")
@FieldMapping(target = "name")
@FieldMapping(source = "streetName", target = "street")
private Mapper<User, Person> personMapper;

Simple isn’t it? We don’t need to define a particular instance or anything, just define the implementation constraints where you need it, ie on the injection point.

Now we know what we want we just have to implement it, the implementation itself is pretty much the same as in the previous article excepted it uses a CDI @Produces method which makes it really easy to do and avoid a lot of CDI extension wiring. We also leverage the @FunctionalInterface to implement it inline but this is fully optional and a static MapperImpl would avoid to keep the reference to the contextual variable:

@ApplicationScoped
public class MapperProducer {
    @Produces
    @FieldMappings
    public Mapper produceMapper(final InjectionPoint injectionPoint) {
        final Type type = injectionPoint.getType();
        if (!ParameterizedType.class.isInstance(type)) {
            throw new IllegalArgumentException("Only use @FieldMappings with Mapper<A, B> types");
        }
        final ParameterizedType pt = ParameterizedType.class.cast(type);
        if (!Class.class.isInstance(pt.getActualTypeArguments()[0]) || !Class.class.isInstance(pt.getActualTypeArguments()[0])) {
            throw new IllegalArgumentException("Only Class are supported as generic parameter in Mapper<A, B>");
        }

        final Class<?> from = Class.class.cast(pt.getActualTypeArguments()[0]);
        final Class<?> to = Class.class.cast(pt.getActualTypeArguments()[1]);

        final Map<Field, Field> fields = Stream.of(injectionPoint.getAnnotated().getAnnotation(FieldMappings.class).value())
            .map(m -> new ImmutablePair<>( // capture the field
                FieldUtils.getField(from, m.source().isEmpty() ? m.target() : m.source(), true),
                FieldUtils.getField(to, m.target(), true)))
            .collect(toMap(Pair::getKey, Pair::getValue));

        return input -> {
            try {
                final Object out = to.newInstance();
                fields.entrySet().forEach(entry -> {
                    try {
                        entry.getValue().set(out, entry.getKey().get(input));
                    } catch (final IllegalAccessException e) {
                        throw new IllegalArgumentException(e);
                    }
                });
                return out;
            } catch (final IllegalAccessException | InstantiationException e) {
                throw new IllegalStateException(e);
            }
        };
    }
}

Note: as previous implementation type coercing etc can be enhanced but it shows you that CDI offers a great way to make nice and new API keeping the implementation simple!

Now you can just map you beans (JPA to JAXRS model for instance) as easy as:

MyModel jaxrsModel = mapper.map(jpaModel);
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s