Criteria transfer object pattern

Use the criteria transfer object (CTO) pattern to construct client-side versions of persistent entity criteria instances and pass them through the chosen communication mechanism to remote server-side components, employing a CTO converter for seamless CTO-to-criteria transformation.

External resources

Overview

  • Project: daofusion-core
  • Package reference: com.anasoft.os.daofusion.cto

Introduction

Even though the criteria transfer object (CTO) pattern might sound like a buzzword, it's actually a concept that aims for application scenarios with loose coupling between the client and server. The primary goal here is to enable clients to construct serializable versions of persistent entity criteria instances (criteria transfer objects) and pass them through the chosen communication mechanism to remote components which can convert them into corresponding PersistentEntityCriteria seamlessly.

Using this concept, building complex data grids that use filtering, sorting and paging functionality can be accomplished in a very efficient and easy way, assuming that:

  • GUI changes within grid elements (filter value widgets, sorted column headers, paging widgets, etc.) will change the corresponding CTO accordingly
  • grid's data fetch operation will pass the actual CTO as an argument of a remote method call using the chosen communication mechanism, receiving requested results and populating the data grid thereafter

Besides standard Java based client-server application scenarios, the CTO pattern proves to be useful within the world of RIA (rich internet application) technologies such as Google Web Toolkit or Flex. In general, any client technology that allows direct or indirect modification of the Java based CTO instance can benefit from using the CTO pattern.

One important thing to note here is that the CTO pattern defines a client-side persistent entity criteria transfer object model along with its server-side processing (the "request" part of the client-server communication). CTO pattern does NOT define a "response" the server-side component should produce after retrieving results based on converted CTO instance - this is a task left to developers based on the transfer object strategy for the given project.

CTO class overview (client side)

CriteriaTransferObject represents a generic CTO class containing persistent entity criteria as well as paging constraints. CriteriaTransferObject acts as map-based container for FilterAndSortCriteria instances - the key is the propertyId of the given FilterAndSortCriteria. This means that all FilterAndSortCriteria instances must have unique propertyId values within the same transfer object.

FilterAndSortCriteria defines basic filter and sort criteria for a single property of the target persistent entity. This class is essentially an analogy of server-side FilterCriterion and SortCriterion classes, combined together in a simple client-side implementation. The propertyId mentioned above is a symbolic persistent entity property identifier which is resolved into the corresponding associationPath / targetPropertyName combination back on the server via the CTO converter (see the persistent entity criteria API for more information about association paths). FilterAndSortCriteria handles filter values as simple strings to avoid unnecessary client-side type conversions. Same as the propertyId, all filter values are converted into their typed object representations (integers, dates, etc.) during the CTO conversion process. Note that it is common to pass multiple filter values to a single FilterAndSortCriteria instance - a good example would be a date property of an entity which needs to fall between two boundary values.

In the client code, it's important to bind GUI changes within grid elements (filter value widgets, sorted column headers, paging widgets, etc.) to the associated CTO instance, for example:

public class CtoAwareDataGrid extends DefaultDataGrid {

    private final CriteriaTransferObject cto = new CriteriaTransferObject();

    public void onChangeValue(FilterWidget widget) {
        String propertyId = widget.getPropertyId();
        String value = widget.getValue();
        cto.get(propertyId).setFilterValue(value);
    }

}

These two simple classes conclude the client part of the CTO pattern.

CTO class overview (server side)

CriteriaTransferObjectConverter interface represents the general CTO converter contract defining conversion pattern between client-side CriteriaTransferObject instances and their corresponding server-side PersistentEntityCriteria counterparts:

public interface CriteriaTransferObjectConverter {

    /**
     * Converts the given CriteriaTransferObject instance into a corresponding
     * PersistentEntityCriteria according to property mappings defined by the
     * requested property mapping group.
     */
    PersistentEntityCriteria convert(CriteriaTransferObject transferObject,
            String mappingGroupName);

}

CriteriaTransferObjectConverter uses the notion of property mapping groups to decide which mapping rules to apply for which CTO instance.

NestedPropertyCriteriaBasedConverter is the default general purpose CriteriaTransferObjectConverter implementation that uses NestedPropertyCriteria as the server-side criteria implementation to be returned by the convert method. NestedPropertyCriteriaBasedConverter acts as map-based container for NestedPropertyMappingGroup instances, which in turn hold mappings (NestedPropertyMapping instances) for specific propertyId values. Note that there can be multiple property mappings for handling the same propertyId during CTO conversion. Each NestedPropertyMappingGroup holds a name that is unique within the CTO converter and represents a set of mappings for a particular persistent entity in a specific usage scenario.

