Custom Sorting

When lists of values are displayed in an application page, a user can sort the list by clicking on the column headers. The sort order of the rows will be determined by the sort order of the values in the selected column. Successive clicks on a column header alternate between the forward and reverse sort order for that column. The sort order for any type of data can be customized easily, though the sort-order for code-table codes must be controlled using the code-table administration interface. The sort order is calculated when responding to a user's request, so the user's active locale is available by calling the inherited getLocale method and can be used to influence the sort order in a locale-specific manner.

The domain comparator plug-ins are responsible for making the comparisons that control the sort order. The sorting algorithms swap the position of values in their value lists depending on the value returned by the compare method of the plug-in. The comparator plug-ins used in the Cúram application behave as described in Comparator Plug-ins. These sort orders are simple and intuitive, but may not meet the needs of some specialized domains. In these cases, custom sort orders may be required and there is no limitation on what order can be used.

What Values are Compared?: All compare operations are performed by invoking the comparator plug-ins compare method. This takes two java.lang.Object arguments. The method is invoked automatically by the client infrastructure before the values are formatted. This means that the objects passed are of the types shown in Java Object Representations, not formatted string representations of the values.

In most cases, having access to Java object representations makes the comparisons much easier to perform: comparing dates and numbers is much easier when they are represented by objects that conveniently provide a compareTo method that returns the same values that the compare method must return. However, there are some situations where, for example, encoded strings are decoded by the format operation and comparing them before they are formatted is not simple or would involve the duplication of the formatting code. In these cases, it is possible to invoke the appropriate formatter and compare the results. This will be described later.

The general guidelines for implementing a custom comparator plug-in to control the sort order for a domain are as follows:

  1. Create a new sub-class of the AbstractComparator class described in Extending Existing Plug-ins.
  2. Implement the compare method to perform your custom comparison.
  3. Configure your new plug-in for the relevant domains.

To illustrate this, you will see how to write a comparator that compares string values as if they were numbers. Some of the entities in the Cúram application use a string-based domain for their key values to support the use of identifiers that may not just contain digits. Sorting of these types works well in most cases, but there can be problems. Because the base domain is a string, the values are sorted lexicographically, not numerically. If the values are all of the same length, this is not a problem, but if the lengths differ, the sorting becomes confusing. For example, the string values "22" and "33" will be sorted into the order "22", "33", but if the values are "22" and "3", the sort order will be "22", "3", because the character "2" comes before the character "3" in a lexicographical sort and representations of numbers with positional digits are not recognized.

There are a number of ways to solve this problem:

The latter solution is, perhaps, the easiest to achieve. Here is an example of a custom comparator plug-in that does this for values that are limited to no more than ten characters:

Figure 1. Sorting Strings Numerically
/**
 * Compares string values after padding them with leading
 * zeros to make the sorting work correctly for numeric
 * values. Values must not be longer than ten characters.
 */
public class IDComparator
       extends AbstractComparator {
  public int compare(Object s1, Object s2) {
    return _pad((String) s1).compareTo(_pad((String) s2));
  }

  private String _pad(String s) {
    return "0000000000".substring(0, 10 - s.length()) + s;
  }
}

The _pad method pads a value with leading zeros, so that all returned strings will be ten characters long and numeric values will be compared correctly as the positional digits will all be aligned correctly. No change needs to be made to the format or parse operations or to any existing values in the database; the sort order is entirely controlled by this simple comparator code. While the numeric values could have been parsed from the strings and a numeric comparison made, this sample code is much simpler and more efficient.

Another need for custom sorting arises when values are in an encoded form that is decoded by the format operation. In this case, sorting of the encoded form may not be meaningful. For example, if a domain exists that uses an encoded string containing several localized messages and their locale codes like this "en|Hello|es|Hola", calculating the sort orders for such strings is meaningless. The string could be decoded, but, as decoding must be done by the format operation, it is simpler to invoke the format operation instead and compare the values that it returns.

Figure 2. Sorting Formatted Values
/**
 * Compares two encoded message strings using their
 * formatted values.
 */
public class MessageComparator
       extends AbstractComparator {
  public int compare(Object value1, Object value2) {
    final DomainConverter converter;

    try {
      converter = ((ClientDomain) getDomain())
          .getConverter(getLocale());
      return converter.format(value1)
          .compareTo(converter.format(value2));
    } catch (Exception e) {
      // Do nothing except report the values to be equal.
      return 0;
    }
  }
}

This code retrieves the converter plug-in that implements the format operation for the same domain as that of the values being compared. The returned converter will also be aware of the active user's locale. The exact mechanism behind this is unimportant, simply copying the code above is all that is needed. Other uses of the ClientDomain class are not supported. The exception handling is simple: it does nothing. The compare method is not declared to throw exceptions, and thrown run-time exceptions trigger an application error page, so there is not much useful error handling that can be performed. The reason that none is attempted at all is that if the converter cannot be retrieved or the format operation fails, it will be for reasons beyond the control of the comparator plug-in and these reasons will cause failures in other places that will be reported in time. In fact, the sorting operation is carried out just before the infrastructure formats all of the values ready for display, so the very next operation will detect and report the errors that may have been ignored by the comparator.

A final example shows how to make the Cúram application zero date (January 1,0001), appear after all other dates instead of before them:

Figure 3. Sorting Zero Dates
/**
 * Compares dates, but places the zero date at the end,
 * rather than the start, or the sort order.
 */
public class ZeroDateComparator
       extends AbstractComparator {
  public int compare(Object value1, Object value2) {
    final Date date1 = (Date) value1;
    final Date date2 = (Date) value2;

    if (Date.kZeroDate.equals(date1)
        && !Date.kZeroDate.equals(date2)) {
      return -1;
    } else if (!Date.kZeroDate.equals(date1)
               && Date.kZeroDate.equals(date2)) {
      return 1;
    }
    return date1.compareTo(date2);
  }
}

The comparator returns a negative number (the magnitude is not important) if the first date is the zero date and the second date is not the zero date to indicate that the first date comes after the second in the sort order. Likewise, a positive number is returned if the first date is not the zero date and the second date is the zero date to indicate that the order is correct. Otherwise, the dates are compared as normal. This causes the zero date to be positioned after all other dates instead of before them in the sort order.

This type of manipulation should be used with caution: the comparator plug-ins are also used during pre-validation to check a value against the maximum and minimum values defined for its domain in the UML model's domain definition options. In this case, if the UML domain definition options define a maximum date and no date is set, then the zero date will be assumed and this will appear to be later than all other dates, including the maximum date, and the pre-validation check will always fail with an error. If no maximum value is specified in the model, then this comparator will work without problems.

To override the default comparator for all dates with this new comparator, the configuration will look like this:

Figure 4. Configuration for Custom Sorting
<dc:domains xmlns:dc="http://www.curamsoftware.com/curam/util/common/domain-config">
  <dc:domain name="SVR_DATE">
    <dc:plug-in name="comparator"
                class="custom.ZeroDateComparator"/>
  </dc:domain>
</dc:domains>

Now, all date values for all domains that are descendants of the root SVR_DATE domain, and values in the root domain itself, will be sorted according to the new rules. There is no need to configure any other domains, as they will all inherit this new comparator (unless, of course, a descendant domain has been configured with another comparator that will override any inherited comparator). This comparator could also be applied more selectively to descendant domains of SVR_DATE.