/* * 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, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * 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 and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.gui.configuration; import java.io.IOException; import java.util.List; import java.util.Map; import javax.el.ValueExpression; import javax.faces.application.FacesMessage; import javax.faces.component.UIComponent; import javax.faces.component.UIForm; import javax.faces.component.UIInput; import javax.faces.component.UIParameter; import javax.faces.component.html.HtmlForm; import javax.faces.component.html.HtmlOutputLink; import javax.faces.component.html.HtmlPanelGrid; import javax.faces.component.html.HtmlPanelGroup; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; import org.ajax4jsf.component.html.HtmlAjaxCommandLink; import org.ajax4jsf.component.html.HtmlAjaxOutputPanel; import org.ajax4jsf.component.html.HtmlAjaxRegion; import org.ajax4jsf.context.AjaxContext; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.richfaces.component.html.HtmlModalPanel; import org.richfaces.component.html.HtmlSimpleTogglePanel; import org.rhq.core.clientapi.agent.configuration.ConfigurationUtility; import org.rhq.core.domain.configuration.Property; import org.rhq.core.domain.configuration.PropertyList; import org.rhq.core.domain.configuration.PropertyMap; import org.rhq.core.domain.configuration.PropertySimple; import org.rhq.core.domain.configuration.definition.PropertyDefinition; import org.rhq.core.domain.configuration.definition.PropertyDefinitionMap; import org.rhq.core.domain.configuration.definition.PropertyDefinitionSimple; import org.rhq.core.domain.configuration.definition.PropertyGroupDefinition; import org.rhq.core.gui.RequestParameterNameConstants; import org.rhq.core.gui.configuration.helper.PropertyRenderingUtility; import org.rhq.core.gui.configuration.propset.ConfigurationSetComponent; import org.rhq.core.gui.configuration.propset.PropertySetComponent; 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.gui.util.PropertyIdGeneratorUtility; /** * A renderer that renders an {@link AbstractConfigurationComponent} component as XHTML. * * @author Ian Springer */ // TODO: Make subclasses of this for rendering ConfigUIComponents versus rendering ConfigurationSetComponents. public class ConfigRenderer extends Renderer { public static final String PROPERTY_SET_COMPONENT_ID = "rhq_propSet"; protected static final String NOTE_PANEL_STYLE_CLASS = "note-panel"; protected static final String PROPERTY_GROUP_HEADER_STYLE_CLASS = "BlockTitle"; protected static final String PROPERTY_GROUP_BODY_STYLE_CLASS = "BlockContent"; protected static final String UNGROUPED_PROPERTIES_STYLE_CLASS = "BlockContent"; protected static final String REQUIRED_MARKER_TEXT_STYLE_CLASS = "required-marker-text"; protected static final String GROUP_DESCRIPTION_PANEL_STYLE_CLASS = "group-description-panel"; protected static final String GROUP_DESCRIPTION_TEXT_PANEL_STYLE_CLASS = "group-description-text-panel"; private final Log LOG = LogFactory.getLog(ConfigRenderer.class); private static final String INIT_INPUTS_JAVA_SCRIPT_COMPONENT_ID_SUFFIX = "-initInputsJavaScript"; /** * Decode request parameters for the given {@link ConfigUIComponent}. * * @param facesContext the JSF context for the current request * @param component the {@link ConfigUIComponent} for which request parameters should be decoded */ @Override public void decode(FacesContext facesContext, UIComponent component) { AbstractConfigurationComponent configurationComponent = (AbstractConfigurationComponent) component; validateAttributes(configurationComponent); String function = FacesContextUtility.getOptionalRequestParameter(RequestParameterNameConstants.FUNCTION_PARAM); if (function != null) { if (function.equals(AbstractPropertyBagUIComponentTreeFactory.DELETE_LIST_MEMBER_PROPERTY_FUNCTION)) { deleteListMemberProperty(configurationComponent); } else if (function .equals(AbstractPropertyBagUIComponentTreeFactory.DELETE_OPEN_MAP_MEMBER_PROPERTY_FUNCTION)) { deleteOpenMapMemberProperty(configurationComponent); } } if (configurationComponent.getConfiguration() != null) { String id = getInitInputsJavaScriptComponentId(configurationComponent); UIComponent initInputsJavaScriptComponent = configurationComponent.findComponent(id); if (initInputsJavaScriptComponent != null) { FacesComponentUtility.detachComponent(initInputsJavaScriptComponent); PropertyRenderingUtility.addInitInputsJavaScript(configurationComponent, id, configurationComponent.isFullyEditable(), true); } } } /** * Encode the beginning of the given {@link ConfigUIComponent}. * * @param facesContext the JSF context for the current request * @param component the {@link ConfigUIComponent} to be encoded */ @Override public void encodeBegin(FacesContext facesContext, UIComponent component) throws IOException { AbstractConfigurationComponent configurationComponent = (AbstractConfigurationComponent) component; if (isAjaxRefresh(configurationComponent)) { if (configurationComponent instanceof ConfigUIComponent) { component.getChildren().clear(); } else { // configurationComponent instanceof ConfigurationSetComponent // If it's an AJAX request for a configSet component, recalculate the group config from 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. ConfigurationSetComponent configurationSetComponent = ((ConfigurationSetComponent) configurationComponent); HtmlModalPanel propSetModalPanel = configurationSetComponent.getPropSetModalPanel(); if (propertySetComponentContainsInvalidInputs(configurationSetComponent)) { // Make sure the modal panel is shown when rendered, so the user will see the validation errors. propSetModalPanel.setShowWhenRendered(true); } else { // Otherwise, make sure it's not rendered. propSetModalPanel.setShowWhenRendered(false); configurationSetComponent.getConfigurationSet().calculateGroupConfiguration(); 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) addChildComponents(configurationComponent); } private boolean isAjaxRefresh(AbstractConfigurationComponent configurationComponent) { if (configurationComponent instanceof ConfigUIComponent) { return AjaxContext.getCurrentInstance().isAjaxRequest(); } else { // configurationComponent instanceof ConfigurationSetComponent String refresh = FacesContextUtility.getOptionalRequestParameter("refresh"); return configurationComponent.getId().equals(refresh); } } private static boolean propertySetComponentContainsInvalidInputs(ConfigurationSetComponent configurationSetComponent) { HtmlModalPanel propSetModalPanel = configurationSetComponent.getPropSetModalPanel(); boolean containsInvalidInputs = false; List<PropertySetComponent> propertySetComponents = FacesComponentUtility.getDescendantsOfType( propSetModalPanel, PropertySetComponent.class); if (propertySetComponents.size() == 1) { PropertySetComponent propertySetComponent = propertySetComponents.get(0); List<UIInput> inputs = FacesComponentUtility.getDescendantsOfType(propertySetComponent, UIInput.class); for (UIInput input : inputs) { if (!input.isValid()) { containsInvalidInputs = true; break; } } } return containsInvalidInputs; } @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 **********"); } public void addChildComponents(AbstractConfigurationComponent configurationComponent) { if ((configurationComponent.getConfigurationDefinition() == null) || ((configurationComponent.getConfiguration() != null) && configurationComponent.getConfiguration() .getMap().isEmpty())) { if (configurationComponent.getNullConfigurationDefinitionMessage() != null) { String styleClass = (configurationComponent.getNullConfigurationStyle() == null) ? "ErrorBlock" : configurationComponent.getNullConfigurationStyle(); HtmlPanelGroup messagePanel = FacesComponentUtility.addBlockPanel(configurationComponent, configurationComponent, styleClass); FacesComponentUtility.addVerbatimText(messagePanel, configurationComponent.getNullConfigurationDefinitionMessage()); } return; } if (configurationComponent.getConfiguration() == null) { if (configurationComponent.getNullConfigurationMessage() != null) { HtmlPanelGroup messagePanel = FacesComponentUtility.addBlockPanel(configurationComponent, configurationComponent, "WarnBlock"); FacesComponentUtility.addVerbatimText(messagePanel, configurationComponent.getNullConfigurationMessage()); } return; } ConfigurationUtility.normalizeConfiguration(configurationComponent.getConfiguration(), configurationComponent.getConfigurationDefinition()); if (configurationComponent instanceof ConfigurationSetComponent) { ConfigurationSetComponent configurationSetComponent = (ConfigurationSetComponent) configurationComponent; // Only add the propSet modal panel if it's not already in the component tree. if (configurationSetComponent.getPropSetModalPanel() == null) addPropSetModalPanel(configurationSetComponent); // Otherwise, if the propSet was just submitted and it doesn't contain any validation errors, // close the propSet modal. else if (isAjaxRefresh(configurationSetComponent) && !propertySetComponentContainsInvalidInputs(configurationSetComponent)) { HtmlModalPanel propSetModalPanel = configurationSetComponent.getPropSetModalPanel(); String script = "Richfaces.hideModalPanel('" + propSetModalPanel.getClientId(FacesContext.getCurrentInstance()) + "');"; FacesComponentUtility.addJavaScript(configurationSetComponent, null, null, script); } } if (!configurationComponent.isReadOnly()) addRequiredNotationsKey(configurationComponent); if (configurationComponent.getListName() != null) { if (configurationComponent.getListIndex() == null) { // No index specified means we should add a new map to the list. configurationComponent.setListIndex(addNewMap(configurationComponent)); } addListMemberProperty(configurationComponent); } else { addConfiguration(configurationComponent); } String id = getInitInputsJavaScriptComponentId(configurationComponent); PropertyRenderingUtility.addInitInputsJavaScript(configurationComponent, id, configurationComponent.isFullyEditable(), false); } private void addListMemberProperty(AbstractConfigurationComponent configurationComponent) { // Update member Configurations if this is a group config. if (configurationComponent instanceof ConfigurationSetComponent) { ConfigurationSetComponent configurationSetComponent = ((ConfigurationSetComponent) configurationComponent); configurationSetComponent.getConfigurationSet().applyGroupConfiguration(); } // Now add a new component to the JSF component tree. AbstractPropertyBagUIComponentTreeFactory propertyListUIComponentTreeFactory = new MapInListUIComponentTreeFactory( configurationComponent, configurationComponent.getListName(), configurationComponent.getListIndex()); HtmlPanelGroup propertiesPanel = FacesComponentUtility.addBlockPanel(configurationComponent, configurationComponent, UNGROUPED_PROPERTIES_STYLE_CLASS); propertiesPanel.getChildren().add(propertyListUIComponentTreeFactory.createUIComponentTree(null)); } private void deleteListMemberProperty(AbstractConfigurationComponent configurationComponent) { String listName = FacesContextUtility .getRequiredRequestParameter(RequestParameterNameConstants.LIST_NAME_PARAM); PropertyList propertyList = configurationComponent.getConfiguration().getList(listName); if (propertyList == null) { FacesContextUtility.addMessage(FacesMessage.SEVERITY_ERROR, "Configuration does not contain a list named '" + listName + "'."); return; } int listIndex = FacesContextUtility.getRequiredRequestParameter(RequestParameterNameConstants.LIST_INDEX_PARAM, Integer.class); if (listIndex >= propertyList.getList().size()) { FacesContextUtility.addMessage(FacesMessage.SEVERITY_ERROR, "List '" + listName + "' does not contain an item at index " + listIndex + "."); return; } // First remove the corresponding JSF component from the component tree. Property memberProperty = propertyList.getList().get(listIndex); String memberComponentId = PropertyIdGeneratorUtility.getIdentifier(memberProperty, listIndex, AbstractPropertyBagUIComponentTreeFactory.PANEL_ID_SUFFIX); UIComponent listMemberComponent = configurationComponent.findComponent(memberComponentId); if (listMemberComponent == null) { throw new IllegalStateException("JSF component with id '" + memberComponentId + "' not found for list '" + listName + "' item " + listIndex + "."); } FacesComponentUtility.detachComponent(listMemberComponent); correctListIndexesInSiblingMembers(configurationComponent, propertyList, listIndex); // Now delete the property from the actual Configuration. propertyList.getList().remove(listIndex); // ...and from member Configurations if this is a group config. if (configurationComponent instanceof ConfigurationSetComponent) { ConfigurationSetComponent configurationSetComponent = ((ConfigurationSetComponent) configurationComponent); configurationSetComponent.getConfigurationSet().applyGroupConfiguration(); } FacesContextUtility.addMessage(FacesMessage.SEVERITY_INFO, "Property at index " + listIndex + " deleted from list '" + listName + "'."); } private void correctListIndexesInSiblingMembers(AbstractConfigurationComponent config, PropertyList propertyList, int deletedMemberIndex) { for (int oldIndex = deletedMemberIndex + 1; oldIndex < propertyList.getList().size(); oldIndex++) { int newIndex = oldIndex - 1; String listMemberComponentId = PropertyIdGeneratorUtility.getIdentifier(propertyList.getList() .get(oldIndex), oldIndex, AbstractPropertyBagUIComponentTreeFactory.PANEL_ID_SUFFIX); UIComponent listMemberComponent = config.findComponent(listMemberComponentId); String newListMemberComponentId = PropertyIdGeneratorUtility.getIdentifier( propertyList.getList().get(newIndex), newIndex, AbstractPropertyBagUIComponentTreeFactory.PANEL_ID_SUFFIX); listMemberComponent.setId(newListMemberComponentId); String listIndexParamComponentId = PropertyIdGeneratorUtility.getIdentifier( propertyList.getList().get(oldIndex), oldIndex, AbstractPropertyBagUIComponentTreeFactory.PARAM_ID_SUFFIX); UIParameter listIndexParam = (UIParameter) config.findComponent(listIndexParamComponentId); String newListIndexParamComponentId = PropertyIdGeneratorUtility.getIdentifier( propertyList.getList().get(newIndex), newIndex, AbstractPropertyBagUIComponentTreeFactory.PARAM_ID_SUFFIX); listIndexParam.setId(newListIndexParamComponentId); listIndexParam.setValue(newIndex); } } private void deleteOpenMapMemberProperty(AbstractConfigurationComponent configurationComponent) { String mapName = FacesContextUtility.getRequiredRequestParameter(RequestParameterNameConstants.MAP_NAME_PARAM); PropertyMap propertyMap = configurationComponent.getConfiguration().getMap(mapName); if (propertyMap == null) { FacesContextUtility.addMessage(FacesMessage.SEVERITY_ERROR, "Configuration does not contain a map named '" + mapName + "'."); return; } String memberName = FacesContextUtility .getRequiredRequestParameter(RequestParameterNameConstants.MEMBER_NAME_PARAM); if (!propertyMap.getMap().containsKey(memberName)) { FacesContextUtility.addMessage(FacesMessage.SEVERITY_ERROR, "Map '" + mapName + "' does not contain a member property named '" + memberName + "'."); return; } Property memberProperty = propertyMap.getMap().get(memberName); String memberComponentId = PropertyIdGeneratorUtility.getIdentifier(memberProperty, null, AbstractPropertyBagUIComponentTreeFactory.PANEL_ID_SUFFIX); UIComponent mapMemberComponent = configurationComponent.findComponent(memberComponentId); if (mapMemberComponent == null) { throw new IllegalStateException("JSF component with id '" + memberComponentId + "' not found for map '" + mapName + "' member property '" + memberName + "'."); } FacesComponentUtility.detachComponent(mapMemberComponent); // Now delete the property from the actual Configuration. propertyMap.getMap().remove(memberName); // ...and from member Configurations if this is a group config. if (configurationComponent instanceof ConfigurationSetComponent) { ConfigurationSetComponent configurationSetComponent = ((ConfigurationSetComponent) configurationComponent); configurationSetComponent.getConfigurationSet().applyGroupConfiguration(); } FacesContextUtility.addMessage(FacesMessage.SEVERITY_INFO, "Property '" + memberName + "' deleted from map '" + mapName + "'."); } private void addRequiredNotationsKey(AbstractConfigurationComponent config) { addDebug(config, true, ".addNotePanel()"); HtmlPanelGroup footnotesPanel = FacesComponentUtility.addBlockPanel(config, config, NOTE_PANEL_STYLE_CLASS); FacesComponentUtility.addOutputText(footnotesPanel, config, "*", REQUIRED_MARKER_TEXT_STYLE_CLASS); FacesComponentUtility.addOutputText(footnotesPanel, config, " denotes a required field.", FacesComponentUtility.NO_STYLE_CLASS); if (config.isGroup()) { HtmlPanelGroup overridePanel = FacesComponentUtility.addBlockPanel(config, config, NOTE_PANEL_STYLE_CLASS); FacesComponentUtility.addOutputText(overridePanel, config, "note: if override is not checked, that property will not be altered on any group members", FacesComponentUtility.NO_STYLE_CLASS); } addDebug(config, false, ".addNotePanel()"); } private void addConfiguration(AbstractConfigurationComponent config) { addNonGroupedProperties(config); addGroupedProperties(config); } private void addNonGroupedProperties(AbstractConfigurationComponent config) { addDebug(config, true, ".addNonGroupedProperties()"); HtmlPanelGroup propertiesPanel = FacesComponentUtility.addBlockPanel(config, config, UNGROUPED_PROPERTIES_STYLE_CLASS); AbstractPropertyBagUIComponentTreeFactory propertyListUIComponentTreeFactory = new GroupUIComponentTreeFactory( config, GroupUIComponentTreeFactory.NO_GROUP); propertiesPanel.getChildren().add(propertyListUIComponentTreeFactory.createUIComponentTree(null)); addDebug(config, false, ".addNonGroupedProperties()"); } private void addGroupedProperties(AbstractConfigurationComponent config) { addDebug(config, true, ".addGroupedProperties()"); List<PropertyGroupDefinition> groups = config.getConfigurationDefinition().getGroupDefinitions(); for (PropertyGroupDefinition group : groups) { HtmlSimpleTogglePanel groupPanel = addGroupPanel(config, group); AbstractPropertyBagUIComponentTreeFactory propertyListUIComponentTreeFactory = new GroupUIComponentTreeFactory( config, group.getName()); groupPanel.getChildren().add(propertyListUIComponentTreeFactory.createUIComponentTree(null)); } addDebug(config, false, ".addGroupedProperties()"); } private HtmlSimpleTogglePanel addGroupPanel(AbstractConfigurationComponent config, PropertyGroupDefinition group) { addDebug(config, true, ".addGroupPanel()"); HtmlSimpleTogglePanel groupPanel = FacesComponentUtility.addSimpleTogglePanel(config, config, null); // TODO: On AJAX requests, set "opened" attribute to its previous state. groupPanel.setOpened(!group.isDefaultHidden()); groupPanel.setHeaderClass(PROPERTY_GROUP_HEADER_STYLE_CLASS); groupPanel.setBodyClass(PROPERTY_GROUP_BODY_STYLE_CLASS); // Custom header that includes the name and description HtmlPanelGroup headerPanel = FacesComponentUtility.createBlockPanel(config, null); FacesComponentUtility.addOutputText(headerPanel, config, group.getDisplayName(), null); FacesComponentUtility .addOutputText(headerPanel, config, group.getDescription(), "group-description-text-panel"); groupPanel.getFacets().put("header", headerPanel); // custom "close" widget HtmlPanelGroup closePanel = FacesComponentUtility.createBlockPanel(config, null); closePanel.setStyle("text-align: right; font-weight: normal; font-size: 0.8em; whitespace: nowrap;"); FacesComponentUtility.addGraphicImage(closePanel, config, "/images/ico_trigger_wht_collapse.gif", "collapse"); FacesComponentUtility.addOutputText(closePanel, config, " Collapse", null); groupPanel.getFacets().put("closeMarker", closePanel); // custom "open" widget HtmlPanelGroup openPanel = FacesComponentUtility.createBlockPanel(config, null); openPanel.setStyle("text-align: right; font-weight: normal; font-size: 0.8em; whitespace: nowrap;"); FacesComponentUtility.addGraphicImage(openPanel, config, "/images/ico_trigger_wht_expand.gif", "expand"); FacesComponentUtility.addOutputText(openPanel, config, " Expand", null); groupPanel.getFacets().put("openMarker", openPanel); addDebug(config, true, ".addGroupPanel()"); return groupPanel; } private void validateAttributes(AbstractConfigurationComponent configurationComponent) { // TODO: Add back attribute validation - remember config and configSet components require different attributes. /*if (configurationComponent.getValueExpression("configurationDefinition") == null) { throw new IllegalStateException("The " + configurationComponent.getClass().getName() + " component requires a 'configurationDefinition' attribute."); } if (configurationComponent.getValueExpression("configuration") == null) { throw new IllegalStateException("The " + configurationComponent.getClass().getName() + " component requires a 'configuration' attribute."); }*/ } private int addNewMap(AbstractConfigurationComponent config) { String listName = config.getListName(); PropertyDefinitionMap mapDefinition = (PropertyDefinitionMap) config.getConfigurationDefinition() .getPropertyDefinitionList(listName).getMemberDefinition(); String mapName = mapDefinition.getName(); PropertyMap newMap = new PropertyMap(mapName); for (PropertyDefinition mapMemberDefinition : mapDefinition.getOrderedPropertyDefinitions()) { PropertyDefinitionSimple simpleDefinition = (PropertyDefinitionSimple) mapMemberDefinition; newMap.put(new PropertySimple(simpleDefinition.getName(), (simpleDefinition.isRequired()) ? "" : null)); } PropertyList list = config.getConfiguration().getList(listName); list.add(newMap); return list.getList().size() - 1; } /** * simple method that will add debugging HTML comments so that I can figure out which part of the HTML is generated * by which part of this class * * @param component the component this comment should be added * @param start true if this is the "START" comment, false if it is the "END" comment * @param methodName the name of the method this is calling from */ private void addDebug(UIComponent component, boolean start, String methodName) { if (LOG.isDebugEnabled()) { StringBuilder msg = new StringBuilder("\n<!--"); msg.append(start ? " START " : " END "); msg.append(this.getClass().getSimpleName()); msg.append(methodName); msg.append(" -->\n "); FacesComponentUtility.addVerbatimText(component, msg); } } private String getInitInputsJavaScriptComponentId(AbstractConfigurationComponent configUIComponent) { return configUIComponent.getId() + INIT_INPUTS_JAVA_SCRIPT_COMPONENT_ID_SUFFIX; } private void addPropSetModalPanel(ConfigurationSetComponent configurationSetComponent) { UIForm configSetForm = FacesComponentUtility.getEnclosingForm(configurationSetComponent); // Add the modal panel as a sibling of the configSet form. HtmlModalPanel modalPanel = FacesComponentUtility.createComponent(HtmlModalPanel.class); String modalPanelId = ConfigurationSetComponent.getPropSetModalPanelId(configurationSetComponent); modalPanel.setId(modalPanelId); modalPanel.setWidth(715); modalPanel.setHeight(535); modalPanel.setTrimOverlayedElements(false); modalPanel.setStyle("overflow-y: auto;"); modalPanel.setOnbeforeshow("sizeAppropriately('configSetForm:" + modalPanelId + "')"); modalPanel.setOnresize("keepCentered('configSetForm:" + modalPanelId + "')"); // Insert the modal panel as a sibling of the configSet form, just after the form in the component tree. int index = getComponentIndex(configSetForm); configSetForm.getParent().getChildren().add(index + 1, modalPanel); // Add the propSet form as a child of the modal panel. HtmlForm propSetForm = new HtmlForm(); modalPanel.getChildren().add(propSetForm); String propSetFormId = ConfigurationSetComponent.getPropSetFormId(configurationSetComponent); propSetForm.setId(propSetFormId); propSetForm.setOnsubmit("prepareInputsForSubmission(this)"); // Add inputs for the request params that need to be forwarded when the propSet form is submitted. forwardParameter(propSetForm, "id"); forwardParameter(propSetForm, "groupId"); forwardParameter(propSetForm, "arcuId"); forwardParameter(propSetForm, "apcuId"); forwardParameter(propSetForm, "mode"); addPropSetButtons(configurationSetComponent, propSetForm); // Add an AJAX region+panel as a child of this second form. HtmlAjaxRegion ajaxRegion = FacesComponentUtility.createComponent(HtmlAjaxRegion.class); propSetForm.getChildren().add(ajaxRegion); HtmlAjaxOutputPanel ajaxOutputPanel = FacesComponentUtility.createComponent(HtmlAjaxOutputPanel.class); ajaxRegion.getChildren().add(ajaxOutputPanel); ajaxOutputPanel.setLayout("block"); ajaxOutputPanel.setAjaxRendered(true); ajaxOutputPanel.setKeepTransient(true); // And finally add the propSet component as a child of this AJAX region. PropertySetComponent propertySet = new PropertySetComponent(); ajaxOutputPanel.getChildren().add(propertySet); propertySet.setId(PROPERTY_SET_COMPONENT_ID); propertySet.setReadOnly(configurationSetComponent.isReadOnly()); propertySet.setListIndex(configurationSetComponent.getListIndex()); ValueExpression valueExpression = FacesExpressionUtility.createValueExpression( "#{param.propertyExpressionString}", String.class); propertySet.setValueExpression(PropertySetComponent.PROPERTY_EXPRESSION_STRING_ATTRIBUTE, valueExpression); // The below can be uncommented in order for the "propertyExpressionValue" attribute to have a valid value // on the initial page load (i.e. for testing purposes). //propertySet.getAttributes().put(PropertySetComponent.PROPERTY_EXPRESSION_STRING_ATTRIBUTE, // "#{EditTestConfigurationUIBean.configurationSet.groupConfiguration.map['String1'].stringValue}"); propertySet.setValueExpression(PropertySetComponent.CONFIGURATION_SET_ATTRIBUTE, configurationSetComponent.getValueExpression(ConfigurationSetComponent.CONFIGURATION_SET_ATTRIBUTE)); addPropSetButtons(configurationSetComponent, propSetForm); return; } private void forwardParameter(HtmlForm modalPanelForm, String paramName) { Map<String, String> requestParamMap = FacesContext.getCurrentInstance().getExternalContext() .getRequestParameterMap(); String paramValue = requestParamMap.get(paramName); if (paramValue != null) FacesComponentUtility.addVerbatimText(modalPanelForm, "<input type='hidden' name='" + paramName + "' value='" + paramValue + "'/>"); } private void addPropSetButtons(ConfigurationSetComponent configurationSetComponent, HtmlForm modalPanelForm) { //the modal panel form is embedded inside the modal panel itself String modalPanelClientId = modalPanelForm.getParent().getClientId(FacesContext.getCurrentInstance()); HtmlPanelGrid panelGrid = FacesComponentUtility.addPanelGrid(modalPanelForm, null, 2, CssStyleClasses.BUTTONS_TABLE); // OK button HtmlAjaxCommandLink okLink = FacesComponentUtility.createComponent(HtmlAjaxCommandLink.class); panelGrid.getChildren().add(okLink); okLink.setTitle("OK"); FacesComponentUtility.addParameter(okLink, null, "refresh", configurationSetComponent.getId()); FacesComponentUtility.addButton(okLink, "OK", CssStyleClasses.BUTTON_MEDIUM); // Cancel button (only if not read-only) if (!configurationSetComponent.isReadOnly()) { HtmlOutputLink cancelLink = FacesComponentUtility.addOutputLink(panelGrid, null, "#"); cancelLink.setOnclick("Richfaces.hideModalPanel('" + modalPanelClientId + "'); return false;"); cancelLink.setTitle("Cancel"); FacesComponentUtility.addButton(cancelLink, "CANCEL", CssStyleClasses.BUTTON_MEDIUM); } } private static int getComponentIndex(UIForm component) { if (component.getParent() == null) return -1; List<UIComponent> children = component.getParent().getChildren(); for (int i = 0; i < children.size(); i++) { if (children.get(i) == component) return i; } throw new IllegalStateException("Unable to determine index of component " + component); } }