7 Replies Latest reply: May 11, 2010 7:19 PM by Javier Rodriguez RSS

Enhanced sort capabilities EntityQuery

Javier Rodriguez Newbie

I have derived a new class from EntriryQuery to obtain the following features:



  1. Allow sort one listing column by one or more entity properties. For each property, sorting direction can be defined independently ('EntityQuery' allows to define only one property to sort each listing column).

  2. Track previous listing orders so user selected ordering can take in account any previously selected one. This feature can be enabled or disabled programatically. For example: user clicks a column header to sort listing by its properties. Then she/he selects another column header. The listing will be sorted primary by last selected header properties, and as a second sort criteria, by the previously selected header properties. This behavior is obeyed for subsecuent header clicks up to a programatically configured deep.



  I have tryed to add this features seamless in seam-gen generated code. Changes that must be realized to seam-gen code are:


1) Change original 'sort.xhtml' to:


<ui:composition xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:c="http://java.sun.com/jstl/core"
    xmlns:s="http://jboss.com/products/seam/taglib">
    <s:link styleClass="columnHeader" 
         value="#{propertyLabel} #{entityList.orderColumns == propertyPath ? (entityList.orderDirection == 'desc' ? messages.down : messages.up) : ''}">
        <f:param name="sort" value="#{propertyPath}"/>
        <f:param name="dir" value="#{entityList.orderColumns == propertyPath and entityList.orderDirection == 'asc' ? 'desc' : 'asc'}"/>
    </s:link>
</ui:composition>



The only one change consists on renaming 'entityList.orderColum' to 'entityList.orderColumns'.


2) In 'xxxList.xhtml', configure for each listing column the property/properties that must be used in its sorting and the desired direction for each one using its 'propertyPath' param value. I have maintained the same param name to aviod unnecesary typing:


<rich:dataTable id="departmentList"
    var="_department"
    value="#{departmentList.resultList}"
    rendered="#{not empty departmentList.resultList}">

    <! Here, other listing columns -->

    <h:column>
        <f:facet name="header">
            <ui:include src="/layout/sort.xhtml">
                <ui:param name="entityList" value="#{departmentList}"/>
                <ui:param name="propertyLabel" value="Responsable"/>
                <ui:param name="propertyPath" value="department.manager.firstName ASC, department.manager.lastName ASC"/>
                </ui:include>
        </f:facet>
        <h:outputText value="#{_department.manager.firstName} #{_department.manager.lastName}" />
    </h:column>

    <!-- Here, other listing columns -->

</rich:dataTable>



Typed property directions dictates listing direction when column is sorted in ASC direction. For listings in DESC direction, my code will swap property directions form ASC to DESC or viceversa.


3) Change in 'xxxList.xhtml' 'sort' param value from

#{xxxList.orderColum}

to
#{xxxList.orderColumns}



 <param name="firstResult" value="#{departmentList.firstResult}"/>
 <param name="sort" value="#{departmentList.orderColumns}"/>     <!-- Change this -->
 <param name="dir" value="#{departmentList.orderDirection}"/>



4) If order tracking is desired for the listing, add to 'xxxList.page.xhtml' the following parameter:


 <param name="firstResult" value="#{departmentList.firstResult}"/>
 <param name="sort" value="#{departmentList.orderColumns}"/>
 <param name="dir" value="#{departmentList.orderDirection}"/>
 <param name="prevSort" value="#{departmentList.prevSanitizedOrderByParamsStr}"/>  <!-- Add this param -->



5) Derive 'xxxList' class from 'EnhancedShortEntityQuery' instead of 'EntityQuery'


public class DepartmentList extends EnhancedSortEntityQuery<Department> {
    //...
}



6) If listing order tracking is desired, configure programatically the desired behavior (for example in 'xxxList' constructor) by calling this inherited methods:


    setActivePreviousOrderTracking(true/false);
    setOrderTrackingDeep(int);




  • 'trackingDeep' defaults to no limit (0), so actual deep will depend on the number of listing properties used for its sorting. If listing declares nine entity properties for its sorting, maximun deep will be nine (when user has clicked in all listing column headers). This method limits the maximun number of entity properties (with its directions) the code will take in account (will remember) to determine current listing order. Note: this limits does not affect de number of properties selected to sort a single listing column. If the developer puts four properties in 'propertyPath' for one colum and tracking deep is set to three, the code will take in account all this four properties, but none of the previous listing order.




  • 'activePreviousOrderTracking' defaults to 'true' (feature active).