Managing mapping groups as well as individual property mappings can be quite complex. NestedPropertyCriteriaBasedConverter tries to hide internal NestedPropertyMappingGroup processing away from its users via the addMapping method, which is the preferred way of configuring this CTO converter implementation:

/**
 * Adds the given NestedPropertyMapping to this converter by
 * associating it with the requested property mapping group.
 */
public void addMapping(String mappingGroupName, NestedPropertyMapping mapping);

As you might guess, a NestedPropertyMapping is pretty essential in terms of NestedPropertyCriteriaBasedConverter configuration. NestedPropertyMapping represents an abstract CTO mapping that supports a nested property of a persistent entity (just like the NestedPropertyCriterion described in the previous section). NestedPropertyMapping defines a general relation between CriteriaTransferObject and NestedPropertyCriteria for a single property of the target persistent entity using the propertyId concept as shown above. Each property mapping is responsible for updating the server-side criteria instance according to client-side FilterAndSortCriteria - this is achieved via the apply method that gets called for each NestedPropertyMapping within a NestedPropertyMappingGroup during the CTO conversion:

/**
 * Applies query constraints defined by the clientSideCriteria to the
 * serverSideCriteria according to the property mapping implementation.
 */
public abstract void apply(FilterAndSortCriteria clientSideCriteria,
        NestedPropertyCriteria serverSideCriteria);

FilterAndSortMapping is the default NestedPropertyMapping subclass that is intended for direct use in conjunction with NestedPropertyCriteriaBasedConverter. Presence of a custom FilterCriterionProvider implementation within the FilterAndSortMapping indicates the use of the filtering functionality (if not null, the given FilterCriterionProvider will be used to construct a FilterCriterion instance to be added to the server-side criteria). However, the FilterCriterion instance will be added to the server-side criteria only in case the FilterAndSortCriteria contains at least one filter value. See the persistent entity criteria API for more information about FilterCriterionProvider and related classes.

Filter values arrive from the client as simple strings, as declared by the FilterAndSortCriteria. In order to resolve such values into their typed object representations, a custom FilterValueConverter implementation needs to be provided as well (assuming the use of the filtering functionality):

public interface FilterValueConverter<T> {

    /**
     * Converts the given stringValue into
     * appropriate object representation.
     */
    T convert(String stringValue);

}

Note that all filter values converted via this interface will be passed as directValues to the underlying FilterCriterion instance - NestedPropertyCriteriaBasedConverter generally doesn't use the filter object concept since the structure and type of filter values within the FilterAndSortCriteria make it too generic for it to be used correctly (remember that the filter object is efficient in applications where GUI changes can be seamlessly propagated to the server).

As for the sorting functionality, no additional settings are necessary since the FilterAndSortCriteria contains all supported sort options by default. Note that the sorting functionality (underlying SortCriterion instance processing) can be turned off by setting FilterAndSortCriteria's sortAscending property to null.

Since the CriteriaTransferObject allows its users to define paging and sort criteria in addition to property filter constraints, converting such CTO instance directly into a NestedPropertyCriteria and passing it to count methods of your DAO classes might not work correctly for most databases. This is because AbstractHibernateEntityDao's row count technique implementation relies on Hibernate rowCount projection (AbstractHibernateEntityDao uses BaseHibernateDataAccessor's rowCount method under the hood for this purpose). The bottom line is that the target Criteria instance shouldn't contain any paging constraints since the rowCount method relies on a result set with its "shape" defined by the projection itself. The rowCount projection essentially results in SELECT COUNT(*) FROM ... SQL statement and we expect to receive exactly one integer value as its result (the shape of our projection). Adding paging constraints to such Criteria instance doesn't make much sense for projections since it restricts their elements (which is not what we want in general).

So, how to use CTO instances safely within the context of standard count methods? The answer is CriteriaTransferObjectCountWrapper - a simple server-side CTO wrapper designed for entity instance count purposes. CriteriaTransferObjectCountWrapper takes the original CTO instance as its argument and returns a new CTO which delegates most of its methods to the wrapped CTO instance, with the exception of paging and sort constraints and methods that modify internal state of the transfer object:

PersistentEntityCriteria countCriteria = converter.convert(
        new CriteriaTransferObjectCountWrapper(transferObject).wrap(),
        myMappingGroup);

int totalRecords = myDao.count(countCriteria);

Sample CTO converter

Let's build a sample CTO converter that demonstrates basic property mapping configuration. One way of doing this is to extend the NestedPropertyCriteriaBasedConverter class and provide property mappings within the implementation itself:

