Thursday, 20 August 2009

Seam Master-detail and Pagination - 2nd Try

After further work and testing, I have a Master-Detail editing page, with built in pagination for the Detail instances. The scenario applies where a single Master has a large number of Details. For example, a General Ledger Account, which has many thousands or millions of Transactions.

Key is to avoid referring to the Master's mapped list of Details (or remove the Hibernate mapping entirely?). This will mean that Hibernate will not load the large number of Details. However, transaction management of the Detail instances becomes a problem, as I struggle to find a solution that retains Seam's transaction management, but has it including new Details as well as changed items.

The solution that follows does function. The normal editing of existing Details works as expected, with changes saved when the Save button is pushed. However any new Detail instances are saved immediately on closing the ModalPanel. This problem is because we're not adding the new Details into the Master (which would mean that they are persisted as part of saving the Master).

One solution would be to keep a local List of new Detail instances, and override the EntityHome update() and persist() functions to iterate the List and persist() the Detail entities. However going to this much trouble is making me wonder if there is not a simpler approach.

Anyway, here is the code for the revised example:

1. The EntityHome class:

package org.asr.md.session;

import javax.persistence.EntityManager;

import org.asr.md.entity.*;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.framework.EntityHome;

@Name("masterEditorWithDetailPaging")
@Scope(ScopeType.CONVERSATION)
public class MasterEditorWithDetailPaging extends EntityHome {

private static final long serialVersionUID = -7731494569738456413L;
@In EntityManager entityManager;
@In(create=true) DetailListForPaging detailListForPaging;

private String filterDescription;
private Detail currentlySelectedDetail;

//Getters and setters
public String getFilterDescription() { return this.filterDescription; }
public void setFilterDescription(String filterDescription) { this.filterDescription = filterDescription; }
public Detail getCurrentlySelectedDetail() { return this.currentlySelectedDetail; }
public void selectDetail(Detail detail) { this.currentlySelectedDetail = detail; }

public void createNewDetail() {
Detail detail = new Detail();
detail.setMaster(getInstance());
detailListForPaging.getResultList().add(detail);
currentlySelectedDetail = detail;
}

public void saveNewDetail() {
if (!entityManager.contains(currentlySelectedDetail)) {
entityManager.persist(currentlySelectedDetail);
}
}

public void cancelNewDetail() {
removeDetail(currentlySelectedDetail);
}

public void cancelChanges() {
entityManager.clear();
}

//Delete a detail item
public void removeDetail(Detail detail) {
detailListForPaging.getResultList().remove(detail);
entityManager.remove(detail);
}

public String filterDetails() {
detailListForPaging.setMasterInstance(getInstance());
detailListForPaging.setDetailDescription(filterDescription);
return "success";
}

public void setMasterId(Integer id) {
setId(id);
}

public Integer getMasterId() {
return (Integer) getId();
}

@Override
public String update() {
return super.update();
}

@Override
protected Master createInstance() {
Master master = new Master();
return master;
}

public void wire() {
getInstance();
}

public boolean isWired() {
return true;
}

public Master getDefinedInstance() {
return isIdDefined() ? getInstance() : null;
}
}


2. The DetailQuery class:

package org.asr.md.session;

import org.asr.md.entity.*;
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.framework.EntityQuery;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;

@Name("detailListForPaging")
@Scope(ScopeType.CONVERSATION)
public class DetailListForPaging extends EntityQuery {

private static final long serialVersionUID = -427189818462003108L;
private static final String EJBQL = "select detail from Detail detail";
private static final String[] RESTRICTIONS = { "detail.master = #{detailListForPaging.masterInstance}",
"lower(detail.detailDescription) like concat(#{detailListForPaging.detailDescription},'%')", };

private String detailDescription = new String();
private Master master;
private boolean criteriaHaveChanged = true;

public DetailListForPaging() {
setEjbql(EJBQL);
setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
setMaxResults(25);
}

public List getResultList() {
//If the detailDescription is not set to something, then return an empty List.
if ((master == null) || (detailDescription.isEmpty())) {
return new Vector(0);
}

if (criteriaHaveChanged) {
setRestrictionExpressionStrings(Arrays.asList(RESTRICTIONS));
criteriaHaveChanged = false;
}
return super.getResultList();
}

public void setDetailDescription(String detailDescription) {
this.detailDescription = detailDescription;
this.criteriaHaveChanged = true;
}
public String getDetailDescription() { return this.detailDescription; }
public void setMasterInstance(Master master) {
this.master = master;
this.criteriaHaveChanged = true;
}
public Master getMasterInstance() { return this.master; }

}


