views:

158

answers:

2

Hi.

We are using JSF 1.2 and Seam 2.2.

I have found some similar questions like this one, however there are so many ways this can be done that it made me more confused.

We are getting a XML file that we are reading. This XML contains information on some form fields that needs to be presented.

So I created this custom DynamicField.java that has all the information we need.

@Data //lombok annotation generating boilerplate code
public class DynamicField {
  String label; //label of the field
  String fieldKey; // some key to identify the field
  String fieldValue; //the value of field
  String type; //can be input,radio,selectbox etc
}

So we have a List<DynamicField>.
I want to iterate through this list and populate the form fields so it looks something like this:

<h:dataTable value="#{dynamicList} var="dyn">
    <kf:ourCustomComponent value="#{dyn}"/>
</h:dataTable>

The <kf:ourCustomComponent> would then return the appropriate JSF form components ie (label, inputText etc)

OR

Another approach would be to just display the <kf:ourCustomComponent> and then that would return a HtmlDataTable with form elements. (I think this is maybe easier to do).

My questions

  1. Which approach is best?
  2. Can someone show me to some links or code where it shows how I can create this? I prefer complete code examples, and not answers like "`You need a subclass of javax.faces.component.UIComponent`".


UPDATE

This is how I solved this. Any feedback/comments appreciated

I updated my DynamicField.java to be

@Data //lombok annotation generating boilerplate code
public class DynamicField {
  String label; //label of the field
  String fieldKey; // some key to identify the field
  String fieldValue; //the value of field
  String list; //comma separated list that I later transform to List<String> to be used with SelectMany, CheckMany etc
  Type type = Type.TEXT; //default
  transient Boolean option = Boolean.FALSE; //This is a transient variable used for Radio, CheckOne etc

  public enum Type {
    TEXT, SECRET, TEXTAREA, RADIO, SELECTONE, SELECTMANY, CHECKONE, CHECKMANY;
    @Override
    public String toString() {
        return this.name();
    }
  }
  /**
 * We need to do this check because if it is a radio, and the value is 'true' 
 * @return
 */
@XmlTransient
public Boolean getOption() {
    option = Boolean.valueOf(this.fieldValue); 
    return option;
}

public void setOption(Boolean option) {
    this.option = option;
    this.fieldValue = String.valueOf(option);
}

}

I put all the values in a Map<String, ArrayList<DynamicField>> and I create a List which is a new JavaBean containing the title (key in the map) and ArrayList<DynamicField>

@ToString
public class SubjectDynamicField implements Serializable {

    @Getter private final String title;
    //Must use ArrayList because List will not work in JAXB
    @Getter private final ArrayList<DynamicField> fields;

    public SubjectDynamicField(String title, ArrayList<DynamicField> fields) {
        if(fields == null) {
                throw new NullPointerException("DynamicFields cannot be null");
        }
        this.title = title;
        this.fields = fields;
    }
}

I populate this List like this:

List<SubjectDynamicField> subjectDynamicField = new ArrayList<SubjectDynamicField>();
for (String key : getDynamicFields().keySet()) {
  SubjectDynamicField sd = new SubjectDynamicField(key, getDynamicFields().get(key));
  subjectDynamicField.add(sd);
}

And in the XHTML I iterate over this list (Can you iterate over a map in Facelets?)

