Samstag, 12. Dezember 2009

Spring context configured JSF tables

Icefaces

This time I had the demand to create dynamic JSF tables with icefaces. The disadvantage of the usual static way like the following is that you have barley access on the columns in the backing bean and much rendundant xhtml code for each table.

<ice:dataTable value="#{viewUsersBean.users}"
               var="item"
               rows="10">
    <ice:column>
        <f:facet name="header">
            <ice:outputText value="#{msgs['user.firstName']}"/>
        </f:facet>
        <ice:outputText value="#{item.firstName}"/>
    </ice:column>
    <ice:column>
        <f:facet name="header">
            <ice:outputText value="#{msgs['user.lastName']}"/>
        </f:facet>
        <ice:outputText value="#{item.lastName}"/>
    </ice:column>
</ice:dataTable>

A cool approach is to make a xhtml dataTable template and configure the columns in the spring context. We also need a backing table data model to access the attributes in a generic way (see TableModel#getCellValue).

viewUsers.xhtml:
<ice:form partialSubmit="true">
    <ui:decorate template="/templates/dataTableTemplate.xhtml">
        <ui:param name="tableModel" value="#{viewUsersBean.tableModel}"/>
        <ui:param name="items" value="#{viewUsersBean.users}"/>
    </ui:decorate>
</ice:form>

dataTableTemplate.xhtml:
<ice:dataTable value="#{items}"
    var="item"
     rows="10">
    <ice:columns value="#{tableModel.columnModel}" 
        var="columnConfig">
        <f:facet name="header">
            <ice:panelGroup>
                <ice:commandSortHeader columnName="#{columnConfig.field}" >
                    <ice:outputText value="#{columnConfig.label}"/>
                </ice:commandSortHeader>
            </ice:panelGroup>
        </f:facet>
        <ice:outputText value="#{tableModel.cellValue}"/>
    </ice:columns>
</ice:dataTable>

web-context.xml:
<bean id="viewUsersBean" class="com.bamboit.webapp.controller.ViewUsersBean" scope="request">
    <constructor-arg index="0" value="com.bamboit.model.User"/><constructor-arg index="1">
        <list>
            <bean class="com.bamboit.model.ColumnConfig">
                <property name="field" value="firstName"/>
                <property name="label" value="user.firstName"/>
            </bean>
            <bean class="com.bamboit.model.ColumnConfig">
                <property name="field" value="lastName"/>
                <property name="label" value="user.lastName"/>
            </bean>
        </list>
    </constructor-arg>
</bean>

public class ViewUsersBean {
    private Class entityClass;
    private TableModel tableModel;
    private UserManager userManager;

    public ViewUsersBean(Class entityClass, List<ColumnConfig> columnConfig) {
        this.entityClass = entityClass;
        this.tableModel = new TableModel( columnConfig );
    }

    public void init() {
        tableModel.setItems( userManager.getAll() );
    }
    
    public TableModel getTableModel() {
        return tableModel;
    }
    
    public void setUserManager( UserManager userManager ) {
        this.userManager = userManager;
    }
}

public class TableModel<t>() {
    private List< T > items;
    private int rowIndex;
    private List<ColumnConfig> columnConfig;
    private DataModel columnModel;

    public TableModel(List<ColumnConfig> columnConfig) {
        this.columnConfig = columnConfig;
        this.columnModel = new ListDataModel( columnConfig );
    }

    public DataModel getColumnModel() {
        return dataModel;
    }

    public void setItems( List<t> items ) {
        this.items = items;
    }

    public Object getCellValue ()
    {
        try
        {
            if ( isRowAvailable() && columnModel.isRowAvailable() )
            {
                final int col = columnModel.getRowIndex();
                final Object rowData = getRowData();
                try
                {
                    return BeanUtils.getProperty( rowData, columnConfigs.get(
                            col ).getField() );
                }
                catch ( final NestedNullException e )
                {
                    // in this case we have a nested property and a nested reference returns null
                    // for us this mean the cellvalue is null too
                    return null;
                }
            }
        }
        catch ( final IllegalAccessException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        catch ( final InvocationTargetException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        catch ( final NoSuchMethodException e )
        {
            log.error("Failed to lookup cell value, possibly invalid column field configured",
                    e );
        }
        return null;
    }

    public boolean isRowAvailable ()
    {
        return items != null &&
        (!(rowIndex > items.size()) || !(rowIndex < 0));
    }

    public int getRowCount ()
    {
        return items.size();
    }

    public Object getRowData ()
    {
        return items.get( rowIndex );
    }

    public int getRowIndex ()
    {
        return rowIndex;
    }

    public void setRowIndex ( int rowIndex )
    {
        this.rowIndex = rowIndex;
    }

    public Object getWrappedData ()
    {
        return null;
    }

    public void setWrappedData ( Object o )
    {
    }

}

public class ColumnConfig
{

    private String label;

    private String field;

...
}

This is a basis to add more features like:
- attribute type specific output rendering
- row selection
- table inline data editing
- data pagination
- role based column rendering
- user customized column order/visibility

I'll describe this features in my next blogs.

Freitag, 11. Dezember 2009

Arithmetic operations using generic Hibernate Aliases

hibernate

I explained in my last blog Generic Joins with Hibernate Criteria how to create aliases for association paths. With the alias it's possible to get access to every attribute in your entity objects graph. Now I want to use an alias to create Projections with arithmetic operations. Therefore I use the method Projections.sqlProjection.
As the method name implies we have to use the SQL aliases instead of the Hibernate aliases. How do we get Hibernate generated SQL aliases before query execution. First thing is to create the alias for an association path as explained in Generic Joins with Hibernate Criteria. As next we need to get access to the CriteriaQueryTranslator which holds the SQL aliases. With CriteriaQueryTranslator.getSQLAlias( Criteria ) it's possible to get the sql alias. The SQL aliases can be directly used in the SQL projection. The following code calculates the total of an order with order items.

public class Order() {
    Set<OrderItem> orderItems = new HashSet<OrderItem>();
    ...
}

public class OrderItem() {
    private int quantity;
    private double price;
}

public class OrderDao() {

    public double getTotal() {
        Criteria criteria = new Critera(Order.class);
        String quantityAlias = createAlias( criteria, "orderItems.quantity);
        String priceAlias = createAlias( criteria, "orderItems.price");

        String quantitySQLAlias = getSqlAlias(criteria, quantityAlias);
        String priceSQLAlias = getSqlAlias(criteria, priceAlias);

        criteria.setProjection(Projections.sqlProjection( "sum(" + quantitySQLAlias + "*" + priceSQLAlias + ") as total", new String[] {"total"}, new Type[] {Hibernate.DOUBLE} ));
        return criteria.uniqueResult();
    }

    public static String getSqlAlias ( Criteria criteria, String alias )
    {
        CriteriaImpl criteriaImpl = ( CriteriaImpl ) criteria;
        SessionImplementor session = ( ( CriteriaImpl ) criteria ).getSession();
        SessionFactoryImplementor factory = session.getFactory();
        String[] implementors = factory.getImplementors( criteriaImpl.getEntityOrClassName() );

        CriteriaQueryTranslator translator = new CriteriaQueryTranslator( factory,
                ( CriteriaImpl ) criteria, implementors[ 0 ], CriteriaQueryTranslator.ROOT_SQL_ALIAS );

        String[] split = alias.split( "\\." );
        String path = split[ 0 ];
        String propertyName = split[ 1 ];

        Criteria aliasCriteria = findCriteriaForAlias( criteria, path );

        return translator.getSQLAlias( aliasCriteria ) + "." + propertyName;
    }

    public static Criteria findCriteriaForAlias ( Criteria criteria, String alias )
    {
        Iterator subcriterias = ( ( CriteriaImpl ) criteria )
                .iterateSubcriteria();
        while ( subcriterias.hasNext() )
        {
            Criteria subcriteria = ( Criteria ) subcriterias.next();
            if ( subcriteria.getAlias().equals( alias ) )
            {
                return subcriteria;
            }
        }
        return null;
    }

Generic Aliases/Joins with Hibernate Criteria

hibernate

In my last project I had the demand to create generic hibernate queries for any associations paths through the entity objects graph. The next code is an example where I want to filter users by street name and by role name. The association paths are "addresses.street" and "role.name" as you can see in the UserManager.

public class User {
   private String id;

   private Set<Address>; addresses = new HashSet<Address>();

   private Role role;

}

public class Address {
    private String id;

    private String street;

    ...
}

public class Role {
    private String id;

    private String name;

    ...
}

public UserDao {
    public List<User> get(String filterPath, String value) {
        Query query = new Query("from User u where u." + filterPath + "="+value)
        return query.list();      
    }
}

public UserManager {
    /** Dosen't work because addresses is a collection **/
    public List<User> getByStreet(String street) {
        return userDao.get("addresses.street", "Longstreet");
    }
    /** This works **/
    public List<User> getByRoleName(String roleName) {
        return userDao.get("role.name", "roleName");
    }
}

The problem is we have to insert joins and create aliases to make restrictions work via collections. Doing this in a generic way is pretty error-prone. Let's try this with Criteria instead of Query.

With criteria we have to create "subcriterias" on the root criteria for every join. We also have to consider not create multiple subcriterias for the same node in the object graph because this ends in an exception. The public "createAlias" methods of the following code adds subcriterias on a criteria a returns the alias for it. If you don't want to make a inner join you can specify it with CriteriaSpecification.

public static String createAlias ( Criteria criteria, String associationPath )
    {
        return createAlias( criteria, criteria, criteria.getAlias(),
                associationPath, CriteriaSpecification.INNER_JOIN );
    }

    public static String createAlias ( Criteria criteria, String associationPath,
                                       int joinType )
    {
        return createAlias( criteria, criteria, criteria.getAlias(),
                associationPath, joinType );
    }

    private static String createAlias ( Criteria rootCriteria,
                                        Criteria currentSubCriteria, String alias,
                                        String associationPath, int joinType )
    {
        StringTokenizer st = new StringTokenizer( associationPath, "." );
        if ( st.countTokens() == 1 )
        {
            return alias + "." + associationPath;
        }
        else
        {
            String associationPathToken = st.nextToken();
            String newAlias = alias + "_" + associationPathToken;
            Criteria criteriaForAlias = findCriteriaForAlias( rootCriteria,
                    newAlias );
            if ( criteriaForAlias == null )
            {
                currentSubCriteria = currentSubCriteria.createCriteria(
                        associationPathToken, newAlias, joinType );
            }
            else
            {
                currentSubCriteria = criteriaForAlias;
            }

            String newKey = associationPath.substring( associationPathToken
                    .length() + 1, associationPath.length() );
            return createAlias( rootCriteria, currentSubCriteria, newAlias,
                    newKey, joinType );
        }
    }

    public static Criteria findCriteriaForAlias ( Criteria criteria, String alias )
    {
        Iterator subcriterias = ( ( CriteriaImpl ) criteria )
                .iterateSubcriteria();
        while ( subcriterias.hasNext() )
        {
            Criteria subcriteria = ( Criteria ) subcriterias.next();
            if ( subcriteria.getAlias().equals( alias ) )
            {
                return subcriteria;
            }
        }
        return null;
    }

Now it's possible to make queries with association paths via collections into to the depths of our object graph.

public UserDao {    
    public List<User> getStreetStartingWithA() {
        Criteria criteria = new Criteria(User.class);
        String streetAlias = createAlias(criteria, "addresses.street");
        criteria.add(Restrictions.like( streetAlias, "A", MatchMode.START);
        return criteria.list();
    }

    public List getStreetNamesOrderedAsc() {
        Criteria criteria = new Criteria(User.class);
        String streetAlias = createAlias(criteria, "addresses.street");
        criteria.add( Projections.distinct( streetAlias);
        criteria.addOrder( Order.asc( streetAlias ) );
        return criteria.list();
    }
}


The next thing is to add some classes to make adding restrictions more convenient.
With the "createCriteria" method it's possible create a criteria and add restrictions from FilterCriterias.

public UserDao {    
    public List<User> getStreetStartingWithA() {
        Filter filter = new Filter();
        filter.addFilterCriteria( "addresses.street", "A", Matcher.START);
        Criteria criteria = createCriteria( session, User.class, filter);
        return criteria.list();
    }
}

    public static Criteria createCriteria ( Session session, Class persistentClass, Filter filter )
    {
        String alias = persistentClass.getSimpleName().toLowerCase();
        Criteria criteria = session.createCriteria( persistentClass,
                alias );

        if ( filter != null )
        {
            for ( FilterCriteria filterCriteria : filter.getFilterCriterias() )
            {
                String restrictionPath = createAlias( criteria,
                        filterCriteria.getPath() );
                if ( filterCriteria.getValue() instanceof String )
                {
                    criteria.add( Restrictions.like( restrictionPath,
                            ( String ) filterCriteria.getValue(), filterCriteria
                                    .getMatcher().getMatchMode() ) );
                }
                else if ( Collection.class.isAssignableFrom( filterCriteria.getValue().getClass() ) )
                {
                    Collection coll = ( Collection ) filterCriteria.getValue();
                    criteria.add( Restrictions.in( restrictionPath,
                            coll ) );
                }else if ( Object[].class.isAssignableFrom( filterCriteria.getValue().getClass() ) )
                {
                    Object[] coll = ( Object[] ) filterCriteria.getValue();
                    criteria.add( Restrictions.in( restrictionPath,
                            coll ) );
                }
                else
                {
                    criteria.add( Restrictions.eq( restrictionPath,
                            filterCriteria.getValue() ) );
                }

            }
        }
        return criteria;
    }

public class Filter
{

    private List< FilterCriteria > filterCriterias = new ArrayList< FilterCriteria >();
    public Class getEntityClass()
    {
        return entityClass;
    }

    public List< FilterCriteria > getFilterCriterias()
    {
        return filterCriterias;
    }

    public void addFilterCriteria( String path, Object value )
    {
        filterCriterias.add( new FilterCriteria( path, value ) );
    }

    public void addFilterCriteria( String path, Object value, Matcher matcher )
    {
        filterCriterias.add( new FilterCriteria( path, value, matcher ) );
    }

}

public class FilterCriteria
{

    private Matcher matcher = Matcher.EXACT;

    private String path;

    private Object value;

    public FilterCriteria ( String path, Object value )
    {
        this.path = path;
        this.value = value;
    }

    public FilterCriteria ( String path, Object value, Matcher matcher )
    {
        this.path = path;
        this.value = value;
        this.matcher = matcher;
    }

    public Matcher getMatcher()
    {
        return matcher;
    }

    public Object getValue()
    {
        return value;
    }

    public String getPath()
    {
        return path;
    }

}

public enum Matcher
{
    EXACT( "filter.matcher.exact", MatchMode.EXACT ), ANYWHERE(
            "filter.matcher.anywhere", MatchMode.ANYWHERE ), START(
            "filter.matcher.start", MatchMode.START ), END(
            "filter.matcher.end", MatchMode.END );

    private String messageKey;

    private MatchMode matchMode;

    Matcher ( String messageKey, MatchMode matchMode )
    {
        this.messageKey = messageKey;
        this.matchMode = matchMode;
    }

    public String getMessageKey()
    {
        return messageKey;
    }

    public MatchMode getMatchMode()
    {
        return matchMode;
    }
}