3. The XHTML page:

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
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:a="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
template="layout/template.xhtml">

<ui:define name="body">

<rich:modalPanel id="detailEditWizard"
minHeight="200"
minWidth="500"
autosized="true">

<f:facet name="header"><h:outputText value="Edit Detail..." /></f:facet>
<f:facet name="controls">
<h:graphicImage value="/img/ico_close.gif"
styleClass="linkImage"
onclick="#{rich:component('detailEditWizard')}.hide()" />
</f:facet>

<h:form id="detailEditForm" styleClass="edit">

<h:messages globalOnly="true"/>

<s:decorate id="masterIdField" template="layout/edit.xhtml">
<ui:define name="label">Master id</ui:define>
<h:outputText id="masterId"
required="true"
value="#{masterEditorWithDetailPaging.currentlySelectedDetail.master.id}">
</h:outputText>
</s:decorate>

<s:decorate id="detailDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Detail description</ui:define>
<h:inputText id="detailDescription"
required="true"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.currentlySelectedDetail.detailDescription}">
<a:support event="onblur" reRender="detailDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div class="actionButtons" style="clear:both">

<a:commandButton id="detailEditMPClose"
onclick="#{rich:component('detailEditWizard')}.hide()"
action="#{masterEditorWithDetailPaging.saveNewDetail()}"
reRender="detailListTable"
value="Save" />

<a:commandButton id="detailEditMPCancel"
onclick="#{rich:component('detailEditWizard')}.hide()"
action="#{masterEditorWithDetailPaging.cancelNewDetail()}"
reRender="detailListTable"
value="Cancel" />

</div>
</h:form>

</rich:modalPanel>

<h:form id="master" styleClass="edit">

<rich:panel>
<f:facet name="header">#{masterEditorWithDetailPaging.managed ? 'Edit' : 'Add'} Master</f:facet>

<s:decorate id="masterDescriptionField" template="layout/edit.xhtml">
<ui:define name="label">Master description</ui:define>
<h:inputText id="masterDescription"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.instance.masterDescription}">
<a:support event="onblur" reRender="masterDescriptionField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div style="clear:both">
<span class="required">*</span>
required fields
</div>

</rich:panel>

<rich:panel>
<f:facet name="header">Detail Filter</f:facet>
<s:decorate id="detailDescriptionFilterField" template="layout/edit.xhtml">
<ui:define name="label">Detail Description</ui:define>
<h:inputText id="detailDescriptionFilter"
size="100"
maxlength="100"
value="#{masterEditorWithDetailPaging.filterDescription}">
<a:support event="onblur" reRender="detailDescriptionFilterField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div class="actionButtons">

<h:commandButton id="filter"
value="Search"
action="#{masterEditorWithDetailPaging.filterDetails}"
disabled="#{!masterEditorWithDetailPaging.wired}"
rendered="#{masterEditorWithDetailPaging.managed}">
<s:conversationId />
</h:commandButton>

<a:commandButton id="doFiltering"
action="#{masterEditorWithDetailPaging.filterDetails()}"
reRender="detailListTable"
value="Filter">
<s:conversationId />
</a:commandButton>

</div>

</rich:panel>

<!-- Detail section -->
<rich:panel>
<f:facet name="header">Detail List</f:facet>
<div class="results" id="detailList">

<h:outputText value="The detail search returned no results."
rendered="#{empty detailListForPaging.resultList}"/>

<rich:dataTable id="detailListTable"
var="_detail"
value="#{detailListForPaging.resultList}">
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Master id"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.master.id}"/>
</h:column>
<h:column>
<f:facet name="header">
<ui:include src="layout/sort.xhtml">
<ui:param name="propertyLabel" value="Detail description"/>
</ui:include>
</f:facet>
<h:outputText value="#{_detail.detailDescription}"/>
</h:column>
<rich:column styleClass="action">
<f:facet name="header">Action</f:facet>
<a:commandLink id="editDetailMP"
action="#{masterEditorWithDetailPaging.selectDetail(_detail)}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="Edit">
<s:conversationId />
</a:commandLink>
#{' '}
<a:commandLink id="removeDetail"
action="#{masterEditorWithDetailPaging.removeDetail(_detail)}"
reRender="detailListTable"
value="Delete">
<s:conversationId />
</a:commandLink>