<ui:repeat value="#{subjectDynamicField}" var="sdf" rendered="#{not empty subjectDynamicField}" id="dynamicField">
        <h2 class="title"><a href="#">#{sdf.title}</a></h2>
        <div class="tightPanel">
        <fieldset class="fieldSet" style="border:0;margin-bottom:0.7em;">
            <ui:repeat value="#{sdf.fields}" var="df">
                <s:label>#{df.fieldKey}</s:label> 

                <h:inputText value="#{df.fieldValue}" rendered="#{df.type == 'TEXT'}" />
                <h:inputSecret value="#{df.fieldValue}" rendered="#{df.type == 'SECRET'}" />
                <h:inputTextarea value="#{df.fieldValue}" rendered="#{df.type == 'TEXTAREA'}" />

                <!-- Select one -->
                <h:selectOneRadio title="#{df.fieldKey}" label="#{df.fieldKey}" value="#{df.option}" rendered="#{df.type == 'RADIO'}">
                    <f:selectItem itemLabel="#{messages['yes']}" itemValue="#{true}" />
                    <f:selectItem itemLabel="#{messages['no']}" itemValue="#{false}" />
                </h:selectOneRadio>
                <h:selectOneMenu title="#{df.fieldKey}" label="#{df.fieldKey}" value="#{df.option}" rendered="#{df.type == 'SELECTONE'}">
                    <f:selectItem itemLabel="#{messages['yes']}" itemValue="#{true}"  />
                    <f:selectItem itemLabel="#{messages['no']}" itemValue="#{false}" />
                </h:selectOneMenu>
                <h:selectBooleanCheckbox title="#{df.fieldKey}" label="#{df.fieldKey}" value="#{df.option}" rendered="#{df.type ==  'CHECKONE'}">
                    <f:selectItem itemLabel="#{messages['yes']}" itemValue="#{true}" />
                    <f:selectItem itemLabel="#{messages['no']}" itemValue="#{false}" />
                </h:selectBooleanCheckbox>

                <!-- Select many -->
                <h:selectManyMenu value="#{df.choicesSelected}" rendered="#{df.type == 'SELECTMANY'}">
                    <s:selectItems value="#{df.choices}" var="c" label="#{c}"/>
                </h:selectManyMenu>
                <h:selectManyCheckbox value="#{df.choicesSelected}" rendered="#{df.type == 'CHECKMANY'}">
                    <s:selectItems value="#{df.choices}" var="c" label="#{c}"/>
                </h:selectManyCheckbox>

                <br/>
            </ui:repeat>
        </fieldset>
        </div>
    </ui:repeat>
+4  A: 

If the origin is XML, I suggest to go for a completely different approach: XSL. Facelets is XHTML based. You can easily use XSL to go from XML to XHTML. This is doable with a bit decent Filter which kicks in before JSF is doing the works.

Here's a kickoff example.

persons.xml

<?xml version="1.0" encoding="UTF-8"?>
<persons>
    <person>
        <name>one</name>
        <age>1</age>
    </person>
    <person>
        <name>two</name>
        <age>2</age>
    </person>
    <person>
        <name>three</name>
        <age>3</age>
    </person>
</persons>

persons.xsl

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet 
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:h="http://java.sun.com/jsf/html"&gt;

    <xsl:output method="xml"
        doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN"
        doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"/&gt;

    <xsl:template match="persons">
        <html>
        <f:view>
            <head></head>
            <body>
                <h:panelGrid columns="2">
                    <xsl:for-each select="person">
                        <xsl:variable name="name"><xsl:value-of select="name" /></xsl:variable>
                        <xsl:variable name="age"><xsl:value-of select="age" /></xsl:variable>
                        <h:outputText value="{$name}" />
                        <h:outputText value="{$age}" />
                    </xsl:for-each>
                </h:panelGrid>
            </body>
        </f:view>
        </html>
    </xsl:template>
</xsl:stylesheet>

JsfXmlFilter which is mapped on <servlet-name> of the FacesServlet and assumes that the FacesServlet itself is mapped on an <url-pattern> of *.jsf.

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    throws IOException, ServletException
{
    HttpServletRequest r = (HttpServletRequest) request;
    String rootPath = r.getSession().getServletContext().getRealPath("/");
    String uri = r.getRequestURI();
    String xhtmlFileName = uri.substring(uri.lastIndexOf("/")).replaceAll("jsf$", "xhtml"); // Change this if FacesServlet is not mapped on `*.jsf`.
    File xhtmlFile = new File(rootPath, xhtmlFileName);

    if (!xhtmlFile.exists()) { // Do your caching job.
        String xmlFileName = xhtmlFileName.replaceAll("xhtml$", "xml");
        String xslFileName = xhtmlFileName.replaceAll("xhtml$", "xsl");
        File xmlFile = new File(rootPath, xmlFileName);
        File xslFile = new File(rootPath, xslFileName);
        Source xmlSource = new StreamSource(xmlFile);
        Source xslSource = new StreamSource(xslFile);
        Result xhtmlResult = new StreamResult(xhtmlFile);

        try {
            Transformer transformer = TransformerFactory.newInstance().newTransformer(xslSource);
            transformer.transform(xmlSource, xhtmlResult);
        } catch (TransformerException e) {
            throw new RuntimeException("Transforming failed.", e);
        }
    }

    chain.doFilter(request, response);
}