7) Of course, you must add the following class to your project (probably in your shared folder)


package com.<your_company>.<your_project>.shared;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Pattern;

import org.jboss.seam.framework.EntityQuery;

public class EnhancedSortEntityQuery<E> extends EntityQuery<E> {
     
    // Copied from 'Query' because has been declared private in that class !!
    private static final Pattern ORDER_COLUMN_PATTERN = Pattern.compile("^\\w+(\\.\\w+)*$");
    private static final String DIR_ASC = "asc";
    private static final String DIR_DESC = "desc";
     
    /*************************************************************************/
    /* EnhancedSortEntityQuery configuration params                          */
    /*************************************************************************/
     
    /** (De)/Activate listing order tracking feature */
    private boolean activePrevListingOrderTracking = true;  // Default: active

    /** @return true if listing order tracking feature is active */
    public boolean isActivePrevListingOrderTracking() {
        return activePrevListingOrderTracking;
    }

    /**
     * (De)/Activate listing order tracking feature
     * @param activePrevListingOrderTracking 'true' to activate order tracking
     */
    public void setActivePreviousOrderTracking(boolean activePrevListingOrderTracking) {
        this.activePrevListingOrderTracking = activePrevListingOrderTracking;
    }
     

    /** 
     * Establish maximun number of properties that must be remembered for order tracking.
     * Only meaningless when order tracking has been enabled.
     */
    private int orderTrackingDeep = 0;  // Default: no limit

    /** @return maximun number of properties remenbered for order tracking */
    public int getOrderTrackingDeep() { return orderTrackingDeep; }
    /**
     * Sets maximun number of properties remmembered for order tracking
     * Only meaninfull if order tracking has been enabled
     * @param orderTrackingDeep nof properties remembered for order tracking. 0 for no limit. Default 0
     */
    public void setOrderTrackingDeep(int orderTrackingDeep) {
        this.orderTrackingDeep = orderTrackingDeep;
    }

     
     
    /** 
     * Save the 'propertyPath' param set in 'xxxList.xhtml' in the columns header sections for the user clicked column.
     * Format: propertyPath [ASC/DESC][, propertyPath [ASC/DESC]].
     * If no direction (ASC/DESC) is included for a property, deafult ASC direction is used
     * Updated from 'sort.xhtml' with the contens of 'propertyPath' param set in 'xxxList.xhtml' for the user clicked column. 
     * Note: Must be updated with exactly the same string used in 'xxxList.xhtml' because 'sort.xhtml' test its equality to 
     * determine wich column that has been used to sort the listing and to apped the up or down arrow in its header label
     */
     
    // Example 'xxxList.xhtml' column definition code:
    // 
    //    <h:column>
    //           <f:facet name="header">
    //            <ui:include src="/layout/sort.xhtml">
    //                   <ui:param name="entityList" value="#{departmentList}"/>
    //                <ui:param name="propertyLabel" value="Manager"/>
    //                <ui:param name="propertyPath" value="department.manager.firstName, department.manager.lastName"/>
    //            </ui:include>
    //        </f:facet>
    //        <h:outputText value="#{_department.manager.firstName} #{_department.manager.lastName}"
    //    </h:column>

    private String orderColumns;
    /** Returns user clicked column header 'propertyPath' param */
    public String getOrderColumns() { return orderColumns; }
    /** Called from the view to set user clicked column header 'propertyPath' param */
    public void setOrderColumns(String orderColumns) { this.orderColumns = orderColumns; }
     
    /** Called from the view to establish order column direction */
    @Override
    public void setOrderDirection(String orderDirection) {
        super.setOrderDirection(orderDirection);
        setOrder(processQueryOrder());
    }
     