</rich:column>
</rich:dataTable>

</div>
<s:div styleClass="actionButtons" rendered="#{empty from}">
<a:commandLink id="editDetailMP"
action="#{masterEditorWithDetailPaging.createNewDetail()}"
oncomplete="javascript:Richfaces.showModalPanel('detailEditWizard')"
reRender="detailEditForm"
value="New Detail...">
<s:conversationId />
</a:commandLink>
</s:div>
<div class="tableControl">

<s:link id="firstPage"
action="#{detailListForPaging.first()}"
reRender="detailListTable"
value="First Page">
</s:link>

<s:link id="previousPage"
action="#{detailListForPaging.previous()}"
reRender="detailListTable"
value="Previous Page">
</s:link>

<s:link id="nextPage"
action="#{detailListForPaging.next()}"
reRender="detailListTable"
value="Next Page">
</s:link>

<s:link id="lastPage"
action="#{detailListForPaging.last()}"
reRender="detailListTable"
value="Last Page">
</s:link>

</div>
</rich:panel>

<div class="actionButtons">

<h:commandButton id="save"
value="Save"
action="#{masterEditorWithDetailPaging.persist}"
disabled="#{!masterEditorWithDetailPaging.wired}"
rendered="#{!masterEditorWithDetailPaging.managed}"/>

<h:commandButton id="update"
value="Save"
action="#{masterEditorWithDetailPaging.update}"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<h:commandButton id="delete"
value="Delete"
action="#{masterEditorWithDetailPaging.remove}"
immediate="true"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<s:button id="done"
value="Done"
propagation="end"
view="/MasterList.xhtml"
rendered="#{masterEditorWithDetailPaging.managed}"/>

<s:button id="cancel"
value="Cancel"
action="#{masterEditorWithDetailPaging.cancelChanges}"
propagation="end"
view="/#{empty masterFrom ? 'MasterList' : masterFrom}.xhtml"
rendered="#{masterEditorWithDetailPaging.managed}"/>

</div>

</h:form>

</ui:define>

</ui:composition>


4. The page.xml:

<?xml version="1.0" encoding="UTF-8"?>
<page xmlns="http://jboss.com/products/seam/pages"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.com/products/seam/pages http://jboss.com/products/seam/pages-2.1.xsd"
no-conversation-view-id="/MasterList.xhtml"
login-required="true">

<begin-conversation join="true" flush-mode="MANUAL"/>

<action execute="#{masterEditorWithDetailPaging.wire}" on-postback="false"/>

<param name="masterFrom"/>
<param name="masterId" value="#{masterEditorWithDetailPaging.masterId}"/>


<navigation from-action="#{masterEditorWithDetailPaging.persist}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

<navigation from-action="#{masterEditorWithDetailPaging.update}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

<navigation from-action="#{masterEditorWithDetailPaging.remove}">
<rule>
<end-conversation/>
<redirect view-id="/MasterEditorWithPaging.xhtml"/>
</rule>
</navigation>

</page>

Wednesday, 19 August 2009

Filtering details in Master-Detail edit

In response to questions about the Seam Master-Detail editing approach that I described here, I have put together a couple of small enhancements that allow for filtering the Detail list within an instance of the Master. This might arise when working with a large list of Detail instances.

Add to MasterEditor.java:

private static final String EJB_QL = "SELECT d FROM Detail d " +
"WHERE d.master = ?1 AND d.detailDescription like ?2";
private Query detailQuery;
private String filterDescription;
public String getFilterDescription() { return this.filterDescription; }
public void setFilterDescription(String filterDescription) { this.filterDescription = filterDescription; }

@SuppressWarnings("unchecked")
public String filterDetails() {
detailQuery.setParameter(1, getInstance());
detailQuery.setParameter(2, this.filterDescription + "%");
detailListFromMaster = (List) detailQuery.getResultList();
return "success";
}



Add to the front-end:

<rich:panel>
<f:facet name="header">Detail Filter</f:facet>

