/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
// RadioRenderer.java
package com.sun.faces.renderkit.html_basic;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.component.UINamingContainer;
import javax.faces.component.UISelectItem;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.event.ComponentSystemEventListener;
import javax.faces.event.ListenerFor;
import javax.faces.event.PostAddToViewEvent;
import javax.faces.model.SelectItem;
import com.sun.faces.RIConstants;
import com.sun.faces.renderkit.Attribute;
import com.sun.faces.renderkit.AttributeManager;
import com.sun.faces.renderkit.RenderKitUtils;
import com.sun.faces.renderkit.SelectItemsIterator;
import com.sun.faces.util.RequestStateManager;
import com.sun.faces.util.Util;
/**
* <B>ReadoRenderer</B> is a class that renders the current value of
* <code>UISelectOne<code> or <code>UISelectMany<code> component as a list of
* radio buttons
*/
@ListenerFor(systemEventClass=PostAddToViewEvent.class)
public class RadioRenderer extends SelectManyCheckboxListRenderer implements ComponentSystemEventListener {
private static final Attribute[] ATTRIBUTES =
AttributeManager.getAttributes(AttributeManager.Key.SELECTONERADIO);
// -------------------------------------------------------------------------------------------------- Public Methods
/**
* After adding component to view, if component has group attribute set, then pre-collect the components by group.
*/
@Override
public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
UISelectOne radio = (UISelectOne) event.getComponent();
Group group = getGroup(event.getFacesContext(), radio);
if (group != null) {
group.addRadio(event.getFacesContext(), radio);
}
}
/**
* This override delegates to {@link #decodeGroup(FacesContext, UISelectOne, String)}
* when 'group' attribute is set. It will only decode when the current component is the first one of group.
*/
@Override
public void decode(FacesContext context, UIComponent component) {
UISelectOne radio = (UISelectOne) component;
Group group = getGroup(context, radio);
if (group != null) {
decodeGroup(context, radio, group);
}
else {
super.decode(context, component); // Continue default decoding.
}
}
/**
* This override delegates to {@link #encodeEndGroup(FacesContext, UISelectOne, String)}
* when 'group' attribute is set.
*/
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
UISelectOne radio = (UISelectOne) component;
Group group = getGroup(context, radio);
if (group != null) {
encodeEndGroup(context, radio, group);
}
else {
super.encodeEnd(context, component); // Continue default table rendering.
}
}
// ------------------------------------------------------- Protected Methods
/**
* The difference with default decoding is:
* <li>Submitted value is obtained by group name.
* <li>Submitted value is prefixed with client ID of radio button component, this need to be compared and trimmed.
* <li>If any submitted value does not belong to current radio button component, reset its value.
*/
protected void decodeGroup(FacesContext context, UISelectOne radio, Group group) {
rendererParamsNotNull(context, radio);
if (!shouldDecode(radio)) {
return;
}
String clientId = decodeBehaviors(context, radio);
if (clientId == null) {
clientId = radio.getClientId(context);
}
assert(clientId != null);
Map<String, String> requestParameterMap = context.getExternalContext().getRequestParameterMap();
String newValue = requestParameterMap.get(group.getClientName());
String prefix = clientId + UINamingContainer.getSeparatorChar(context);
if (newValue != null) {
if (newValue.startsWith(prefix)) {
String submittedValue = newValue.substring(prefix.length());
setSubmittedValue(radio, submittedValue);
if (logger.isLoggable(Level.FINE)) {
logger.fine("submitted value for UISelectOne group component "
+
radio.getId()
+ " after decoding "
+ submittedValue);
}
} else {
radio.resetValue();
}
} else {
// There is no submitted value at all, but this is different from a null value.
radio.setSubmittedValue(RIConstants.NO_VALUE);
}
}
/**
* The difference with default encoding is:
* <li>Every radio button of same 'group' will have same 'name' attribute rendered, relative to UIForm parent.
* <li>The 'value' attribute of every radio button is prefixed with client ID of radio button component itself.
* <li>No additional (table) markup is being rendered.
* <li>Label, if any, is rendered directly after radio button element, without additional markup.
*/
protected void encodeEndGroup(FacesContext context, UISelectOne radio, Group group)
throws IOException
{
rendererParamsNotNull(context, radio);
if (!shouldEncode(radio)) {
return;
}
SelectItem currentItem = RenderKitUtils.getSelectItems(context, radio).next();
String clientId = radio.getClientId(context);
Object itemValue = currentItem.getValue();
Converter<?> converter = radio.getConverter();
boolean checked = isChecked(context, radio, itemValue);
boolean disabled = Util.componentIsDisabled(radio);
ResponseWriter writer = context.getResponseWriter();
assert (writer != null);
renderInput(context, writer, radio, clientId, itemValue, converter, checked, disabled, group);
if (currentItem.getLabel() != null) {
renderLabel(writer, radio, clientId, currentItem, new OptionComponentInfo(radio));
}
}
protected boolean isChecked(FacesContext context, UISelectOne radio, Object itemValue) {
Object currentValue = radio.getSubmittedValue();
if (currentValue == null) {
currentValue = radio.getValue();
}
Class<?> type = String.class;
if (currentValue != null) {
type = currentValue.getClass();
if (type.isArray()) {
currentValue = ((Object[]) currentValue)[0];
if (null != currentValue) {
type = currentValue.getClass();
}
}
else if (Collection.class.isAssignableFrom(type)) {
Iterator<?> valueIter = ((Collection<?>) currentValue).iterator();
if ((null != valueIter) && valueIter.hasNext()) {
currentValue = valueIter.next();
if (null != currentValue) {
type = currentValue.getClass();
}
}
}
}
RequestStateManager.set(context, RequestStateManager.TARGET_COMPONENT_ATTRIBUTE_NAME, radio);
Object newValue;
try {
newValue = context.getApplication().getExpressionFactory().coerceToType(itemValue, type);
}
catch (ELException | IllegalArgumentException e) {
// If coerceToType fails, per the docs it should throw an ELException, however, SJAS 9.0 and 9.0u1 will
// throw an IllegalArgumentException instead (see https://java.net/jira/browse/GLASSFISH-1527).
newValue = itemValue;
}
return (newValue != null) && newValue.equals(currentValue);
}
@Override
protected void renderOption(FacesContext context,
UIComponent component,
Converter converter,
SelectItem curItem,
Object currentSelections,
Object[] submittedValues,
boolean alignVertical,
int itemNumber,
OptionComponentInfo optionInfo) throws IOException {
ResponseWriter writer = context.getResponseWriter();
assert (writer != null);
UISelectOne selectOne = (UISelectOne) component;
Object curValue = curItem.getValue();
boolean checked = isChecked(context, selectOne, curValue);
if (optionInfo.isHideNoSelection()
&& curItem.isNoSelectionOption()
&& curValue != null
&& !checked) {
return;
}
if (alignVertical) {
writer.writeText("\t", component, null);
writer.startElement("tr", component);
writer.writeText("\n", component, null);
}
writer.startElement("td", component);
writer.writeText("\n", component, null);
String clientId = component.getClientId(context) + UINamingContainer.getSeparatorChar(context) + Integer.toString(itemNumber);
// Don't render the disabled attribute twice if the 'parent' component is already marked disabled.
boolean disabled = !optionInfo.isDisabled() && curItem.isDisabled();
renderInput(context, writer, component, clientId, curValue, converter, checked, disabled, null);
renderLabel(writer, component, clientId, curItem, optionInfo);
writer.endElement("td");
writer.writeText("\n", component, null);
if (alignVertical) {
writer.writeText("\t", component, null);
writer.endElement("tr");
writer.writeText("\n", component, null);
}
}
protected void renderInput(FacesContext context, ResponseWriter writer, UIComponent component, String clientId, Object itemValue,
Converter<?> converter, boolean checked, boolean disabled, Group group) throws IOException
{
writer.startElement("input", component);
writer.writeAttribute("type", "radio", "type");
if (checked) {
writer.writeAttribute("checked", Boolean.TRUE, null);
}
Object value = (getFormattedValue(context, component, itemValue, converter));
if (group == null) {
writer.writeAttribute("name", component.getClientId(context), "clientId");
writer.writeAttribute("id", clientId, "id");
writer.writeAttribute("value", value, "value");
}
else {
writer.writeAttribute("name", group.getClientName(), "group");
writer.writeAttribute("id", clientId, "id");
writer.writeAttribute("value", clientId + UINamingContainer.getSeparatorChar(context) + value, "value");
}
if (disabled) {
writer.writeAttribute("disabled", true, "disabled");
}
// Apply HTML 4.x attributes specified on UISelectMany component to all
// items in the list except styleClass and style which are rendered as
// attributes of outer most table.
RenderKitUtils.renderPassThruAttributes(context,
writer,
component,
ATTRIBUTES,
getNonOnClickSelectBehaviors(component));
RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer,
component);
RenderKitUtils.renderSelectOnclick(context, component, false);
writer.endElement("input");
}
protected void renderLabel(ResponseWriter writer, UIComponent component, String forClientId, SelectItem curItem,
OptionComponentInfo optionInfo) throws IOException
{
String labelClass;
if (optionInfo.isDisabled() || curItem.isDisabled()) {
labelClass = optionInfo.getDisabledClass();
} else {
labelClass = optionInfo.getEnabledClass();
}
writer.startElement("label", component);
writer.writeAttribute("for", forClientId, "for");
// if enabledClass or disabledClass attributes are specified, apply
// it on the label.
if (labelClass != null) {
writer.writeAttribute("class", labelClass, "labelClass");
}
String itemLabel = curItem.getLabel();
if (itemLabel != null) {
writer.writeText(" ", component, null);
if (!curItem.isEscape()) {
// It seems the ResponseWriter API should
// have a writeText() with a boolean property
// to determine if it content written should
// be escaped or not.
writer.write(itemLabel);
} else {
writer.writeText(itemLabel, component, "label");
}
}
writer.endElement("label");
}
protected static Group getGroup(FacesContext context, UISelectOne radio) {
String groupName = radio.getGroup();
if (groupName == null) {
return null;
}
UIComponent groupContainer = RenderKitUtils.getForm(radio, context);
if (groupContainer == null) {
groupContainer = context.getViewRoot();
}
String clientName = groupContainer.getClientId(context) + UINamingContainer.getSeparatorChar(context) + groupName;
Map<String, Group> radioButtonGroups = RequestStateManager.get(context, RequestStateManager.PROCESSED_RADIO_BUTTON_GROUPS);
if (radioButtonGroups == null) {
radioButtonGroups = new HashMap<>();
RequestStateManager.set(context, RequestStateManager.PROCESSED_RADIO_BUTTON_GROUPS, radioButtonGroups);
}
Group group = radioButtonGroups.get(clientName);
if (group == null) {
group = new Group(context, clientName);
radioButtonGroups.put(clientName, group);
}
return group;
}
/**
* Keeps track of all <h:selectOneRadio group> detail.
*/
protected static class Group {
private final String clientName;
private final List<String> clientIds;
private ValueExpression value;
public Group(FacesContext context, String clientName) {
this.clientName = clientName;
this.clientIds = new ArrayList<>();
}
public String getClientName() {
return clientName;
}
public void addRadio(FacesContext context, UISelectOne radio) {
String clientId = radio.getClientId(context);
if (!clientIds.contains(clientId)) {
if (clientIds.isEmpty()) {
value = radio.getValueExpression("value");
}
else if (radio.getValueExpression("value") == null) {
radio.setValueExpression("value", value);
}
if (!RenderKitUtils.getSelectItems(context, radio).hasNext()) {
radio.getChildren().add(new GroupSelectItem());
}
clientIds.add(clientId);
radio.getAttributes().put(GroupSelectItem.class.getName(), Collections.unmodifiableList(clientIds));
}
}
}
/**
* Used when a <h:selectOneRadio group> doesn't have a select item; it will then get it via first radio of the group.
*/
public static class GroupSelectItem extends UISelectItem {
private SelectItem selectItem;
@SuppressWarnings("unchecked")
private SelectItem getSelectItem() {
if (selectItem == null) {
FacesContext context = getFacesContext();
UISelectOne radio = (UISelectOne) getParent();
List<String> groupClientIds = (List<String>) radio.getAttributes().get(GroupSelectItem.class.getName());
UIComponent firstRadioOfGroup = context.getViewRoot().findComponent(groupClientIds.get(0));
SelectItemsIterator<SelectItem> iterator = RenderKitUtils.getSelectItems(context, firstRadioOfGroup);
int index = groupClientIds.indexOf(radio.getClientId(context));
while (index-- > 0 && iterator.hasNext()) {
iterator.next();
}
if (!iterator.hasNext()) {
throw new IllegalStateException(MessageFormat.format(
"UISelectOne component id=\"{0}\" group=\"{1}\" has no UISelectItem",
new Object[] { radio.getId(), radio.getGroup() }));
}
selectItem = iterator.next();
}
return selectItem;
}
@Override
public Object getItemValue() {
return getSelectItem().getValue();
}
@Override
public String getItemLabel() {
return getSelectItem().getLabel();
}
@Override
public String getItemDescription() {
return getSelectItem().getDescription();
}
@Override
public boolean isItemEscaped() {
return getSelectItem().isEscape();
}
@Override
public boolean isNoSelectionOption() {
return getSelectItem().isNoSelectionOption();
}
@Override
public boolean isItemDisabled() {
return getSelectItem().isDisabled();
}
}
} // end of class RadioRenderer