Programming Thoughts
J2EE - Miscellaneous
Transfer Object Assembler

Requirements for Transfer Object assembly can become complex

In an J2EE, microservice architecture, remote clients cannot directly access entities and must request session beans assemble complex object graphs. This is known as the Transfer Object Assembler design pattern. Different UI pages have different use cases, so they request different assemblies of the same record. A function for each bloats the session bean interface and it can become unclear what is assembled, especially if comments are missing. Worse, this tightly couples client with server and a new or changed client use case requires rebuilding and redeploying both. Redeploying should be minimised for critical servers.

Redesigning the interface functions

Letting clients specify what they need assembled loosens coupling. Alas, there can be so many options, a function parameter for each quickly bloats the function definition, making it hard to read. A custom class encompassing all options and passed as a function parameter means the client sets only the desired ones using descriptive names. This makes more readable code and permits sophisticated options but client code spans multiple lines.

For the sake of better readability, a different technique using enumerated values in variables arguments is preferred. An example function definition and client is shown below, where the client can specify related records and data.

/** * Options when retrieving webcam QA report. * <DL> * <DT>FILES</DT> * <DD>Includes webcam report file records.</DD> * <DT>FILE_DATA</DT> * <DD>Includes file contents attached to each webcam report file record. FILES must be set.</DD> * <DT>FILE_MIME_TYPE</DT> * <DD>Includes Global MIME type record attached to each webcam report file record. FILES must be set.</DD> * <DT>NOTES</DT> * <DD>Includes webcam report note records.</DD> * <DT>STAFF</DT> * <DD>Includes host record.</DD> * </DL> */ public enum WebQAReportOption {ALL, FILE_DATA, FILE_MIME_TYPE, FILES, NOTES, STAFF} /** * Returns webcam report, with any additional records, or null if not found. */ public WebQAReportDTO findWebQAReport(int id, WebQAReportOption... options); result = webQAModel.findWebQAReport(key, WebQAReportOption.FILES, WebQAReportOption.FILE_MIME_TYPE, WebQAReportOption.NOTES);

Checking options parameter

The options parameter is an array, which is awkward to check if a value is present. The obvious solution is to convert it into an EnumSet but a custom helper class can look for a special 'ALL' value. Credit goes to my colleague, Jonah Rees, for this class.