<s:decorate id="detailDescriptionFilterField" template="layout/edit.xhtml">
<ui:define name="label">Detail Description</ui:define>
<h:inputText id="detailDescriptionFilter"
size="100"
maxlength="100"
value="#{masterEditor.filterDescription}">
<a:support event="onblur" reRender="detailDescriptionFilterField" bypassUpdates="true" ajaxSingle="true"/>
</h:inputText>
</s:decorate>

<div class="actionButtons">

<a:commandButton id="doFilter"
value="Search"
action="#{masterEditor.filterDetails()}"
reRender="detailListTable">
<s:conversationId />
</a:commandButton>
</div>
</rich:panel>

The "wire()" function links the detailListFromMaster to the embedded list of Detail instances from within the Master that we're editing. This can be problematic, because wire() is called as a page action. This means every resubmission will re-trigger the wire() function, effectively overwriting the filtering that we've done. In order to stop this, change the XXX.page.xml to use on-postback = false:


<action execute="#{masterEditor.wire}" on-postback="false"/>

Friday, 14 August 2009

CRM in Insurance

Here's a great analysis of typical problems that insurance companies run into when attempting to implement CRM software. The article has been written with Commercial Insurance in mind. However in my experience most of the lessons apply just as much to the personal Life Insurance industry.

In particular, it's important to highlight the difference between a reporting-focused CRM and a sales-oriented system ("1. Built for management, not sales."). Life insurance agents are incredibly protective of their customer information at the best of times, and why not when their income depends entirely on making sales to those customers? So naturally a CRM system that does not give tangible benefits to the agent is not going to have any credibility. The agents need information pushed out to them automatically - warnings of customers' birthdays, contacts from the customer to the insurance company (say surrender request), etc.

Friday, 7 August 2009

Eclipse, Quartz and Seam, Classloader exception

When changing workspaces in Eclipse, one of the projects started encountering a problem with a ClassLoader problem when running an asynchronous job through Quartz. The error was:

15:43:17,175 ERROR [JobStoreCMT] Error retrieving job, setting trigger state to ERROR.
org.quartz.JobPersistenceException: Couldn't retrieve job because a required class was not found: No ClassLoaders found for: org.jboss.seam.async.AsynchronousInvocation [See nested exception: java.lang.ClassNotFoundException: No ClassLoaders found for: org.jboss.seam.async.AsynchronousInvocation]
at org.quartz.impl.jdbcjobstore.JobStoreSupport.retrieveJob(JobStoreSupport.java:1392)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.triggerFired(JobStoreSupport.java:2879)
at org.quartz.impl.jdbcjobstore.JobStoreSupport$38.execute(JobStoreSupport.java:2847)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.executeInNonManagedTXLock(JobStoreSupport.java:3760)
at org.quartz.impl.jdbcjobstore.JobStoreSupport.triggerFired(JobStoreSupport.java:2841)
at org.quartz.core.QuartzSchedulerThread.run(QuartzSchedulerThread.java:342)
Caused by: java.lang.ClassNotFoundException: No ClassLoaders found for: org.jboss.seam.async.AsynchronousInvocation


Needless to say this was a bit suprising. The scheduler started ok (setup to start at application startup), and the application could submit a job correctly. It just wouldn't run through the scheduler.

After much hunting through the forums (again), it became clear the problem was that the Quartz library being used was the one included in JBoss (which I had previously upgraded to 1.6.5). The solution turned out to be manually adding a copy of the library (quartz.jar) into the WebContent/WEB-INF/lib directory in the Eclipse project. Then a clean rebuild and restart of the server, and hey presto, it works.

SaaS and the Tenancy Debate

This post on CouldAve highlights a debate that has been a hot topic recently - specifically whether SaaS should always be multi-tenant or not. As he notes, this is close to a religious topic, but one which I believe misses the key point.

A customer buying into the SaaS model is buying a service rather than software, and needs to stick with that understanding. Generally, they are unlikely to be overly interested in the architecture or technology behind the service, other than as much as is relevant to guaranteed service levels.

The post quotes Josh Greenbaum, who points out that this is linked to the maturity of the business / service / software. Offerings aimed at small businesses will probably end up in a multi-tenant model, offering no customisation capabilities and an arms-length support model. However it is likely that larger companies will move towards the SaaS model, and this will present some challenges as greater flexibility is demanded. The multi-tenant model is not designed for this.