    // Copied from 'Query' because has been declared private in that class !!
    private String sanitizeOrderColumn(String columnName) {
        if (columnName == null || columnName.trim().length() == 0) {
            return null;
        } else if (ORDER_COLUMN_PATTERN.matcher(columnName).find()) {
            return columnName;
        } else {
            throw new IllegalArgumentException("invalid order column (\"" + columnName + "\" must match the regular expression \"" + ORDER_COLUMN_PATTERN + "\")");
        }
    }
     
    // Copied from 'Query' because has been declared private in that class !!
    private String sanitizeOrderDirection(String direction) {
        if (direction == null || direction.length()==0) {
            return null;
        } else if (direction.equalsIgnoreCase(DIR_ASC)) {
            return DIR_ASC;
        } else if (direction.equalsIgnoreCase(DIR_DESC)) {
            return DIR_DESC;
        } else {
            throw new IllegalArgumentException("invalid order direction");
        }
    }


    /** Sort Directions enumeration with its labels */
    private enum SortDirection {
        ASC(DIR_ASC),
        DESC(DIR_DESC);
        SortDirection(String label) { this.label = label; }
        private String label;
        public String getLabel() { return label; }
    }
     
    /** Auxiliary class */
    private class SortParam {
        public String propertyPath;
        public SortDirection dir;
        public SortParam(String column, SortDirection dir) {
            this.propertyPath = column; this.dir = dir;
        }
    };
     
    /** 
     * Parses 'sanitizedOrderByParamsStr' and fills an ArrayList with {propertyPath, "asc"/"desc"} pairs 
     * for each property used in listing sort.
     * @param sanitizedOrderByParamsStr Format:
     *     propertyPath "asc"/"desc", propertyPath "asc"/"desc", ...... propertyPath "asc"/"desc"
     * Ex: "department.manager.firstName asc, department.manager.lastName asc"
     * @return ArrayList initialized with the info extracted from 'sanitizedOrderByParamStr'
     */
    private List<SortParam> initSortParamsArray(String sanitizedOrderByParamsStr) {
        List<SortParam> sortParamsList = new ArrayList<SortParam>();
        StringTokenizer tokens = new StringTokenizer(sanitizedOrderByParamsStr, ",");
        String token;
        String propertyPath;
        String sortDirectionStr;
        while (tokens.hasMoreTokens()) {
            token = tokens.nextToken().trim();
            propertyPath = token.substring(0,token.lastIndexOf(' ')).trim(); 
            sortDirectionStr = token.substring(token.lastIndexOf(' ')).trim();
            sortParamsList.add(new SortParam(
                propertyPath, 
                sortDirectionStr.equals(DIR_ASC) ? SortDirection.ASC : SortDirection.DESC)
            );
        }
        return sortParamsList;
    }
     
    /**
     * Parses 'sanitizedOrderByParamsStr' and fills a Map with key='propertyPath', value="ASC"/"DESC". 
     * @param sanitizedOrderByParamsStr Format: 
     *     propertyPath "asc"/"desc", propertyPath "asc"/"desc", ...... propertyPath "asc"/"desc"
     * Ex: "department.manager.firstName asc, department.manager.lastName asc"
     * @return HashMap with keys an values extracted from 'sanitizedOrderByParamsStr'
     */
    private Map<String, SortDirection> initSortParamsMap(String sanitizedOrderByParamsStr) {
        Map<String, SortDirection> sortParamsMap = new HashMap<String, SortDirection>(0);
        StringTokenizer tokens = new StringTokenizer(sanitizedOrderByParamsStr, ",");
        String token;
        String propertyPath;
        String sortDirectionStr;
        while (tokens.hasMoreTokens()) {
            token = tokens.nextToken().trim();
            propertyPath = token.substring(0,token.lastIndexOf(' ')).trim(); 
            sortDirectionStr = token.substring(token.lastIndexOf(' ')).trim();
            sortParamsMap.put(
                propertyPath, 
                sortDirectionStr.equals(DIR_ASC) ? SortDirection.ASC : SortDirection.DESC
            );
        }
        return sortParamsMap;
    }
     
          
    /** Previous listing sanitized ORDER BY parameters string */
    private String prevSanitizedOrderByParamsStr = new String();
    public String getPrevSanitizedOrderByParamsStr() { return prevSanitizedOrderByParamsStr; }
    public void setPrevSanitizedOrderByParamsStr(String prevSanitizedOrderByParamsStr) {
        this.prevSanitizedOrderByParamsStr = prevSanitizedOrderByParamsStr;
    }

