/* * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Lesser General Public License * (LGPL) version 2.1 which accompanies this distribution, and is available at * http://www.gnu.org/licenses/lgpl.html * * This library 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 * Lesser General Public License for more details. * * Contributors: * George Lefter * * $Id$ */ package org.nuxeo.ecm.platform.ui.web.directory; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import javax.el.ValueExpression; import javax.faces.application.FacesMessage; import javax.faces.component.NamingContainer; import javax.faces.component.UIComponent; import javax.faces.component.UIInput; import javax.faces.component.UISelectItem; import javax.faces.component.html.HtmlSelectOneListbox; import javax.faces.context.FacesContext; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.nuxeo.ecm.core.api.ClientException; import org.nuxeo.ecm.core.api.DocumentModel; import org.nuxeo.ecm.core.api.DocumentModelList; import org.nuxeo.ecm.directory.Session; import org.nuxeo.ecm.directory.api.DirectoryService; import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils; /** * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a> * */ public abstract class ChainSelectBase extends UIInput implements NamingContainer { private static final Log log = LogFactory.getLog(ChainSelect.class); protected static final String DISPLAY_LABEL = "label"; protected static final String DISPLAY_ID = "id"; protected static final String DISPLAY_IDLABEL = "idAndLabel"; protected static final String DEFAULT_KEYSEPARATOR = "/"; protected static final String SELECT = "selectListbox"; public static final String VOCABULARY_SCHEMA = "vocabulary"; /** Directory with a parent column. */ public static final String XVOCABULARY_SCHEMA = "xvocabulary"; protected String directoryNames; protected String keySeparator = DEFAULT_KEYSEPARATOR; protected boolean qualifiedParentKeys = false; protected int depth; protected String display = DISPLAY_LABEL; protected boolean translate; protected boolean showObsolete; protected String style; protected String styleClass; protected int listboxSize; protected boolean allowBranchSelection; protected String reRender; private boolean displayValueOnly; protected String defaultRootKey; protected Map<String, String[]> selectionMap = new HashMap<String, String[]>(); protected ChainSelectBase() { HtmlSelectOneListbox select = new HtmlSelectOneListbox(); getFacets().put(SELECT, select); } public String getDirectory(int level) { String[] directories = getDirectories(); if (isRecursive()) { return directories[0]; } else { if (level < directories.length) { return directories[level]; } else { return null; } } } @Override @SuppressWarnings("unchecked") public void restoreState(FacesContext context, Object state) { Object[] values = (Object[]) state; super.restoreState(context, values[0]); ChainSelectState chainState = (ChainSelectState) values[1]; selectionMap = (Map<String, String[]>) values[2]; depth = chainState.getDepth(); display = chainState.getDisplay(); directoryNames = chainState.getDirectoryNames(); keySeparator = chainState.getKeySeparator(); qualifiedParentKeys = chainState.getQualifiedParentKeys(); showObsolete = chainState.getShowObsolete(); listboxSize = chainState.getListboxSize(); style = chainState.getStyle(); styleClass = chainState.getStyleClass(); translate = chainState.getTranslate(); allowBranchSelection = chainState.getAllowBranchSelection(); reRender = chainState.getReRender(); displayValueOnly = chainState.getDisplayValueOnly(); defaultRootKey = chainState.getDefaultRootKey(); } @Override public Object saveState(FacesContext context) { ChainSelectState chainState = new ChainSelectState(); chainState.setDepth(depth); chainState.setDisplay(display); chainState.setDirectoryNames(directoryNames); chainState.setKeySeparator(keySeparator); chainState.setQualifiedParentKeys(qualifiedParentKeys); chainState.setShowObsolete(showObsolete); chainState.setStyle(style); chainState.setStyleClass(styleClass); chainState.setTranslate(translate); chainState.setListboxSize(listboxSize); chainState.setAllowBranchSelection(allowBranchSelection); chainState.setReRender(reRender); chainState.setDisplayValueOnly(displayValueOnly); chainState.setDefaultRootKey(defaultRootKey); Object[] values = new Object[3]; values[0] = super.saveState(context); values[1] = chainState; values[2] = selectionMap; return values; } protected HtmlSelectOneListbox getListbox(FacesContext context, int level) { String componentId = getComponentId(level); HtmlSelectOneListbox listbox = new HtmlSelectOneListbox(); getChildren().add(listbox); listbox.setId(componentId); listbox.getChildren().clear(); String reRender = getReRender(); if (reRender == null) { reRender = getId(); } UIComponent support = context.getApplication().createComponent( "org.ajax4jsf.ajax.Support"); support.getAttributes().put("event", "onchange"); support.getAttributes().put("reRender", reRender); support.getAttributes().put("immediate", Boolean.TRUE); support.getAttributes().put("id", componentId + "_a4jSupport"); listbox.getChildren().add(support); return listbox; } protected void encodeListbox(FacesContext context, int level, String[] selectedKeys) throws IOException { HtmlSelectOneListbox listbox = getListbox(context, level); listbox.setSize(getListboxSize()); List<DirectoryEntry> items; if (level <= selectedKeys.length) { items = getDirectoryEntries(level, selectedKeys); } else { items = new ArrayList<DirectoryEntry>(); } UISelectItem emptyItem = new UISelectItem(); emptyItem.setItemLabel(ComponentUtils.translate(context, "label.vocabulary.selectValue")); emptyItem.setItemValue(""); emptyItem.setId(context.getViewRoot().createUniqueId()); listbox.getChildren().add(emptyItem); for (DirectoryEntry child : items) { UISelectItem selectItem = new UISelectItem(); String itemValue = child.getId(); String itemLabel = child.getLabel(); itemLabel = computeItemLabel(context, itemValue, itemLabel); selectItem.setItemValue(itemValue); selectItem.setItemLabel(itemLabel); selectItem.setId(context.getViewRoot().createUniqueId()); listbox.getChildren().add(selectItem); } if (level < selectedKeys.length) { listbox.setValue(selectedKeys[level]); } ComponentUtils.encodeComponent(context, listbox); } public String[] getDirectories() { return StringUtils.split(getDirectoryNames(), ","); } public boolean isRecursive() { return getDirectories().length != getDepth(); } /** * Computes the items that should be displayed for the nth listbox, * depending on the options that have been selected in the previous ones. * * @param level the index of the listbox for which to compute the items * @param selectedKeys the keys for the items selected on the previous * levels * @return a list of directory items */ public List<DirectoryEntry> getDirectoryEntries(int level, String[] selectedKeys) { assert level <= selectedKeys.length; List<DirectoryEntry> result = new ArrayList<DirectoryEntry>(); String directoryName = getDirectory(level); DirectoryService service = DirectoryHelper.getDirectoryService(); Session session = null; try { String schema = service.getDirectorySchema(directoryName); session = service.open(directoryName); Map<String, Serializable> filter = new HashMap<String, Serializable>(); if (level == 0) { if (schema.equals(XVOCABULARY_SCHEMA)) { filter.put("parent", null); } } else { if (getQualifiedParentKeys()) { Iterator<String> iter = Arrays.asList(selectedKeys).subList( 0, level).iterator(); String fullPath = StringUtils.join(iter, getKeySeparator()); filter.put("parent", fullPath); } else { filter.put("parent", selectedKeys[level - 1]); } } if (!getShowObsolete()) { filter.put("obsolete", "0"); } Set<String> emptySet = Collections.emptySet(); Map<String, String> orderBy = new LinkedHashMap<String, String>(); // adding sorting suport if (schema.equals(VOCABULARY_SCHEMA) || schema.equals(XVOCABULARY_SCHEMA)) { orderBy.put("ordering", "asc"); orderBy.put("id", "asc"); } DocumentModelList entries = session.query(filter, emptySet, orderBy); for (DocumentModel entry : entries) { DirectoryEntry newNode = new DirectoryEntry(schema, entry); result.add(newNode); } } catch (ClientException e) { throw new RuntimeException("failed to query directory: " + directoryName, e); } finally { try { session.close(); } catch (Exception e) { } } return result; } /** * Resolves a list of keys (a selection) to a list of coresponding directory * items. Example: [a, b, c] is resolved to [getNode(a), getNode(b), * getNode(c)] * * @param keys * @return */ public List<DirectoryEntry> resolveKeys(String[] keys) { List<DirectoryEntry> result = new ArrayList<DirectoryEntry>(); DirectoryService service = DirectoryHelper.getDirectoryService(); Session session = null; for (int level = 0; level < keys.length; level++) { try { String directoryName = getDirectory(level); String schema = service.getDirectorySchema(directoryName); session = service.open(directoryName); Map<String, Serializable> filter = new HashMap<String, Serializable>(); if (level == 0) { if (schema.equals(XVOCABULARY_SCHEMA)) { filter.put("parent", null); } } else { if (getQualifiedParentKeys()) { Iterator<String> iter = Arrays.asList(keys).subList(0, level).iterator(); String fullPath = StringUtils.join(iter, getKeySeparator()); filter.put("parent", fullPath); } else { filter.put("parent", keys[level - 1]); } } filter.put("id", keys[level]); DocumentModelList entries = session.query(filter); if (entries == null || entries.isEmpty()) { log.warn("keyList could not be resolved at level " + level); break; } DirectoryEntry node = new DirectoryEntry(schema, entries.get(0)); result.add(node); } catch (ClientException e) { throw new RuntimeException("failed to lookup keys: ", e); } finally { closeSession(session); } } return result; } private static void closeSession(Session session) { try { session.close(); } catch (Exception e) { } } public String getComponentId(int level) { String directory = getDirectory(level); if (isRecursive()) { return directory + '_' + level; } else { return directory + '_' + level; } } public String getKeySeparator() { ValueExpression ve = getValueExpression("keySeparator"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return keySeparator; } } public void setKeySeparator(String keySeparator) { this.keySeparator = keySeparator; } public String getDefaultRootKey() { ValueExpression ve = getValueExpression("defaultRootKey"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return defaultRootKey; } } public void setDefaultRootKey(String defaultRootKey) { this.defaultRootKey = defaultRootKey; } public boolean getDisplayValueOnly() { ValueExpression ve = getValueExpression("displayValueOnly"); if (ve != null) { Boolean value = (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); return value == null ? false : value; } else { return displayValueOnly; } } public void setDisplayValueOnly(boolean displayValueOnly) { this.displayValueOnly = displayValueOnly; } public int getListboxSize() { ValueExpression ve = getValueExpression("listboxSize"); if (ve != null) { return (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return listboxSize; } } public void setListboxSize(int listboxSize) { this.listboxSize = listboxSize; } public String getDisplay() { ValueExpression ve = getValueExpression("display"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return display != null ? display : DISPLAY_LABEL; } } public void setDisplay(String display) { this.display = display; } public boolean getQualifiedParentKeys() { ValueExpression ve = getValueExpression("qualifiedParentKeys"); if (ve != null) { return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return qualifiedParentKeys; } } public String getDirectoryNames() { ValueExpression ve = getValueExpression("directoryNames"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return directoryNames; } } public void setDirectoryNames(String directoryNames) { this.directoryNames = directoryNames; } public int getDepth() { int myDepth; ValueExpression ve = getValueExpression("depth"); if (ve != null) { myDepth = (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { myDepth = depth; } return myDepth != 0 ? myDepth : getDirectories().length; } public void setDepth(int depth) { this.depth = depth; } public String getStyle() { ValueExpression ve = getValueExpression("style"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return style; } } public void setStyle(String style) { this.style = style; } public String getStyleClass() { ValueExpression ve = getValueExpression("styleClass"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return styleClass; } } public void setStyleClass(String styleClass) { this.styleClass = styleClass; } public boolean getTranslate() { ValueExpression ve_translate = getValueExpression("translate"); if (ve_translate != null) { return (Boolean) ve_translate.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return translate; } } public void setTranslate(boolean translate) { this.translate = translate; } public boolean getShowObsolete() { ValueExpression ve = getValueExpression("showObsolete"); if (ve != null) { return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return showObsolete; } } public void setShowObsolete(boolean showObsolete) { this.showObsolete = showObsolete; } public boolean getAllowBranchSelection() { ValueExpression ve = getValueExpression("allowBranchSelection"); if (ve != null) { return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return allowBranchSelection; } } public void setAllowBranchSelection(boolean allowBranchSelection) { this.allowBranchSelection = allowBranchSelection; } public String getReRender() { ValueExpression ve = getValueExpression("reRender"); if (ve != null) { return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext()); } else { return reRender; } } public void setReRender(String reRender) { this.reRender = reRender; } protected String[] getValueAsArray(String value) { if (value == null) { return new String[0]; } return StringUtils.split(value, getKeySeparator()); } protected String getValueAsString(String[] ar) { return StringUtils.join(ar, getKeySeparator()); } protected String computeItemLabel(FacesContext context, String id, String label) { boolean translate = getTranslate(); String display = getDisplay(); String translatedLabel = label; if (translate) { translatedLabel = ComponentUtils.translate(context, label); } if (DISPLAY_ID.equals(display)) { return id; } else if (DISPLAY_LABEL.equals(display)) { return translatedLabel; } else if (DISPLAY_IDLABEL.equals(display)) { return id + ' ' + translatedLabel; } else { throw new RuntimeException( "invalid value for attribute 'display'; should be either 'id', 'label' or 'idAndLabel'"); } } public abstract String[] getSelection(); protected void decodeSelection(FacesContext context) { List<String> selectedKeyList = new ArrayList<String>(); Map<String, String> parameters = context.getExternalContext().getRequestParameterMap(); String[] selection = getSelection(); for (int level = 0; level < getDepth(); level++) { String clientId = getClientId(context) + SEPARATOR_CHAR + getComponentId(level); String value = parameters.get(clientId); if (StringUtils.isEmpty(value)) { break; } selectedKeyList.add(value); // compare the old value with the new one; if they differ // the new list of keys is finished if (level >= selection.length) { break; } String oldValue = selection[level]; if (!value.equals(oldValue)) { break; } } selection = selectedKeyList.toArray(new String[selectedKeyList.size()]); setSelection(selection); } protected void setSelection(String[] selection) { String clientId = getClientId(FacesContext.getCurrentInstance()); selectionMap.put(clientId, selection); } protected boolean validateEntry(FacesContext context, String[] keys) { if (!getAllowBranchSelection() && keys.length != getDepth()) { String messageStr = ComponentUtils.translate(context, "label.chainSelect.incomplete_selection"); FacesMessage message = new FacesMessage( FacesMessage.SEVERITY_ERROR, messageStr, messageStr); context.addMessage(getClientId(context), message); setValid(false); return false; } else { return true; } } }