Run by http://example.com/context/persons.jsf and this filter will kick in and transform persons.xml to persons.xhtml using persons.xsl and finally put persons.xhtml there where JSF expect it is.

True, XSL has a bit of learning curve, but it's IMO the right tool for the job since the source is XML and destination is XML based as wel.

To do the mapping between the form and the managed bean, just use a Map<String, Object>. If you name the input fields like so

<h:inputText value="#{bean.map.field1}" />
<h:inputText value="#{bean.map.field2}" />
<h:inputText value="#{bean.map.field3}" />
...

The submitted values will be available by Map keys field1, field2, field3, etc.

BalusC
Hi @BalusC. Thanks for an extensive answer. However, I am not sure if I can benefit from this with our current model.Yes we are getting the data through XML, however it is already through Smooks transfered to a JavaBean (xml2Java).So I am not sure I can do what you suggest here...
Shervin
+2  A: 

Since the origin is actually not XML, but a Javabean, and the other answer doesn't deserve to be edited into a totally different flavor (it may still be useful for future references by others), I'll add another answer based on a Javabean-origin.


I see basically three options when the origin is a Javabean.

  1. Make use of the rendered attribute.

    <h:dataTable value="#{bean.fields}" var="field">
        <h:column>
            <h:inputText value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXT'}" />
            <h:inputSecret value="#{bean.values[field.name]}" rendered="#{field.type == 'SECRET'}" />
            <h:inputTextArea value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXTAREA'}" />
            <h:selectOneRadio value="#{bean.values[field.name]}" rendered="#{field.type == 'RADIO'}">
                <f:selectItems value="#{field.options}" />
            </h:selectOneRadio>
            <h:selectOneMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTONE'}">
                <f:selectItems value="#{field.options}" />
            </h:selectOneMenu>
            <h:selectManyMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTMANY'}">
                <f:selectItems value="#{field.options}" />
            </h:selectManyMenu>
            <h:selectBooleanCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKONE'}" />
            <h:selectManyCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKMANY'}">
                <f:selectItems value="#{field.options}" />
            </h:selectManyCheckbox>
        </h:column>
    </h:dataTable>
    

    The #{bean.values} should point to a Map<String, Object> which is already precreated. A HashMap suffices. You may want to prepopulate the map in case of controls which can set multiple values. You should then prepopulate it with a List<Object> as value. Note that I expect the Field#getType() to be an enum since that eases the processing in the Java code side. You can then use a switch statement instead of a nasty if/else block.

  2. Create the components programmatically. First bind a form with the bean:

    <h:form id="form" binding="#{bean.form}" />
    

    And then lazily populate it in the getter of the form:

    public HtmlForm getForm() {
        if (form == null) {
            form = new HtmlForm();
            for (Field field : fields) {
                switch (field.getType()) { // It's easiest if it's an enum.
                    case TEXT:
                        UIInput input = new HtmlInputText();
                        input.setId(field.getName()); // Must be unique!
                        input.setValueExpression("value", createValueExpression("#{bean.values['" + field.getName() + "']}", String.class));
                        form.getChildren().add(input);
                        break;
                    case SECRET:
                        UIInput input = new HtmlInputSecret();
                        // etc...
                }
            }
        }
        return form;
    }
    
  3. Create a custom component with a custom renderer. I am not going to post complete examples since that's a lot of code which I can't write from top of head. I can at least hint to Google using the keywords "extends UIComponentElTag" or "extend UIComponentELTag".

Pros and cons of each option should be clear. It goes from most easy to most hard and subsequently also from least reuseable to best reuseable. It's up to you to pick whatever the best suits your functional requirement and current situation.

BalusC
Excellent! Great answer. Thanks!
Shervin
You're welcome.
BalusC
@BalusC Great! (+1)
Arthur Ronald F D Garcia