You can find the full source code for this website in the Seam package in the directory /examples/wiki. It is licensed under the LGPL.
Generating The Dynamic Table View
Dynamic CRUD through Facelets Composite Templates and Custom EL functions.
Installation Instructions:
<context-param> <param-name>facelets.REFRESH_PERIOD</param-name> <param-value>0</param-value> </context-param>
This gets around a refresh issue with the templates in the jar.
<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:crud="http://enhancements.seam/jsf" template="layout/template.xhtml"> <ui:define name="body"> <crud:masterEdit controlKey="dog"/> </ui:define> </ui:composition>
<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:crud="http://enhancements.seam/jsf" template="layout/template.xhtml"> <ui:define name="body"> <crud:masterView controlKey="dog"/> </ui:define> </ui:composition>
@Name("dogList") public class DogList extends EntityQuery<Dog> { @Override public String getEjbql() { return "select dog from Dog dog"; } //Required with current version private Dog dog = new Dog(); public Dog getDog() { return dog; } }
It is necessary to create a new entity like above with the private Dog dog = new Dog(); and have the getter. The EL looks for an Object based on the controlKey in the List code, on my list to find a work around.
@Entity public class Dog implements Serializable { //seam-gen attributes (you should probably edit these) private Long id; private Integer version; private String name; private String ownerName; private Integer age; //add additional entity attributes //seam-gen attribute getters/setters with annotations (you probably should edit) public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public String getOwnerName() { return ownerName; } public void setOwnerName(String ownerName) { this.ownerName = ownerName; } @Id @GeneratedValue public Long getId() { return id; } public void setId(Long id) { this.id = id; } @Version public Integer getVersion() { return version; } private void setVersion(Integer version) { this.version = version; } @Length(max=20) public String getName() { return name; } public void setName(String name) { this.name = name; } }
And redeploy and refresh http://localhost:8080/<webapp>/dog.seam. Now you'll see the additional fields, go ahead and create a dog if you'd like
<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:crud="http://enhancements.seam/jsf" template="layout/template.xhtml"> <ui:define name="body"> <crud:masterEdit controlKey="dog" requiredFields="name, age"/> </ui:define> </ui:composition>
No need to redeploy here as this is an xhtml file, just refresh your browser after you've exploded or allowed JBDS to synchronize. Now you'll see the two fields are required in dog.seam and if you fail to enter a value a validation error will pop up automatically via an ajax update.
<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:crud="http://enhancements.seam/jsf" template="layout/template.xhtml"> <ui:define name="body"> <crud:masterView controlKey="dog" searchFields="ownerName, name"/> </ui:define> </ui:composition>
but don't refresh yet, one more change:
@Name("dogList") public class DogList extends EntityQuery { private static final String[] RESTRICTIONS = { "lower(dog.name) like lower( concat(#{dogList.dog.name},'%') )", "lower(dog.ownerName) like lower( concat(#{dogList.dog.ownerName},'%') )" }; @Override public String getEjbql() { return "select dog from Dog dog"; } private Dog dog = new Dog(); public Dog getDog() { return dog; } @Override public List<String> getRestrictions() { System.out.println("Adding restrictions: " + RESTRICTIONS); return Arrays.asList(RESTRICTIONS); } }
now re-deploy the app and refresh dogList.seam, given your app has a create-drop policy, create a few more Dogs, then you'll be able to search via the search fields.
If you have any problems running the above, please email me smendenh@redhat.com with the versions of Seam/Richfaces you are using along with the error messages you are getting and how you got those messages. Or feel free to make any feature requests/comments/criticisms/suggestions.
Code Explanation
Note that I am not writing this initially with performance in mind, purely function. And none of the following should ever be used in production in its current incantation.
This allows the following <crud:masterEdit ... eyeColorEnum="#{eyeColors}" eyeColorEnumLabel="label" ... /> where eyeColorEnum and eyeColorEnumLabel are not known to the composite template in the backend. This is accomplished via a TagHandler which exposes the attribute in the FaceletsContext (VariableMapper is private and not accessible, has to be a better way than forcing it) to the the FacesContext. This allows the attributes to be references via EL in the composite template.
Continuing the example above, eyeColorEnum gets exposed to the FacesContext in the edit.xhtml composite template here: <e:setPropertyBindings valueBinding="#{field.name}" endsWith="Enum"/>. Meaning, look for a variable in the Facelets VariableMapper that ends with Enum, and starts with #{field.name} which #{field.name} resolves to eyeColor, and put that in the FacesContext.
Then the following with comments inline:
<e:isEnum id="vb"> <!-- Bind the var enum to the previously exposed value ie eyeColorEnum --> <e:setValueBinding var="enum" valueBinding="#{e:evalEl(e:concat(field.name, 'Enum'))}"/> <!-- Bind eyeColorEnumLabel like above--> <e:setValueBinding var="enumLabel" valueBinding="#{e:evalEl(e:concat(field.name, 'EnumLabel'))}"/> <!-- Now the value can just be referenced like always #{enum} and the label can be referenced via #{e[enumLabel]} which is standard composite EL --> <h:selectOneMenu value="#{entity[field.name]}"> <s:selectItems value="#{enum}" var="e" label="#{e[enumLabel]}" noSelectionLabel="Please select" /> <s:convertEnum /> </h:selectOneMenu> </e:isEnum>
<crud:masterEdit /> Usage
<crud:masterEdit /> is backed by masterEdit.xhtml which is composed of <crud:masterEdit /> backed by edit.xhtml and <crud:actionButtons /> backed by actionButtons.xhtml and it contains all of the necessary formatting for a typical crud page, ie. messages, rich panels, ect...
Available attributes crud:masterEdit:
version, id
Now how do we deal with displaying @OneToMany or @ManyToMany List collections This is extremely easy as long as you observe a few rules:
Ex.
<crud:masterEdit controlKey="person" requiredFields="firstName, email" childrenList="#{childrenList}" />
Ex.
@OneToMany(fetch = FetchType.LAZY, cascade = { CascadeType.ALL }) private List<Child> children = new ArrayList<Child>(); <crud:masterEdit controlKey="person" requiredFields="firstName, email" childrenList="#{childrenList}" childrenListLabel="firstName"/>
The way this looks in the backend is such:
<h:selectManyListbox value="#{entity[field.name]}"> <s:selectItems value="#{list}" var="e" label="#{e:evalEl('e')}"/> <s:convertEntity /> </h:selectManyListbox>
Where list is defined as:
<e:setValueBinding var="list" valueBinding="#{e:evalEl(e:concat(field.name, 'List'))}"/>
Note that there is currently a problem on Post back using the actual Label defined, so in reality the label should be "#{eEevalEleconcatfield.nameListLabel}" so right now I am assuming the toString of the var e, it is not a high priority atm.
Dealing with Enums:
1) Same rules as above, except the field MUST end with the String Enum so if my Person.java has a field:
@Enumerated(EnumType.STRING) private EyeColor eyeColor;
then
<crud:masterEdit controlKey="person" requiredFields="firstName, email" childrenList="#{childrenList}" childrenListLabel="firstName" eyeColorEnum="#{eyeColors}" eyeColorEnumLabel="label"/>
The backing code being:
<e:isEnum id="vb"> <e:setValueBinding var="enum" valueBinding="#{e:evalEl(e:concat(field.name, 'Enum'))}"/> <e:setValueBinding var="enumLabel" valueBinding="#{e:evalEl(e:concat(field.name, 'EnumLabel'))}"/> <h:selectOneMenu value="#{entity[field.name]}"> <s:selectItems value="#{enum}" var="e" label="#{e[enumLabel]}" noSelectionLabel="Please select" /> <s:convertEnum /> </h:selectOneMenu> </e:isEnum>
For dates, the really only concern you need to worry about is if you want a custom pattern, to do this, create an attribute like so: <field name>DatePattern="MM/dd/yyyy" where the field is:
@Temporal(TemporalType.TIMESTAMP) private Date birthDate;
Ex.
<crud:masterEdit controlKey="person" requiredFields="firstName, email" childrenList="#{childrenList}" childrenListLabel="firstName" eyeColorEnum="#{eyeColors}" eyeColorEnumLabel="label" birthDateDatePattern="MM/dd/yyyy" />
And the backing code:
<e:isDate id="vb"> <e:setValueBinding var="datePattern" valueBinding="#{e:evalConcattedEL(field.name, 'DatePattern')}"/> <c:if test="${not empty Display}"> <ui:include src="${Display}" /> </c:if> <c:if test="${empty Display}"> <rich:calendar id="#{field.name}Input" value="#{entity[field.name]}" required="#{e:csvContains(requiredFields, field.name)}" datePattern="#{datePattern}" enableManualInput="true"> <a:support event="ondateselected" status="#{field.name}Status" reRender="#{field.name}Decorate" ajaxSingle="true" bypassUpdates="true" eventsQueue="#{eventsQueue}" ignoreDupResponses="true" requestDelay="200" rendered="#{ajax}"/> </rich:calendar> <a:status forceId="true" id="#{field.name}Status"> <f:facet name="start"> <h:graphicImage value="/img/spinner.gif"/> </f:facet> </a:status> </c:if> </e:isDate>
<crud:masterView /> Usage
<crud:masterView /> is backed by masterView.xhtml which is composed of <crud:searchView /> backed by searchView.xhtml and <crud:view /> backed by view.xhtml and it contains all of the necessary xhtml for a typical search+List page.
Available attributes crud:masterView:
version
id
id
A more complex example would look like:
<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:rich="http://richfaces.org/rich" xmlns:a="http://richfaces.org/a4j" xmlns:crud="http://enhancements.seam/jsf" xmlns:ft="http://facestrace.sourceforge.net" template="layout/template.xhtml"> <ui:define name="body"> <crud:masterView controlKey="person" birthDateDatePattern="MM/dd/yyyy" hobbiesListLabel="hobbyName" eyeColorEnumLabel="label" searchFields="firstName, id, email"/> </ui:define> </ui:composition>
A few points of interest here
Ex.
<crud:masterEdit controlKey="person" excludedFields="id, version" childrenList="#{childrenList}" childrenListLabel="firstName" hobbiesList="#{hobbiesList}" hobbiesDisplay="../manyCheckboxDisplay.xhtml" hobbiesListLabel="hobbyName" eyeColorEnum="#{eyeColors}" eyeColorEnumLabel="label" hairColorEnum="#{hairColors}" hairColorEnumLabel="label" birthDateDatePattern="MM/dd/yyyy"/>
Where ../manyCheckboxDisplay.xhtml resolves to WebContent/layout/manyCheckboxDisplay.xhtml (JBDS) or view/layout/manyCheckboxDisplay.xhtml (seam-gen) and looks like:
<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:rich="http://richfaces.org/rich" xmlns:a="http://richfaces.org/a4j"> <h:selectManyCheckbox value="#{entity[field.name]}"> <s:selectItems value="#{List}" var="e" label="#{e[ListLabel]}" /> <s:convertEntity /> </h:selectManyCheckbox> </ui:composition>
The above may be a bit confusing, but to explain: When you override the default behavior, you must retain the actual EL notations defined in the edit.xhtml code in seam-dynamic-crud.jar, otherwise the dynamic crud won't work if you change the EL. You can change layout, components, whatever within the bounds of what is being displayed, but retain the naming scheme.
The default code for displaying Lists (Many) looks like:
<c:if test="${not empty Display}"> <ui:include src="${Display}" /> </c:if> <c:if test="${empty Display}"> <h:selectManyListbox value="#{entity[field.name]}"> <s:selectItems value="#{List}" var="e" label="#{e[ListLabel]}" /> <s:convertEntity /> </h:selectManyListbox> </c:if>
Which the only thing of concern is what is between the <c:if test="${empty Display}"> being:
<h:selectManyListbox value="#{entity[field.name]}"> <s:selectItems value="#{List}" var="e" label="#{e[ListLabel]}" /> <s:convertEntity /> </h:selectManyListbox>
which is what is customizable. Overriding the Display of a field also has the advantage of being hot-deployable. Now, any changes made to manyCheckboxDisplay.xhtml does not require a redeploy. An important distinction must be made with overriding field inputs. The xhtml you use to override will also be quite generic, so avoid namings like hobbiesDisplay="../hobbiesDisplay.xhtml" as that would look like hobbiesDisplay.xhtml can only be used for the hobbies field, which is not true. Naming hobbiesDisplay="../manyCheckboxDisplay.xhtml" is much more concise since manyCheckboxDisplay.xhtml can be reused for any other List field.
Otherwise the following expensive operation is necessary: <c:forEach items="#{entityQuery.resultList.size() == 0 ? null : entityQuery.resultList.get(0).getClass().getDeclaredFields()}" var="field"> Which with the assumption can be rewritten as so: <c:forEach items="#{entityQuery[controlKey].getClass().getDeclaredFields()}" var="field">
And this: <e:setValueBinding var="vb" valueBinding="#{entityQuery.resultList.get(uiComponent[tableId].rowIndex)[field.name]}"/> rewritten as: <e:setValueBinding var="vb" valueBinding="#{entityQuery[ControlKey][Field.name]}"/>
If you have any problems or bugs to report send to Samuel.Mendenhall@gmail.com