/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 2 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.core.gui.configuration.propset;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import javax.el.ValueExpression;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.component.UIInput;
import javax.faces.component.html.HtmlPanelGrid;
import javax.faces.component.html.HtmlPanelGroup;
import javax.faces.component.html.HtmlSelectBooleanCheckbox;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple;
import org.rhq.core.gui.configuration.ConfigRenderer;
import org.rhq.core.gui.configuration.CssStyleClasses;
import org.rhq.core.gui.configuration.helper.ConfigurationUtility;
import org.rhq.core.gui.configuration.helper.PropertyRenderingUtility;
import org.rhq.core.gui.util.FacesComponentUtility;
import org.rhq.core.gui.util.FacesContextUtility;
import org.rhq.core.gui.util.FacesExpressionUtility;
import org.rhq.core.util.sort.HumaneStringComparator;
/**
* @author Ian Springer
*/
public class PropertySetRenderer extends Renderer {
static final String RENDERER_TYPE = "org.rhq.PropertySet";
private static final String INIT_INPUTS_JAVA_SCRIPT_COMPONENT_ID_SUFFIX = "-initInputsJavaScript";
private static final String PROPERTY_ALL_TO_SAME_VALUE_CONTROLS_COLUMN_CLASSES = ","
+ CssStyleClasses.PROPERTY_VALUE_CELL_BORDERLESS + ",";
private static final String OPTIONAL_PROPERTY_ALL_TO_SAME_VALUE_CONTROLS_COLUMN_CLASSES = ","
+ CssStyleClasses.PROPERTY_VALUE_CELL_BORDERLESS + ",,,";
/**
* Decode any new state from request parameters for the given {@link PropertySetComponent}.
*
* @param facesContext the JSF context for the current request
* @param component the {@link PropertySetComponent} for which request parameters should be decoded
*/
@Override
public void decode(FacesContext facesContext, UIComponent component) {
PropertySetComponent propertySetComponent = (PropertySetComponent) component;
validateAttributes(propertySetComponent);
String id = getInitInputsJavaScriptComponentId(propertySetComponent);
UIComponent initInputsJavaScriptComponent = propertySetComponent.findComponent(id);
if (initInputsJavaScriptComponent != null) {
FacesComponentUtility.detachComponent(initInputsJavaScriptComponent);
PropertyRenderingUtility.addInitInputsJavaScript(propertySetComponent, id, false, true);
}
}
/**
* Render the beginning of the given {@link PropertySetComponent}.
*
* @param facesContext the JSF context for the current request
* @param component the {@link PropertySetComponent} to be encoded
*/
@Override
public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException {
PropertySetComponent propertySetComponent = (PropertySetComponent) component;
validateAttributes(propertySetComponent);
// If it's an AJAX request for this component, apply the group config to the member configs
// and clear our child components. NOTE: This can *not* be done in decode(), because the model
// will not have been updated yet with any changes made to child UIInput values.
String refresh = FacesContextUtility.getOptionalRequestParameter("refresh");
if (refresh != null && refresh.equals(ConfigRenderer.PROPERTY_SET_COMPONENT_ID)) {
propertySetComponent.getConfigurationSet().applyGroupConfiguration();
component.getChildren().clear();
}
ResponseWriter writer = facesContext.getResponseWriter();
String clientId = component.getClientId(facesContext);
writer.write('\n');
writer.writeComment("********** Start of " + component.getClass().getSimpleName() + " component **********");
writer.startElement("div", component);
writer.writeAttribute("id", clientId, "clientId");
// Only create the child components the first time around (i.e. once per JSF lifecycle).
if (component.getChildCount() != 0)
return;
PropertySimple propertySimple = propertySetComponent.getProperty();
if (propertySimple == null)
// This means the 'propertyExpressionString' request parameter was not specified, which means this is
// the initial display of the main configSet page, which means there's nothing for us to render.
return;
PropertyDefinitionSimple propertyDefinitionSimple = propertySetComponent.getPropertyDefinition();
addPropertyDisplayNameAndDescription(propertySetComponent, propertyDefinitionSimple, propertySimple);
boolean configReadOnly = propertySetComponent.getReadOnly() != null && propertySetComponent.getReadOnly();
boolean propIsReadOnly = PropertyRenderingUtility.isReadOnly(propertyDefinitionSimple.isReadOnly(),
propertyDefinitionSimple.isRequired(), null, configReadOnly,false);
HtmlPanelGroup setAllToSameValueControlPanel = null;
if (!propIsReadOnly) {
// NOTE: We'll add children to the below panel a bit later when we know the id's of the inputs.
FacesComponentUtility.addVerbatimText(propertySetComponent, "<table class='"
+ CssStyleClasses.MEMBER_PROPERTIES_TABLE + "'><tr><td align='center'>");
setAllToSameValueControlPanel = FacesComponentUtility.addBlockPanel(propertySetComponent, null, null);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</td></tr></table>");
FacesComponentUtility.addVerbatimText(propertySetComponent, "<br/>\n");
}
FacesComponentUtility.addVerbatimText(propertySetComponent, "\n\n<table class='"
+ CssStyleClasses.MEMBER_PROPERTIES_TABLE + "'>");
addPropertiesTableHeaderRow(propertySetComponent, propertyDefinitionSimple);
List<PropertyInfo> propertyInfos = createPropertyInfos(propertySetComponent, propertySimple);
for (int i = 0; i < propertyInfos.size(); i++) {
PropertyInfo propertyInfo = propertyInfos.get(i);
String rowStyleClass = ((i % 2) == 0) ? CssStyleClasses.ROW_ODD : CssStyleClasses.ROW_EVEN;
addPropertyRow(propertySetComponent, propertyDefinitionSimple, propertyInfo, rowStyleClass);
}
FacesComponentUtility.addVerbatimText(propertySetComponent, "</table>\n");
FacesComponentUtility.addVerbatimText(propertySetComponent, "<br/>\n");
if (!propIsReadOnly)
addSetAllToSameValueControls(propertySetComponent, propertyDefinitionSimple, setAllToSameValueControlPanel,
propertyInfos);
String id = getInitInputsJavaScriptComponentId(propertySetComponent);
PropertyRenderingUtility.addInitInputsJavaScript(propertySetComponent, id, false, false);
}
private void addSetAllToSameValueControls(PropertySetComponent propertySetComponent,
PropertyDefinitionSimple propertyDefinitionSimple, HtmlPanelGroup setAllToSameValueControlPanel,
List<PropertyInfo> propertyInfos) {
String masterInputId = propertySetComponent.getId() + "_setAllToSameValue";
UIInput input = PropertyRenderingUtility.createInput(propertyDefinitionSimple);
input.setId(masterInputId);
// NOTE: Don't add the input to the component tree yet - we'll add it a bit later.
HtmlPanelGrid panelGrid = FacesComponentUtility.createComponent(HtmlPanelGrid.class);
if (isOptional(propertyDefinitionSimple)) {
panelGrid.setColumns(5);
panelGrid.setColumnClasses(OPTIONAL_PROPERTY_ALL_TO_SAME_VALUE_CONTROLS_COLUMN_CLASSES);
} else {
panelGrid.setColumns(3);
panelGrid.setColumnClasses(PROPERTY_ALL_TO_SAME_VALUE_CONTROLS_COLUMN_CLASSES);
}
setAllToSameValueControlPanel.getChildren().add(panelGrid);
FacesComponentUtility.addOutputText(panelGrid, null, "Set All Values To: ", null);
// the property value input
panelGrid.getChildren().add(input);
// the unset checkbox (if property is optional)
HtmlSelectBooleanCheckbox unsetCheckbox = null;
if (isOptional(propertyDefinitionSimple)) {
FacesComponentUtility.addOutputText(panelGrid, null, "Unset All: ", null);
unsetCheckbox = PropertyRenderingUtility.addUnsetControl(panelGrid, propertyDefinitionSimple.isRequired(),
propertyDefinitionSimple.isReadOnly(), null, propertySetComponent.getListIndex(), input, false,
propertySetComponent.getReadOnly(), false);
}
// the 'APPLY' button
UIForm form = FacesComponentUtility.getEnclosingForm(input);
StringBuilder html = new StringBuilder();
html.append("<button type='button' class='" + CssStyleClasses.BUTTON_SMALL + "' onclick='");
if (isOptional(propertyDefinitionSimple)) {
html.append("setAllValuesForOptionalProperty(new Array(");
for (PropertyInfo propertyInfo : propertyInfos) {
String inputHtmlDomReference = getHtmlDomReference(propertyInfo.getInput(), form, '"');
html.append(inputHtmlDomReference).append(", ");
}
html.delete(html.length() - 2, html.length()); // chop off the extra ", "
html.append("), new Array(");
for (PropertyInfo propertyInfo : propertyInfos) {
String unsetCheckboxHtmlDomReference = getHtmlDomReference(propertyInfo.getUnsetCheckbox(), form, '"');
html.append(unsetCheckboxHtmlDomReference).append(", ");
}
html.delete(html.length() - 2, html.length()); // chop off the extra ", "
String masterInputHtmlDomReference = getHtmlDomReference(input, form, '"');
html.append("), getElementValue(").append(masterInputHtmlDomReference);
String masterUnsetCheckboxHtmlDomReference = getHtmlDomReference(unsetCheckbox, form, '"');
html.append("), ").append(masterUnsetCheckboxHtmlDomReference).append(".checked);");
} else {
/*html.append("setInputsUnset(new Array(");
for (PropertyInfo propertyInfo : propertyInfos) {
String inputHtmlDomReference = getHtmlDomReference(propertyInfo.getInput(), form, '"');
html.append(inputHtmlDomReference).append(", ");
}
html.delete(html.length() - 2, html.length()); // chop off the extra ", "
html.append("), false);");*/
html.append("setInputsToValue(new Array(");
for (PropertyInfo propertyInfo : propertyInfos) {
String inputHtmlDomReference = getHtmlDomReference(propertyInfo.getInput(), form, '"');
html.append(inputHtmlDomReference).append(", ");
}
html.delete(html.length() - 2, html.length()); // chop off the extra ", "
html.append("), ");
String masterInputHtmlDomReference = getHtmlDomReference(input, form, '"');
html.append("getElementValue(").append(masterInputHtmlDomReference).append("));");
}
html.append("'>Apply</button>");
FacesComponentUtility.addVerbatimText(panelGrid, html);
}
private static String getHtmlDomReference(UIInput input, UIForm form, char quoteChar) {
FacesContext facesContext = FacesContext.getCurrentInstance();
String inputHtmlDomReference = "document." + form.getClientId(facesContext) + "[" + quoteChar
+ input.getClientId(facesContext) + quoteChar + "]";
return inputHtmlDomReference;
}
@Override
public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.writeText("\n", component, null);
writer.endElement("div");
writer.writeComment("********** End of " + component.getClass().getSimpleName() + " component **********");
}
private void addPropertyDisplayNameAndDescription(PropertySetComponent propertySetComponent,
PropertyDefinitionSimple propertyDefinitionSimple, PropertySimple propertySimple) {
FacesComponentUtility.addVerbatimText(propertySetComponent, "<br/>\n");
PropertyRenderingUtility.addPropertyDisplayName(propertySetComponent, propertyDefinitionSimple, propertySimple,
propertySetComponent.getReadOnly());
if (propertyDefinitionSimple != null) {
FacesComponentUtility.addVerbatimText(propertySetComponent, " - ");
PropertyRenderingUtility.addPropertyDescription(propertySetComponent, propertyDefinitionSimple);
}
FacesComponentUtility.addVerbatimText(propertySetComponent, "<br/><br/>\n");
}
private List<PropertyInfo> createPropertyInfos(PropertySetComponent propertySetComponent,
PropertySimple groupPropertySimple) {
ValueExpression configurationInfosExpression = propertySetComponent
.getValueExpression(PropertySetComponent.CONFIGURATION_SET_ATTRIBUTE);
String configurationInfosExpressionString = configurationInfosExpression.getExpressionString();
String configurationExpressionStringFormat = "#{"
+ FacesExpressionUtility.unwrapExpressionString(configurationInfosExpressionString)
+ ".members[%d].configuration}";
String propertyExpressionStringFormat = createPropertyExpressionFormat(configurationExpressionStringFormat,
groupPropertySimple, propertySetComponent.getListIndex());
String propertyValueExpressionStringFormat = "#{"
+ FacesExpressionUtility.unwrapExpressionString(propertyExpressionStringFormat) + ".stringValue}";
//noinspection ConstantConditions
List<ConfigurationSetMember> configurationSetMembers = propertySetComponent.getConfigurationSet().getMembers();
List<PropertyInfo> propertyInfos = new ArrayList(configurationSetMembers.size());
for (int i = 0; i < configurationSetMembers.size(); i++) {
@SuppressWarnings( { "ConstantConditions" })
ConfigurationSetMember memberInfo = configurationSetMembers.get(i);
String propertyExpressionString = String.format(propertyExpressionStringFormat, i);
PropertySimple propertySimple = FacesExpressionUtility.getValue(propertyExpressionString,
PropertySimple.class);
String propertyValueExpressionString = String.format(propertyValueExpressionStringFormat, i);
ValueExpression propertyValueExpression = FacesExpressionUtility.createValueExpression(
propertyValueExpressionString, String.class);
PropertyInfo propertyInfo = new PropertyInfo(memberInfo.getLabel(), propertySimple, propertyValueExpression);
propertyInfos.add(propertyInfo);
}
Collections.sort(propertyInfos);
return propertyInfos;
}
private void addPropertiesTableHeaderRow(PropertySetComponent propertySetComponent,
PropertyDefinitionSimple propertyDefinitionSimple) {
FacesComponentUtility.addVerbatimText(propertySetComponent, "\n\n<tr>");
FacesComponentUtility.addVerbatimText(propertySetComponent, "<th class='"
+ CssStyleClasses.PROPERTIES_TABLE_HEADER_CELL + "'>");
FacesComponentUtility.addOutputText(propertySetComponent, null, "Member", FacesComponentUtility.NO_STYLE_CLASS);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</th>");
if (isOptional(propertyDefinitionSimple)) {
FacesComponentUtility.addVerbatimText(propertySetComponent, "<th class='"
+ CssStyleClasses.PROPERTIES_TABLE_HEADER_CELL + "'>");
FacesComponentUtility.addOutputText(propertySetComponent, null, "Unset",
FacesComponentUtility.NO_STYLE_CLASS);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</th>");
}
FacesComponentUtility.addVerbatimText(propertySetComponent, "<th class='"
+ CssStyleClasses.PROPERTIES_TABLE_HEADER_CELL + "'>");
FacesComponentUtility.addOutputText(propertySetComponent, null, "Value", FacesComponentUtility.NO_STYLE_CLASS);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</th>");
FacesComponentUtility.addVerbatimText(propertySetComponent, "</tr>");
}
private void validateAttributes(PropertySetComponent propertySetComponent) {
/*if (propertySetComponent.getValueExpression(PropertySetComponent.PROPERTY_DEFINITION_ATTRIBUTE) == null) {
throw new IllegalStateException("The " + propertySetComponent.getClass().getName()
+ " component requires a '" + PropertySetComponent.PROPERTY_DEFINITION_ATTRIBUTE + "' attribute.");
}*/
if (propertySetComponent.getValueExpression(PropertySetComponent.CONFIGURATION_SET_ATTRIBUTE) == null) {
throw new IllegalStateException("The " + propertySetComponent.getClass().getName()
+ " component requires a '" + PropertySetComponent.CONFIGURATION_SET_ATTRIBUTE + "' attribute.");
}
}
private static String createPropertyExpressionFormat(String configurationExpressionString,
PropertySimple propertySimple, Integer listIndex) {
StringBuilder stringBuilder = new StringBuilder("#{");
stringBuilder.append(FacesExpressionUtility.unwrapExpressionString(configurationExpressionString));
LinkedList<Property> propertyHierarchy = ConfigurationUtility.getPropertyHierarchy(propertySimple);
for (Property property : propertyHierarchy) {
Property parentProperty = ConfigurationUtility.getParentProperty(property);
stringBuilder.append(".");
if (parentProperty == null || parentProperty instanceof PropertyMap) {
// top-level or map member property
stringBuilder.append("map['").append(property.getName()).append("']");
} else {
// list member property
stringBuilder.append("list[").append(listIndex).append("]");
}
}
stringBuilder.append("}");
return stringBuilder.toString();
}
private String getInitInputsJavaScriptComponentId(PropertySetComponent propertySetComponent) {
return propertySetComponent.getId() + INIT_INPUTS_JAVA_SCRIPT_COMPONENT_ID_SUFFIX;
}
private static void addPropertyRow(PropertySetComponent propertySetComponent,
PropertyDefinitionSimple propertyDefinitionSimple, PropertyInfo propertyInfo, String rowStyleClass) {
FacesComponentUtility.addVerbatimText(propertySetComponent, "\n\n<tr class='" + rowStyleClass + "'>");
// Member (i.e. label) column
FacesComponentUtility.addVerbatimText(propertySetComponent, "<td class='"
+ CssStyleClasses.MEMBER_PROPERTY_LABEL_CELL + "'>");
FacesComponentUtility.addOutputText(propertySetComponent, null, propertyInfo.getLabel(), null);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</td>");
// Create the input, which is referenced by the unset control and rendered under the Value column.
UIInput input;
if (propertyDefinitionSimple != null)
input = PropertyRenderingUtility.createInputForSimpleProperty(propertyDefinitionSimple, propertyInfo
.getProperty(), propertyInfo.getPropertyValueExpression(), propertySetComponent.getListIndex(), false,
propertySetComponent.getReadOnly(), false, true,null);
else
input = PropertyRenderingUtility.createInputForSimpleProperty(propertyInfo.getProperty(), propertyInfo
.getPropertyValueExpression(), propertySetComponent.getReadOnly());
propertyInfo.setInput(input);
// Unset column (only if property is optional)
if (isOptional(propertyDefinitionSimple)) {
FacesComponentUtility.addVerbatimText(propertySetComponent, "<td class='"
+ CssStyleClasses.MEMBER_PROPERTY_UNSET_CELL + "'>");
HtmlSelectBooleanCheckbox unsetCheckbox = PropertyRenderingUtility.addUnsetControl(propertySetComponent,
propertyDefinitionSimple.isRequired(), propertyDefinitionSimple.isReadOnly(), propertyInfo.getProperty(),
propertySetComponent.getListIndex(), input, false, propertySetComponent.getReadOnly(), false);
propertyInfo.setUnsetCheckbox(unsetCheckbox);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</td>");
}
// Value column
FacesComponentUtility.addVerbatimText(propertySetComponent, "<td class='"
+ CssStyleClasses.MEMBER_PROPERTY_VALUE_CELL + "'>");
propertySetComponent.getChildren().add(input);
FacesComponentUtility.addVerbatimText(propertySetComponent, "<br/>");
PropertyRenderingUtility.addMessageComponentForInput(propertySetComponent, input);
FacesComponentUtility.addVerbatimText(propertySetComponent, "</td>");
FacesComponentUtility.addVerbatimText(propertySetComponent, "</tr>");
}
private static boolean isOptional(PropertyDefinitionSimple propertyDefinitionSimple) {
return propertyDefinitionSimple == null || !propertyDefinitionSimple.isRequired();
}
class PropertyInfo implements Comparable<PropertyInfo> {
private String label;
private PropertySimple property;
private ValueExpression propertyValueExpression;
private UIInput input;
private HtmlSelectBooleanCheckbox unsetCheckbox;
PropertyInfo(String label, PropertySimple property, ValueExpression propertyValueExpression) {
// Ensure this.label will never be null, so our compareTo() impl doesn't need to deal with null labels.
this.label = (label != null) ? label : "";
this.property = property;
this.propertyValueExpression = propertyValueExpression;
}
public String getLabel() {
return label;
}
public PropertySimple getProperty() {
return property;
}
public ValueExpression getPropertyValueExpression() {
return propertyValueExpression;
}
public UIInput getInput() {
return input;
}
public void setInput(UIInput input) {
this.input = input;
}
public HtmlSelectBooleanCheckbox getUnsetCheckbox() {
return unsetCheckbox;
}
public void setUnsetCheckbox(HtmlSelectBooleanCheckbox unsetCheckbox) {
this.unsetCheckbox = unsetCheckbox;
}
public int compareTo(PropertyInfo that) {
return new HumaneStringComparator().compare(this.label, that.label);
}
}
}