public final class CtoFilterCriterionProviders {

    // NestedPropertyCriteriaBasedConverter uses direct filter value approach under the hood
    private static final FilterDataStrategy STRATEGY = FilterDataStrategy.DIRECT;

    public static final FilterCriterionProvider LIKE = new SimpleFilterCriterionProvider(STRATEGY, 1) {
        public Criterion getCriterion(String targetPropertyName,
                Object[] filterObjectValues, Object[] directValues) {
            return Restrictions.like(targetPropertyName, directValues[0]);
        }
    };

    public static final FilterCriterionProvider EQ = new SimpleFilterCriterionProvider(STRATEGY, 1) {
        public Criterion getCriterion(String targetPropertyName,
                Object[] filterObjectValues, Object[] directValues) {
            return Restrictions.eq(targetPropertyName, directValues[0]);
        }
    };

    public static final FilterCriterionProvider BETWEEN = new SimpleFilterCriterionProvider(STRATEGY, 2) {
        public Criterion getCriterion(String targetPropertyName,
                Object[] filterObjectValues, Object[] directValues) {
            return Restrictions.between(targetPropertyName, directValues[0], directValues[1]);
        }
    };

}

public class SampleConverter extends NestedPropertyCriteriaBasedConverter {

    public static final String MAPPING_GROUP_CUSTOMER = "customer";

    // Customer - name
    public static final String CUSTOMER_NAME_ID = "name";
    public static final AssociationPath CUSTOMER_NAME_APATH = AssociationPath.ROOT;
    public static final String CUSTOMER_NAME_TARGET = "name";

    // Customer - userProfile - favoriteNumber
    public static final String CUSTOMER_FAVNO_ID = "favNo";
    public static final AssociationPath CUSTOMER_FAVNO_APATH = new AssociationPath(
            new AssociationPathElement("userProfile"));
    public static final String CUSTOMER_FAVNO_TARGET = "favoriteNumber";

    // Customer - accountCreated
    public static final String CUSTOMER_JOINDATE_ID = "joinDate";
    public static final AssociationPath CUSTOMER_JOINDATE_APATH = AssociationPath.ROOT;
    public static final String CUSTOMER_JOINDATE_TARGET = "accountCreated";

    public static final DateConverter DATE_CONVERTER = new DateConverter("yyyy.MM.dd HH:mm");

    public void initMappings() {
        addStringMapping(MAPPING_GROUP_CUSTOMER, CUSTOMER_NAME_ID,
                CUSTOMER_NAME_APATH, CUSTOMER_NAME_TARGET);

        addIntegerMapping(MAPPING_GROUP_CUSTOMER, CUSTOMER_FAVNO_ID,
                CUSTOMER_FAVNO_APATH, CUSTOMER_FAVNO_TARGET);

        addDateMapping(MAPPING_GROUP_CUSTOMER, CUSTOMER_JOINDATE_ID,
                CUSTOMER_JOINDATE_APATH, CUSTOMER_JOINDATE_TARGET);
    }

    private void addStringMapping(String mappingGroupName, String propertyId,
            AssociationPath associationPath, String targetPropertyName) {
        addMapping(mappingGroupName, new FilterAndSortMapping<String>(
                propertyId, associationPath, targetPropertyName,
                CtoFilterCriterionProviders.LIKE, FilterValueConverters.STRING));
    }

    private void addIntegerMapping(String mappingGroupName, String propertyId,
            AssociationPath associationPath, String targetPropertyName) {
        addMapping(mappingGroupName, new FilterAndSortMapping<Integer>(
                propertyId, associationPath, targetPropertyName,
                CtoFilterCriterionProviders.EQ, FilterValueConverters.INTEGER));
    }

    private void addDateMapping(String mappingGroupName, String propertyId,
            AssociationPath associationPath, String targetPropertyName) {
        addMapping(mappingGroupName, new FilterAndSortMapping<Date>(
                propertyId, associationPath, targetPropertyName,
                CtoFilterCriterionProviders.BETWEEN, DATE_CONVERTER));
    }

}

The SampleConverter contains a single mapping group that defines some property mappings for the fictional Customer entity (name, favorite number and join date). Things get usually far more complex than this, but basically this is the way how you would typically configure the NestedPropertyCriteriaBasedConverter instance. Note that the enabled method of the FilterCriterionProvider interface could be used for disabling filtering within the given FilterAndSortMapping (just like in the sample OrderDaoImpl from the previous section). Additionally, our SampleConverter uses the FilterValueConverters utility class for standard value converter implementations (string, integer and date).