    private String processQueryOrder() {

        if (getOrderColumns() == null) return null;

        StringTokenizer tokens = new StringTokenizer(getOrderColumns(), ",");
        String propertyPath;
        String propertyDir;
        String token;
        String orderByParameters = new String();
 
        while (tokens.hasMoreTokens()) {
               
            token = tokens.nextToken().trim();
               
            int dirIndex = token.lastIndexOf(' ');  // Direction, if included, must be preceded by ' '
               
            if (dirIndex != -1) {  // Direction included in view
                propertyPath = sanitizeOrderColumn(token.substring(0,token.lastIndexOf(' ')));
                propertyDir = sanitizeOrderDirection(token.substring(token.lastIndexOf(' ')).trim());
                if (propertyDir == null) propertyDir = DIR_ASC;  // Por defecto, ascendente
            }
            else {  // Direction not included in view
                propertyPath = sanitizeOrderColumn(token);
                propertyDir = DIR_ASC;  // Default: ASC
            }
               
            if (getOrderDirection().equals(DIR_DESC)) {
                // Reverse property direction
                propertyDir = (propertyDir.equals(DIR_ASC)) ? DIR_DESC : DIR_ASC;
            }

            orderByParameters +=  propertyPath + " " + propertyDir;
               
            if (tokens.hasMoreTokens()) orderByParameters += ", ";
        }
          
        return orderByParameters;
    }
     
    /** Gets query results */
    @Override
    public List<E> getResultList() {

        // Force 'Query' to use 'order' instead of 'orderColumn' to process query ORDER BY clause parameters
        setOrderColumn("");
          
        String currentSanitizedOrderByParamsStr = getOrder();
           
        if (
            !isActivePrevListingOrderTracking() || 
            getOrder() == null ||
            prevSanitizedOrderByParamsStr.equals(getOrder())
        ) {
            return super.getResultList();
        }
          
        List<SortParam> prevSortParams = initSortParamsArray(prevSanitizedOrderByParamsStr);
        Map<String, SortDirection> currentSortParams = initSortParamsMap(currentSanitizedOrderByParamsStr);
          
        String prevPropertyPath;
        SortDirection prevSortDir;
        for (int i = 0; i < prevSortParams.size(); i++) {
            if ((getOrderTrackingDeep() != 0) && (currentSortParams.size() + i >= getOrderTrackingDeep())) break;
            prevPropertyPath = prevSortParams.get(i).propertyPath;
            prevSortDir = prevSortParams.get(i).dir;
            if (!currentSortParams.containsKey(prevPropertyPath)) {
                currentSanitizedOrderByParamsStr += ", " + prevPropertyPath + " " + prevSortDir.getLabel();
            }
        }
          
        setOrder(currentSanitizedOrderByParamsStr);
        prevSanitizedOrderByParamsStr = currentSanitizedOrderByParamsStr;
     
        return super.getResultList();
    }
}



  This class code would be writen more elegant integrated in 'EntityQuery', but i have prefered not touch Seam code, but borrow some private 'EntityQuery' code inside my 'EnhnacedSortEntityQuery'.




  • Compatibility:  This code has been tested in Seam 2.1.1 with JBoss AS 4.2.3. I have not check it against newest versions, but after a quick review of newest EntityQuery' an 'Query' source code I have not seen any reason it will not work properly. If anybody can test my code with it, please notify...




  • Performance:  I have tryed to avoid innecesary overhead. In my tests I have seen that performance impact is negligible compared with the time database query takes. If order tracking is disabled, performance would not be penalized. If enabled, the only one actual impact in performance is the extra time database manager needs to sort query results (but this is unavoidable if you needs to sort listing taking in account more database columns). With a database with about 1000 registers I have not been capable to measure any differences when taking in account up to ten properties, althought I think that with larger databases it will have more performance impact.



I hope this features will be useful for some Seam users. Please reply with any comment, bug, enhancement...