• Home
  • About
  • Doom II
  • Flog 2010
  • Inspiration

Seam Framework JSF Component Validation: At Least One Checkbox

Posted in Computing. on Wednesday, July 7th, 2010 by Derek
Jul 07

For some reason Seam Framework doesn’t include form validation to ensure at least one checkbox is selected out of a group of checkboxes. Like a radio button, but more than one may be selected. One custom code sample was bullshit because validation takes place in JSF lifecycle before the model is updated, so you must wait for the component tree to be built before such a validation can take place.

The solution I found wasn’t optimal but hey, I’m not the only one wanting this simple form functionality.

/resources/WEB-INF/web.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">
.
.
.
	<context-param>
		<param-name>facelets.LIBRARIES</param-name>
		<param-value>/WEB-INF/compositions.taglib.xml</param-value>
	</context-param>
</web-app>

/resources/WEB-INF/compositions.taglib.xml

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC "-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN" "facelet-taglib_1_0.dtd">
<facelet-taglib>
    <namespace>http://mytaglib.com/jsf</namespace>
    <tag>
    	<tag-name>atLeastOneValidator</tag-name>
    	<validator>
    		<validator-id>atLeastOneValidator</validator-id>
    	</validator>
    </tag>
</facelet-taglib>

/src/main/validator/AtLeastOneValidator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
package yourpackage.validator;
 
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.html.HtmlSelectBooleanCheckbox;
import javax.faces.context.FacesContext;
import javax.faces.validator.ValidatorException;
 
import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.faces.Validator;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.ui.component.html.HtmlLabel;
 
import ca.phenogenomics.util.StringUtils;
 
@Name("atLeastOneValidator")
@Validator
@BypassInterceptors
@Scope(ScopeType.CONVERSATION)
public class AtLeastOneValidator implements javax.faces.validator.Validator, Serializable {
 
	private static final long serialVersionUID = -4249428843435574402L;
 
	private static Map<String, Map<String, Boolean>> formCheckboxes;
 
	public void validate(FacesContext context, UIComponent component, Object value)
		throws ValidatorException {
 
		List<HtmlSelectBooleanCheckbox> checkboxes = new ArrayList<HtmlSelectBooleanCheckbox>();
		String groupWith = "";
		UIComponent rootComponent = FacesContext.getCurrentInstance().getViewRoot();
 
		if (!(component instanceof HtmlSelectBooleanCheckbox)) {
			throw new ValidatorException(createErrorMessage("atLeastOneValidator can only be used on HtmlSelectBooleanCheckbox components."));
		}
 
		if (component.getAttributes().get("groupWith") != null) {
			groupWith = (String)component.getAttributes().get("groupWith");
		}
 
		if (formCheckboxes == null) {
			formCheckboxes = new HashMap<String, Map<String, Boolean>>();
		}
 
		if (formCheckboxes.get(groupWith) == null) {
			formCheckboxes.put(groupWith, new HashMap<String, Boolean>());
		}
 
		// Store this component's value, queuing to be checked at the last
		// checkbox in the group to see of any component has the value
		// set to "true"
		formCheckboxes.get(groupWith).put((String)component.getAttributes().get("id"), (Boolean)value);
 
		// retrieve all checkboxes under this group in the component tree
		getCheckboxes(rootComponent, checkboxes, groupWith);
 
		// Last checkbox in the component tree of this group, so
		// now check whether at least one is checked.
		if (component.equals(checkboxes.get(checkboxes.size()-1))) {
			boolean atLeastOneChecked = false;
			String styleClass = "";
 
			for (String componentId : formCheckboxes.get(groupWith).keySet()) {
				if (formCheckboxes.get(groupWith).get(componentId)) {
					atLeastOneChecked = true;
					break;
				}
			}
 
			// highlight or unmark checkboxes/labels
			if (!atLeastOneChecked) {
				styleClass = "required";
			}
			for (HtmlSelectBooleanCheckbox checkbox : checkboxes) {
				if (checkbox.getParent() instanceof HtmlLabel) {
					((HtmlLabel)checkbox.getParent()).setStyleClass(styleClass);
				} else {
					checkbox.setStyleClass(styleClass);
				}
			}
 
			if (!atLeastOneChecked) {
				throw new ValidatorException(createErrorMessage("At least one " +
					(StringUtils.emptyString(groupWith) ? "" : "\"" + groupWith + "\" ")
					+ "checkbox must be selected."));
			}
		}
 
    }
 
	/**
	 * Recursively traverse a component tree to retrieve all the
	 * HtmlSelectBooleanCheckbox objects containing a specific groupWith=""
	 * attribute.
	 *
	 * @param component Initial invocation should be the root component.
	 * @param checkboxes Initial invocation should be instantiated blank list.
	 * @param groupWith Name/label given to the group of checkboxes. null
	 *                  or a blank string returns ALL checkboxes in the tree.
	 * @return List of child checkboxes in a component tree.
	 */
	protected List<HtmlSelectBooleanCheckbox> getCheckboxes(UIComponent component, List<HtmlSelectBooleanCheckbox> checkboxes, String groupWith) {
 
		if (component != null) {
			for (UIComponent childComponent : component.getChildren()) {
				if (childComponent instanceof HtmlSelectBooleanCheckbox) {
					if (StringUtils.emptyString(groupWith) ||
					    groupWith.equals(childComponent.getAttributes().get("groupWith"))) { 
						checkboxes.add((HtmlSelectBooleanCheckbox)childComponent);
					}
				} else if (childComponent.getChildCount() > 0) {
					getCheckboxes(childComponent, checkboxes, groupWith);
				}
			}
		}
 
		return checkboxes;
	}
 
	private FacesMessage createErrorMessage(String s) {
		FacesMessage message = new FacesMessage();
		message.setDetail(s);
		message.setSummary(s);
		message.setSeverity(FacesMessage.SEVERITY_ERROR);
		return message;
	}
 
}

/view/atLeastOneValidatorTest.xhtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html 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:rich="http://richfaces.org/rich"
                xmlns:mytaglib="http://mytaglib.com/jsf">
 
	<h:form>
		<h:messages />
		.
		.
		.
		<s:label>
			<h:selectBooleanCheckbox value="male" groupWith="Sex">
				<tcp:atLeastOneValidator />
			</h:selectBooleanCheckbox>
			<h:outputText value="Duder" />
		</s:label>
		<s:label>
			<h:selectBooleanCheckbox value="female" groupWith="Sex">
				<tcp:atLeastOneValidator />
			</h:selectBooleanCheckbox>
			<h:outputText value="Dudette" />
		</s:label>
		<s:label>
			<h:selectBooleanCheckbox value="unknown" groupWith="Sex">
				<tcp:atLeastOneValidator />
			</h:selectBooleanCheckbox>
			<h:outputText value="???" />
		</s:label>
		.
		.
		.
	</h:form>
 
</ui:composition>
  • Now Playing: Minus the Bear - Omni - 03 - Secret Country

Leave a Reply

CAPTCHA Image
CAPTCHA Audio
Refresh Image

Derek MacDonald

  • Photo Stream
  • Categories
    • Computing
    • Film & TV
    • Gaming
    • General
    • Music
    • Sports
    • Visual Art
  • Search






  • Home
  • About
  • Doom II
  • Flog 2010
  • Inspiration

© Copyright Derek MacDonald. All rights reserved.
Designed by FTL Wordpress Themes brought to you by Smashing Magazine

Back to Top