/* * Copyright (C) 2005-2008 Alfresco Software Limited. * * 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; either version 2 * of the License, or (at your option) any later version. * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * As a special exception to the terms and conditions of version 2.0 of * the GPL, you may redistribute this Program in connection with Free/Libre * and Open Source Software ("FLOSS") applications as described in Alfresco's * FLOSS exception. You should have received a copy of the text describing * the FLOSS exception, and it is also available here: * http://www.alfresco.com/legal/licensing" */ package org.alfresco.web.config; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import org.alfresco.config.ConfigElement; import org.alfresco.config.ConfigException; import org.alfresco.config.element.ConfigElementAdapter; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; /** * Custom config element that represents form values for the client. * * @author Neil McErlean. */ public class FormConfigElement extends ConfigElementAdapter { private static final long serialVersionUID = -7008510360503886308L; private static Log logger = LogFactory.getLog(FormConfigElement.class); public enum Mode { VIEW, EDIT, CREATE; public static Mode fromString(String modeString) { if (modeString.equalsIgnoreCase("create")) { return Mode.CREATE; } else if (modeString.equalsIgnoreCase("edit")) { return Mode.EDIT; } else if (modeString.equalsIgnoreCase("view")) { return Mode.VIEW; } else { return null; } } } public static final String FORM_ID = "form"; private String submissionURL; private List modelOverrides = new ArrayList(); // We need to maintain the order of templates - per mode. private List createTemplates = new ArrayList(); private List editTemplates = new ArrayList(); private List viewTemplates = new ArrayList(); /** * Map of the required roles for create-form templates. * Key = the template String. Value = the requires-role String. */ private Map rolesForCreateTemplates = new HashMap(); private Map rolesForEditTemplates = new HashMap(); private Map rolesForViewTemplates = new HashMap(); private List visibilityRules = new ArrayList(); private Map sets = new HashMap(); private Map fields = new HashMap(); public FormConfigElement() { super(FORM_ID); } public FormConfigElement(String name) { super(name); } /** * @see org.alfresco.config.ConfigElement#getChildren() */ public List getChildren() { throw new ConfigException( "Reading the default-controls config via the generic interfaces is not supported"); } /** * @see org.alfresco.config.ConfigElement#combine(org.alfresco.config.ConfigElement) */ public ConfigElement combine(ConfigElement otherConfigElement) { FormConfigElement otherFormElem = (FormConfigElement)otherConfigElement; FormConfigElement result = new FormConfigElement(); combineSubmissionURL(otherFormElem, result); combineTemplates(otherFormElem, result); combineFieldVisibilities(otherFormElem, result); combineSets(otherFormElem, result); combineFields(otherFormElem, result); //TODO field-controls //TODO constraint-for-field combineModelOverrides(otherFormElem, result); return result; } private void combineFields(FormConfigElement otherFormElem, FormConfigElement result) { for (String nextFieldId : fields.keySet()) { FormField nextField = fields.get(nextFieldId); Map nextAttributes = nextField.getAttributes(); List attributeNames = new ArrayList(nextAttributes.size()); List attributeValues = new ArrayList(nextAttributes.size()); for (String nextAttrName : nextAttributes.keySet()) { attributeNames.add(nextAttrName); attributeValues.add(nextAttributes.get(nextAttrName)); } result.addField(nextFieldId, attributeNames, attributeValues); } for (String nextFieldId : otherFormElem.fields.keySet()) { FormField nextField = otherFormElem.fields.get(nextFieldId); Map nextAttributes = nextField.getAttributes(); List attributeNames = new ArrayList(nextAttributes.size()); List attributeValues = new ArrayList(nextAttributes.size()); for (String nextAttrName : nextAttributes.keySet()) { attributeNames.add(nextAttrName); attributeValues.add(nextAttributes.get(nextAttrName)); } result.addField(nextFieldId, attributeNames, attributeValues); } } private void combineModelOverrides(FormConfigElement otherFormElem, FormConfigElement result) { for (StringPair override : modelOverrides) { result.addModelOverrides(override.getName(), override.getValue()); } for (StringPair override : otherFormElem.modelOverrides) { result.addModelOverrides(override.getName(), override.getValue()); } } private void combineSets(FormConfigElement otherFormElem, FormConfigElement result) { for (String nextOldSet : sets.keySet()) { FormSet nextOldSetData = sets.get(nextOldSet); String setId = nextOldSetData.getSetId(); String parentId = nextOldSetData.getParentId(); String appearance = nextOldSetData.getAppearance(); result.addSet(setId, parentId, appearance); } for (String nextNewSet : otherFormElem.sets.keySet()) { FormSet nextNewSetData = otherFormElem.sets.get(nextNewSet); String setId = nextNewSetData.getSetId(); String parentId = nextNewSetData.getParentId(); String appearance = nextNewSetData.getAppearance(); result.addSet(setId, parentId, appearance); } } private void combineFieldVisibilities(FormConfigElement otherFormElem, FormConfigElement result) { List combinedInstructions = new ArrayList(); combinedInstructions.addAll(this.visibilityRules); //If there are already instructions pertaining to a particular // field id, we can just leave them in place as the new should override the old. //TODO Test this is true. combinedInstructions.addAll(otherFormElem.visibilityRules); for (FieldVisibilityRule fvi : combinedInstructions) { result.addFieldVisibility(fvi.isShow() ? "show" : "hide" , fvi.getFieldId(), fvi.getModes()); } } private void combineTemplates(FormConfigElement otherFormElem, FormConfigElement result) { for (String s : this.createTemplates) { String reqsRole = this.rolesForCreateTemplates.get(s); result.addFormTemplate("create-form", s, reqsRole); } for (String s : otherFormElem.createTemplates) { String reqsRole = otherFormElem.rolesForCreateTemplates.get(s); result.addFormTemplate("create-form", s, reqsRole); } for (String s : this.editTemplates) { String reqsRole = this.rolesForEditTemplates.get(s); result.addFormTemplate("edit-form", s, reqsRole); } for (String s : otherFormElem.editTemplates) { String reqsRole = otherFormElem.rolesForEditTemplates.get(s); result.addFormTemplate("edit-form", s, reqsRole); } for (String s : this.viewTemplates) { String reqsRole = this.rolesForViewTemplates.get(s); result.addFormTemplate("view-form", s, reqsRole); } for (String s : otherFormElem.viewTemplates) { String reqsRole = otherFormElem.rolesForViewTemplates.get(s); result.addFormTemplate("view-form", s, reqsRole); } } private void combineSubmissionURL(FormConfigElement otherFormElem, FormConfigElement result) { String otherSubmissionURL = otherFormElem.getSubmissionURL(); result.setSubmissionURL(otherSubmissionURL == null ? this.submissionURL : otherSubmissionURL); } public String getSubmissionURL() { return this.submissionURL; } public Map getSets() { return Collections.unmodifiableMap(this.sets); } public List getFieldVisibilityRules() { return Collections.unmodifiableList(this.visibilityRules); } //TODO This is all fields. need getVisFields(Role) public Map getFields() { return Collections.unmodifiableMap(this.fields); } public Map getVisibleCreateFields() { return getFieldsVisibleInMode(Mode.CREATE); } public Map getVisibleEditFields() { return getFieldsVisibleInMode(Mode.EDIT); } public Map getVisibleViewFields() { return getFieldsVisibleInMode(Mode.VIEW); } public Map getCreateTemplates() { return Collections.unmodifiableMap(this.rolesForCreateTemplates); } public Map getEditTemplates() { return Collections.unmodifiableMap(this.rolesForEditTemplates); } public Map getViewTemplates() { return Collections.unmodifiableMap(this.rolesForViewTemplates); } public List getModelOverrideProperties() { return Collections.unmodifiableList(modelOverrides); } /* package */void setSubmissionURL(String newURL) { this.submissionURL = newURL; } /* package */void addFormTemplate(String nodeName, String template, String requiredRole) { if (requiredRole == null) { requiredRole = ""; } if (nodeName.equals("create-form")) { if (!createTemplates.contains(template)) { createTemplates.add(template); } rolesForCreateTemplates.put(template, requiredRole); } else if (nodeName.equals("edit-form")) { if (!editTemplates.contains(template)) { editTemplates.add(template); } rolesForEditTemplates.put(template, requiredRole); } else if (nodeName.equals("view-form")) { if (!viewTemplates.contains(template)) { viewTemplates.add(template); } rolesForViewTemplates.put(template, requiredRole); } else { if (logger.isWarnEnabled()) { logger.warn("Unrecognised mode: " + nodeName); } return; } } /* package */void addFieldVisibility(String showOrHide, String fieldId, String mode) { FieldVisibilityRule instruc = new FieldVisibilityRule(showOrHide, fieldId, mode); this.visibilityRules.add(instruc); } /* package */void addSet(String setId, String parentSetId, String appearance) { sets.put(setId, new FormSet(setId, parentSetId, appearance)); } /* package */void addField(String fieldId, List attributeNames, List attributeValues) { Map attrs = new HashMap(); for (int i = 0; i < attributeNames.size(); i++) { attrs.put(attributeNames.get(i), attributeValues.get(i)); } fields.put(fieldId, new FormField(attrs)); } /* package */void addControlForField(String fieldId, String template, List controlParamNames, List controlParamValues) { FormField field = fields.get(fieldId); field.setTemplate(template); for (int i = 0; i < controlParamNames.size(); i++) { field.addControlParam(new StringPair(controlParamNames.get(i), controlParamValues.get(i))); } } /* package */void addConstraintForField(String fieldId, String type, String message, String messageId) { FormField field = fields.get(fieldId); field.setConstraintType(type); field.setConstraintMessage(message); field.setConstraintMessageId(messageId); } /* package */void addModelOverrides(String name, String value) { StringPair modelOverride = new StringPair(name, value); //TODO Consider using a different collection type here. for (Iterator iter = modelOverrides.iterator(); iter.hasNext(); ) { if (iter.next().getName().equals(name)) { iter.remove(); } } modelOverrides.add(modelOverride); } private String findFirstMatchingTemplate(List templates, Map templatesToRoles, List currentRoles) { // If there is no current role, we can only return templates that require no role. if (currentRoles == null || currentRoles.isEmpty()) { for (String template : templates) { String requiredRolesForThisTemplate = templatesToRoles.get(template); if (requiredRolesForThisTemplate.trim().length() == 0) { return template; } } return null; } // Here there is at least one current role. for (String template : templates) { String requiredRolesForThisTemplate = templatesToRoles.get(template); for (String role : currentRoles) { if (requiredRolesForThisTemplate.contains(role)) { return template; } } } return null; } /** * * @param m * @param roles a list of roles, can be an empty list or null. * @return null if there is no template available for the specified role(s). */ public String getFormTemplate(Mode m, List roles) { switch (m) { case CREATE: return findFirstMatchingTemplate(createTemplates, rolesForCreateTemplates, roles); case EDIT: return findFirstMatchingTemplate(editTemplates, rolesForEditTemplates, roles); case VIEW: return findFirstMatchingTemplate(viewTemplates, rolesForViewTemplates, roles); default: return null; } } /** * This method checks whether the specified field is visible in the specified mode. * The algorithm for determining visibility works as follows *
    *
  • If there is no field-visibility configuration (show or hide tags) then * all fields are visible in all modes.
  • *
  • If there are one or more hide tags then the specified fields will be hidden * in the specified modes. All other fields remain visible as before.
  • *
  • However, as soon as a single show tag appears in the config xml, this is * taken as a signal that all field visibility is to be manually configured. * At that point, all fields default to hidden and only those explicitly * configured to be shown (with a show tag) will actually be shown.
  • *
  • Show and hide rules will be applied in sequence with later rules * invalidating previous rules.
  • *
  • Show or hide rules which only apply for specified modes have an implicit * element. e.g. would show the name field * in view mode and by implication, hide it in other modes.
  • *
* @param fieldId * @param m * @return */ public boolean isFieldVisible(String fieldId, Mode m) { if (visibilityRules.isEmpty()) { return true; } else { boolean listContainsAtLeastOneShow = false; // true means show, false means hide, null means 'no answer'. Boolean latestInstructionToShow = null; for (FieldVisibilityRule instruc : visibilityRules) { if (instruc.isShow()) { listContainsAtLeastOneShow = true; } if (instruc.getFieldId().equals(fieldId)) { // This matters if (instruc.getListOfModes().contains(m)) { latestInstructionToShow = instruc.isShow() ? Boolean.TRUE : Boolean.FALSE; } else { latestInstructionToShow = instruc.isShow() ? Boolean.FALSE : Boolean.TRUE; } } } if (latestInstructionToShow == null) { // We really on the 'default' behaviour return !listContainsAtLeastOneShow; } else { return latestInstructionToShow.booleanValue(); } } } private Map getFieldsVisibleInMode(Mode mode) { Map result = new HashMap(); for (String fieldId : this.fields.keySet()) { if (this.isFieldVisible(fieldId, mode)) { result.put(fieldId, fields.get(fieldId)); } } return result; } /** * This inner class represents a <set> element within a <form> * element. * * @author Neil McErlean. */ public static class FormSet { private final String setId; private final String parentId; private final String appearance; public FormSet(String setId, String parentId, String appearance) { this.setId = setId; this.parentId = parentId; this.appearance = appearance; } public String getSetId() { return setId; } public String getParentId() { return parentId; } public String getAppearance() { return appearance; } @Override public boolean equals(Object otherObj) { if (otherObj == null || !otherObj.getClass().equals(this.getClass())) { return false; } FormSet otherSet = (FormSet) otherObj; return this.setId.equals(otherSet.setId) && this.parentId.equals(otherSet.parentId) && this.appearance.equals(otherSet.appearance); } @Override public int hashCode() { return setId.hashCode() + 7 * parentId.hashCode() + 13 * appearance.hashCode(); } } public static class FormField { //TODO I think I'll have to add explicit methods for all params e.g. getRequiredRoles():String // Maps of attributes are not enough as they leave too much work up to the client. private final Map attributes; private String template; private final List controlParams = new ArrayList(); private String constraintType; private String constraintMessage; private String constraintMessageId; public FormField(Map attributes) { this.attributes = attributes; } public void setTemplate(String template) { this.template = template; } public void setConstraintType(String constraintType) { this.constraintType = constraintType; } public void setConstraintMessage(String constraintMessage) { this.constraintMessage = constraintMessage; } public void setConstraintMessageId(String constraintMessageId) { this.constraintMessageId = constraintMessageId; } public void addControlParam(StringPair nameValue) { this.controlParams.add(nameValue); } public Map getAttributes() { return Collections.unmodifiableMap(attributes); } public String getTemplate() { return template; } public List getControlParams() { return Collections.unmodifiableList(controlParams); } public String getConstraintType() { return constraintType; } public String getConstraintMessage() { return constraintMessage; } public String getConstraintMessageId() { return constraintMessageId; } } public static class FieldVisibilityRule { private final boolean show; private final String fieldId; private final List forModes; public FieldVisibilityRule(String showOrHide, String fieldId, String modesString) { if (showOrHide.equals("show")) { this.show = true; } else { this.show = false; } this.fieldId = fieldId; this.forModes = new ArrayList(); if (modesString == null) { forModes.addAll(Arrays.asList(Mode.values())); } else { StringTokenizer st = new StringTokenizer(modesString, ","); while (st.hasMoreTokens()) { String nextToken = st.nextToken().trim(); Mode nextMode = Mode.fromString(nextToken); forModes.add(nextMode); } } } public boolean isShow() { return show; } public boolean isHide() { return !isShow(); } public String getFieldId() { return fieldId; } public List getListOfModes() { return Collections.unmodifiableList(forModes); } public String getModes() { StringBuilder result = new StringBuilder(); for (Iterator iter = forModes.iterator(); iter.hasNext(); ) { result.append(iter.next()); if (iter.hasNext()) { result.append(", "); } } return result.toString(); } public String toString() { StringBuilder result = new StringBuilder(); result.append(show ? "show" : "hide"); result.append(" ").append(fieldId).append(" ").append(forModes); return result.toString(); } @Override public boolean equals(Object otherObj) { if (otherObj == null || !otherObj.getClass().equals(this.getClass())) { return false; } FieldVisibilityRule otherFVI = (FieldVisibilityRule) otherObj; return this.show == otherFVI.show && this.fieldId.equals(otherFVI.fieldId) && this.forModes.equals(otherFVI.forModes); } @Override public int hashCode() { return show ? 1 : 0 + 7 * fieldId.hashCode() + 13 * forModes.hashCode(); } } }