/** * Provides support for enums when used as option parameters for the transformation * of vos into entities. If the enum has an 'ALL' value then it will be used to select all options. * @param O An enum type used for options */ public class OptionEnum<O extends Enum<O>> { private EnumSet<O> optionSet; private Class<O> optionClass; // No zero-arg constructor is available, it is necessary to specific the optionClass private OptionEnum() {super();} /** * Constructs this Options with a default empty set; isRequested(Option) will * always return false. * Use setOptions to add the options to this class. */ public OptionEnum(Class<O> cls) { this.optionClass = cls; } /** * Add one or many options to this class. If the options parameter contains an * option ALL then all the options will be used and contains() will always return true. * @param options if null then isRequested(Option) will always return false. */ public void setOptions(O...options) { if (options == null || options.length == 0) optionSet = EnumSet.noneOf(optionClass); else if (containsAll(options)) optionSet = EnumSet.allOf(optionClass); else { List<O> list = Arrays.asList(options); optionSet = EnumSet.copyOf(list); } } /** * Tests if the passed in option has been requested. * @return True if the passed in option is in the internal set. */ public boolean isRequested(O option) { if (optionSet == null) return false; return optionSet.contains(option); } /** Test if there is an 'ALL' option for the enum in the array */ private boolean containsAll(O[] oo) { for (O o : oo) { if (o.name().equals("ALL")) return true; } return false; } @Override public String toString() { return "OptionEnum [optionSet=" + optionSet + ", optionClass=" + optionClass + "]"; } }

This is used in the assembler like the following.

OptionEnum<WebQAReportOption> myOptions = new OptionEnum<WebQAReportOption>(WebQAReportOption.class); myOptions.setOptions(options); if (myOptions.isRequested(WebQAReportOption.FILES)) { ...

Assembling collections

Rather than replicating option processing code, collections can obviously be assembled for each item of the collection. This becomes inefficient when processing a single record option is slow, such as calling a remote server, but retrieving data in a batch can help.

This leads to a Template method invoking supplied Transformers. This code was originally written by Jonah Rees.

/** * Transforms a collection of entities into a list of values using the transformer's transform method. * @param entities A collection of any objects for which a transformer has been written * @param transformer An entity transformer to transform E into V * @return The list of values, will not be null but may be empty. The ordering is not guaranteed * @see EntityTransformer#transform(Object) */ public static <E,V> List<V> transform(Collection<E> entities, EntityTransformer<E,V> transformer) { transformer.startMultiple(entities); try { if (entities == null || entities.isEmpty()) return new ArrayList<V>(); List<V> values = new ArrayList<V>(); for (E e : entities) { V record = transformer.transform(e); if (record != null) { values.add(record); } } return values; } finally { transformer.endMultiple(entities); } } /** * Transforms an entity into a Data Transfer Object using the transformer's transform method. * @param entity Any object for which a transformer has been written * @param transformer An entity transformer to transform E into V * @return A Data Transfer Object which will be null if the entity param is null * @see EntityTransformer#transform(Object) * @throws IllegalStateException if the transformation fails for any reason. */ public static <E,V> V transform(E entity, EntityTransformer<E, V> transformer) { transformer.startSingle(entity); try { if (entity == null) return null; return transformer.transform(entity); } finally { transformer.endSingle(entity); } } /** * A Transformer to transform entities into Data Transfer Object and used by all versions of {@link EntityUtil#transform}. * Implementors map the entity fields to the value fields. * * @param <E> Entity * @param <V> Data Transfer Object */ public interface EntityTransformer<E,V> { /** * Returns a Data Transfer Object derived from Entity. Not called if entity is null. Can return null to skip value * from any Data Transfer Object collection. */ V transform(E entity); /** * Notification that {@link #transform} is about to be called for a single entity that's not part of a collection. * entity can be null. */ default void startSingle(E entity) {} /** * Notification that {@link #transform} is about to be called only for multiple entities that are part of a * collection. entities can be null. */ default void startMultiple(Collection<E> entities) {} /** * Notification that calls to {@link #transform} for a single entity that's not part of a collection have completed. * entity can be null. */ default void endSingle(E entity) {} /** * Notification that calls to {@link #transform} for multiple entities that are part of a collection have completed. * entities can be null. */ default void endMultiple(Collection<E> entities) {} }

Example

The following example transformer, if client code requests it, loads all needed services from a slow, remote server before assembly.

public class CategoryTransformer implements EntityTransformer<Category,CategoryDTO> { private final OptionEnum<CategoryOption> options = new OptionEnum<CategoryOption>(CategoryOption.class); private Map<Integer,ServiceDTO> serviceLookup; public CategoryTransformer(CategoryOption... options) { this.options.setOptions(options); this.serviceLookup = null; } @Override public void startMultiple(Collection<Category> entities) { ServiceManagerModel serviceManagerModel; Collection<ServiceDTO> services; Set<Integer> serviceIds; if (options.isRequested(CategoryOption.SERVICE)) { serviceIds = entities.stream().map(e -> e.getId()).collect(Collectors.toSet()); serviceManagerModel = ServiceLocatorEJB3.getInstance().getInterfaceUnchecked(ServiceManagerModel.class, ServiceManagerModel.JNDI_BINDING_NAME); services = serviceManagerModel.findServicesByIds(serviceIds); serviceLookup = services.stream().collect(Collectors.toMap(s -> s.getId(), s -> s)); } } @Override public void startSingle(Category entity) { ServiceManagerModel serviceManagerModel; ServiceDTO service; if (options.isRequested(CategoryOption.SERVICE)) { serviceManagerModel = ServiceLocatorEJB3.getInstance().getInterfaceUnchecked(ServiceManagerModel.class, ServiceManagerModel.JNDI_BINDING_NAME); service = serviceManagerModel.findService(entity.getServiceId()); serviceLookup.put(entity.getServiceId(), service); } } @Override public CategoryDTO transform(Category entity) { CategoryDTO result; ServiceManagerModel serviceManagerModel; if (entity != null) { result = entity.getValueObject(); if (options.isRequested(CategoryOption.SERVICE)) { result.setService(serviceLookup.get(entity.getServiceId())); } } else { result = null; } return result; } }

Limitations and other considerations

The difficulty with this design is anticipating what options future clients may or may not need. Typically, they're related records except those forming a one-to-many collection large enough to need a separate page, such as a customer's orders.

Transformers can filter records and options can also control this. For example, product queries should normally ignore those no longer supported but display of historical records may need to include unsupported products. Transformers can use code like

if (options.contains(ProductOption.DELETED) || !entity.isDeleted()) {