/*
* (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:
* Nuxeo - initial API and implementation
*
* $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.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import javax.el.ELException;
import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.nuxeo.ecm.core.api.ClientRuntimeException;
import org.nuxeo.ecm.platform.ui.web.component.ResettableComponent;
import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
import com.sun.faces.facelets.component.UIRepeat;
/**
* DOCUMENT ME.
* <p>
* Refactor me and it's christmas.
*
* @author <a href="mailto:glefter@nuxeo.com">George Lefter</a>
*/
public class ChainSelect extends UIInput implements ResettableComponent {
public static final String COMPONENT_TYPE = "nxdirectory.chainSelect";
public static final String COMPONENT_FAMILY = "nxdirectory.chainSelect";
public static final String DEFAULT_KEY_SEPARATOR = "/";
public static final String DEFAULT_PARENT_KEY = null;
private static final Log log = LogFactory.getLog(ChainSelect.class);
// Direct access from ChainSelectStatus
Map<Integer, NestedChainSelectComponentInfo> compInfos = new HashMap<Integer, NestedChainSelectComponentInfo>();
/**
* The keys of the selected items in chain controls.
*/
private List<String> keyList = new ArrayList<String>();
private String onchange;
private Map<String, DirectorySelectItem>[] optionList;
private Integer size;
private boolean localize;
private boolean multiSelect = false;
private boolean allowRootSelection = false;
private boolean allowBranchSelection = false;
private boolean qualifiedParentKeys = false;
private Selection[] selections;
// XXX AT: this attribute is useless, value is already there to store that
private Selection[] componentValue;
private Boolean displayValueOnly;
private String displayValueOnlyStyle;
private String displayValueOnlyStyleClass;
private String cssStyle;
private String cssStyleClass;
private boolean multiParentSelect = false;
/**
* The index of the last selection box that was selected.
*/
private int lastSelectedComponentIndex;
/**
* This field is used to separate the levels of on hierarchical
* vocabulary.This way all parents of a record will be separated through
* this field.
*/
private String keySeparator;
/**
* Value used to filter on parent key when searching for a hierarchical
* directory roots.
* <p>
* If not set, will use null.
*/
protected String defaultRootKey;
/**
* New attribute to handle bad behaviour on ajax re-render, forcing local
* cache refresh
*
* @since 5.6
*/
protected Boolean resetCacheOnUpdate;
public boolean isAllowBranchSelection() {
return allowBranchSelection;
}
public void setAllowBranchSelection(boolean allowBranchSelection) {
this.allowBranchSelection = allowBranchSelection;
}
public boolean isAllowRootSelection() {
return allowRootSelection;
}
public void setAllowRootSelection(boolean allowRootSelection) {
this.allowRootSelection = allowRootSelection;
}
@Override
public String getFamily() {
return COMPONENT_FAMILY;
}
@Override
public String getRendererType() {
return null;
}
@Override
@SuppressWarnings("unchecked")
public void restoreState(FacesContext context, Object state) {
Object[] values = (Object[]) state;
super.restoreState(context, values[0]);
componentValue = (Selection[]) values[1];
optionList = (Map<String, DirectorySelectItem>[]) values[2];
localize = (Boolean) values[3];
size = (Integer) values[4];
multiSelect = (Boolean) values[5];
allowRootSelection = (Boolean) values[6];
allowBranchSelection = (Boolean) values[7];
selections = (Selection[]) values[8];
qualifiedParentKeys = (Boolean) values[9];
displayValueOnly = (Boolean) values[10];
displayValueOnlyStyle = (String) values[11];
displayValueOnlyStyleClass = (String) values[12];
multiParentSelect = (Boolean) values[13];
cssStyle = (String) values[14];
cssStyleClass = (String) values[15];
keySeparator = (String) values[16];
lastSelectedComponentIndex = (Integer) values[17];
compInfos = (Map<Integer, NestedChainSelectComponentInfo>) values[18];
keyList = (List<String>) values[19];
onchange = (String) values[20];
defaultRootKey = (String) values[21];
resetCacheOnUpdate = (Boolean) values[22];
}
@Override
public Object saveState(FacesContext arg0) {
Object[] values = new Object[23];
values[0] = super.saveState(arg0);
values[1] = componentValue;
values[2] = optionList;
values[3] = localize;
values[4] = size;
values[5] = multiSelect;
values[6] = allowRootSelection;
values[7] = allowBranchSelection;
values[8] = selections;
values[9] = qualifiedParentKeys;
values[10] = displayValueOnly;
values[11] = displayValueOnlyStyle;
values[12] = displayValueOnlyStyleClass;
values[13] = multiParentSelect;
values[14] = cssStyle;
values[15] = cssStyleClass;
values[16] = keySeparator;
values[17] = lastSelectedComponentIndex;
values[18] = compInfos;
values[19] = keyList;
values[20] = onchange;
values[21] = defaultRootKey;
values[22] = resetCacheOnUpdate;
return values;
}
public List<String> getSelectionKeyList() {
return keyList;
}
public void addToSelectionKeyList(String key) {
keyList.add(key);
}
@Override
public void decode(FacesContext context) {
if (getDisplayValueOnly()) {
return;
}
setValid(true);
rebuildOptions();
if (!multiParentSelect) {
componentValue = selections;
String[] value = encodeValue(componentValue);
if (!multiSelect) {
setSubmittedValue(value[0]);
} else {
if (!multiParentSelect) {
// remove the "" entry from the submitted value
List<String> list = new ArrayList<String>(
Arrays.asList(value));
list.remove("");
value = list.toArray(new String[list.size()]);
}
setSubmittedValue(value);
}
} else {
String[] value = encodeValue(componentValue);
setSubmittedValue(value);
}
// identify the repeat child tag that displays
// current added selections to dynamically set
// it's iterable value
List<UIComponent> children = getChildren();
for (UIComponent child : children) {
if (!(child instanceof UIRepeat)) {
continue;
}
UIRepeat component = (UIRepeat) child;
if (component.getId().equals("current_selections")) {
component.setValue(componentValue);
}
}
}
public static String format(Object o) {
if (o == null) {
return "NULL";
}
if (o instanceof String[]) {
return formatAr((String[]) o);
} else if (o instanceof String) {
return (String) o;
} else {
return o.getClass().getName();
}
}
public static String formatAr(String[] ar) {
if (ar == null) {
return "NULL";
}
if (ar.length == 0) {
return "[]";
} else {
return '[' + StringUtils.join(ar, ", ") + ']';
}
}
@Override
public void encodeBegin(FacesContext context) throws IOException {
init();
rebuildOptions();
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this);
if (cssStyle != null) {
writer.writeAttribute("style", cssStyle, "style");
}
if (cssStyleClass != null) {
writer.writeAttribute("class", cssStyleClass, "class");
}
writer.writeAttribute("id", getClientId(context), "id");
super.encodeBegin(context);
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.endElement("div");
}
public Object getProperty(String name) {
ValueExpression ve = getValueExpression(name);
if (ve != null) {
try {
return ve.getValue(getFacesContext().getELContext());
} catch (ELException e) {
throw new FacesException(e);
}
} else {
Map<String, Object> attrMap = getAttributes();
return attrMap.get(name);
}
}
public String getStringProperty(String name, String defaultValue) {
String value = (String) getProperty(name);
return value != null ? value : defaultValue;
}
public Boolean getBooleanProperty(String name, boolean defaultValue) {
Boolean value = (Boolean) getProperty(name);
return value != null ? value : Boolean.valueOf(defaultValue);
}
public Boolean getLocalize() {
return localize;
}
public void setLocalize(Boolean localize) {
this.localize = localize;
}
public String getCssStyle() {
return cssStyle;
}
public void setCssStyle(String cssStyle) {
this.cssStyle = cssStyle;
}
public String getCssStyleClass() {
return cssStyleClass;
}
public void setCSsStyleClass(String cssStyleClass) {
this.cssStyleClass = cssStyleClass;
}
public String getOnchange() {
if (onchange != null) {
return onchange;
}
ValueExpression ve = getValueExpression("onchange");
if (ve != null) {
try {
return (String) ve.getValue(getFacesContext().getELContext());
} catch (ELException e) {
throw new FacesException(e);
}
}
return null;
}
public void setOnchange(String onchange) {
this.onchange = onchange;
}
public Selection getSelection(int i) {
if (selections == null) {
throw new ClientRuntimeException("ChainSelect is mis-behaving, "
+ "it's probable you're experiencing issue NXP-5762");
}
return selections[i];
}
public void setSelections(Selection[] sels) {
selections = sels;
}
public Integer getSize() {
return size;
}
@SuppressWarnings("unchecked")
public void setSize(Integer size) {
optionList = new LinkedHashMap[size];
this.size = size;
}
public Map<String, DirectorySelectItem> getOptions(int index) {
return optionList[index];
}
public void setOptions(int index, Map<String, DirectorySelectItem> opts) {
optionList[index] = opts;
}
/**
* If the user changes selection for position k, all options for n>k will
* be reset. We only have to rebuild options for position k+1.
*/
public void rebuildOptions() {
// for (int i = 0; i < size; i++) {
// if (optionList[i] != null) {
// continue;
// }
// if (i == 0
// || (selections.length != 0 && selections[0].getColumnValue(i - 1) !=
// null)) {
// rebuildOptions(i);
// }
// }
}
private void rebuildOptions(int index) {
ChainSelectListboxComponent component = getComponent(index);
if (component == null) {
return;
}
// LinkedHashMap<String, DirectorySelectItem> options =
// component.rebuildOptions();
// optionList[index] = options;
}
public ChainSelectListboxComponent getComponent(int i) {
List<UIComponent> children = getChildren();
for (UIComponent child : children) {
if (!(child instanceof ChainSelectListboxComponent)) {
continue;
}
ChainSelectListboxComponent component = (ChainSelectListboxComponent) child;
Integer index = component.getIndex();
if (i == index) {
return component;
}
}
// try with multiListbox
for (UIComponent child : children) {
if (child instanceof ChainSelectMultiListboxComponent) {
ChainSelectMultiListboxComponent mcomp = (ChainSelectMultiListboxComponent) child;
ChainSelectListboxComponent[] sComps = mcomp.createSingleComponents();
if (i < sComps.length) {
return sComps[i];
} else {
log.error("xXXXXXXXXXXXXXXXXXXXX " + i);
}
}
// XXX : remove this section
if (child instanceof UIRepeat) {
List<UIComponent> children2 = child.getChildren();
for (UIComponent child2 : children2) {
if (!(child2 instanceof ChainSelectListboxComponent)) {
continue;
}
ChainSelectListboxComponent component = (ChainSelectListboxComponent) child2;
Integer index = component.getIndex();
if (i == index) {
return component;
}
}
}
}
return null;
}
public boolean isMultiSelect() {
return multiSelect;
}
public void setMultiSelect(boolean multiSelect) {
this.multiSelect = multiSelect;
}
public Selection[] getSelections() {
return selections;
}
public boolean isQualifiedParentKeys() {
return qualifiedParentKeys;
}
public void setQualifiedParentKeys(boolean fullyQualifiedParentKey) {
qualifiedParentKeys = fullyQualifiedParentKey;
}
public Boolean getDisplayValueOnly() {
if (displayValueOnly != null) {
return displayValueOnly;
}
return false;
}
public void setDisplayValueOnly(Boolean displayValueOnly) {
this.displayValueOnly = displayValueOnly;
}
public String getDisplayValueOnlyStyle() {
return displayValueOnlyStyle;
}
public void setDisplayValueOnlyStyle(String displayValueOnlyStyle) {
this.displayValueOnlyStyle = displayValueOnlyStyle;
}
public String getDisplayValueOnlyStyleClass() {
return displayValueOnlyStyleClass;
}
public void setDisplayValueOnlyStyleClass(String displayValueOnlyStyleClass) {
this.displayValueOnlyStyleClass = displayValueOnlyStyleClass;
}
public boolean getMultiParentSelect() {
return multiParentSelect;
}
public void setMultiParentSelect(boolean multiParentSelect) {
this.multiParentSelect = multiParentSelect;
if (multiParentSelect) {
multiSelect = true;
}
}
public String[] encodeValue(Selection[] selections) {
String[] keys = new String[selections.length];
for (int i = 0; i < selections.length; i++) {
keys[i] = selections[i].getValue(keySeparator);
}
return keys;
}
private void init() {
if (componentValue == null) {
Object value = getValue();
if (value == null) {
componentValue = new Selection[0];
selections = new Selection[1];
selections[0] = new Selection(new DirectorySelectItem[0]);
return;
}
String[] rows;
if (multiSelect) {
if (value instanceof String[]) {
rows = (String[]) value;
} else if (value instanceof Object[]) {
Object[] values = (Object[]) value;
rows = new String[values.length];
for (int i = 0; i < rows.length; i++) {
rows[i] = String.valueOf(values[i]);
}
} else if (value instanceof List) {
List valueList = (List) value;
rows = new String[valueList.size()];
for (int i = 0; i < rows.length; i++) {
rows[i] = String.valueOf(valueList.get(i));
}
} else {
rows = new String[] {};
}
} else {
rows = new String[] { (String) value };
}
componentValue = new Selection[rows.length];
for (int i = 0; i < rows.length; i++) {
String[] columns = StringUtils.split(rows[i], getKeySeparator());
componentValue[i] = createSelection(columns);
}
if (multiParentSelect) {
selections = new Selection[1];
selections[0] = new Selection(new DirectorySelectItem[0]);
} else {
selections = componentValue;
}
}
}
public Selection createSelection(List<String> columns) {
return createSelection(columns.toArray(new String[columns.size()]));
}
public Selection createSelection(String[] columns) {
List<String> keyList = new ArrayList<String>();
List<DirectorySelectItem> itemList = new ArrayList<DirectorySelectItem>();
for (int i = 0; i < columns.length; i++) {
String id = columns[i];
String directoryName;
VocabularyEntryList directoryValues;
boolean displayObsoleteEntries;
NestedChainSelectComponentInfo compInfo = compInfos.get(i);
if (compInfo != null) {
directoryName = compInfo.directoryName;
directoryValues = compInfo.directoryValues;
displayObsoleteEntries = compInfo.displayObsoleteEntries;
} else {
// fallback to the old solution
ChainSelectListboxComponent comp = getComponent(i);
directoryName = comp.getStringProperty("directoryName", null);
directoryValues = comp.getDirectoryValues();
displayObsoleteEntries = comp.getBooleanProperty(
"displayObsoleteEntries", false);
}
Map<String, Serializable> filter = new HashMap<String, Serializable>();
filter.put("id", id);
if (i == 0) {
if (directoryName != null) {
if (DirectoryHelper.instance().hasParentColumn(
directoryName)) {
filter.put("parent", getDefaultRootKey());
}
}
} else {
String parentId;
if (qualifiedParentKeys) {
parentId = StringUtils.join(keyList.iterator(),
getKeySeparator());
} else {
parentId = columns[i - 1];
}
filter.put("parent", parentId);
}
keyList.add(id);
if (!displayObsoleteEntries) {
filter.put("obsolete", 0);
}
List<DirectorySelectItem> items = null;
if (directoryName != null) {
items = DirectoryHelper.instance().getSelectItems(
directoryName, filter);
} else {
items = DirectoryHelper.getSelectItems(directoryValues, filter);
}
if (items == null) {
throw new IllegalStateException(String.format(
"Item not found: directoryName=%s, filter=%s",
directoryName, filter));
}
if (items.isEmpty()) {
log.warn(String.format("No selection for dir %s ",
directoryName));
return new Selection(
itemList.toArray(new DirectorySelectItem[0]));
} else {
if (items.size() != 1) {
log.warn(String.format(
"Too many items (%s) found: directoryName=%s, filter=%s",
Integer.toString(items.size()), directoryName,
filter));
}
itemList.add(items.get(0));
}
}
return new Selection(
itemList.toArray(new DirectorySelectItem[columns.length]));
}
public Selection[] getComponentValue() {
return componentValue;
}
public void setComponentValue(Selection[] componentValue) {
this.componentValue = componentValue;
}
public int getLastSelectedComponentIndex() {
return lastSelectedComponentIndex;
}
public void setLastSelectedComponentIndex(int index) {
lastSelectedComponentIndex = index;
}
/**
* This structure is needed to keep data for dynamically generated
* components.
*/
static class NestedChainSelectComponentInfo {
String directoryName;
VocabularyEntryList directoryValues;
boolean displayObsoleteEntries;
boolean localize;
String display;
}
public void setCompAtIndex(int index, ChainSelectListboxComponent comp) {
NestedChainSelectComponentInfo compInfo = new NestedChainSelectComponentInfo();
compInfo.directoryName = comp.getStringProperty("directoryName", null);
compInfo.directoryValues = comp.getDirectoryValues();
compInfo.displayObsoleteEntries = comp.getBooleanProperty(
"displayObsoleteEntries", false);
compInfo.localize = comp.getBooleanProperty("localize", false);
compInfo.display = comp.getDisplay();
compInfos.put(index, compInfo);
}
public String getKeySeparator() {
return keySeparator != null ? keySeparator : DEFAULT_KEY_SEPARATOR;
}
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;
}
@Override
public void validateValue(FacesContext context, Object newValue) {
super.validateValue(context, newValue);
if (!isValid()) {
return;
}
if (newValue instanceof String) {
String newValueStr = (String) newValue;
if (StringUtils.isEmpty(newValueStr)) {
return;
}
String[] rows = StringUtils.split(newValueStr, getKeySeparator());
boolean allowBranchSelection = Boolean.TRUE.equals(getBooleanProperty(
"allowBranchSelection", false));
if (!allowBranchSelection && rows.length != size) {
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);
}
}
}
/**
* @since 5.6
*/
public Boolean getResetCacheOnUpdate() {
if (resetCacheOnUpdate != null) {
return resetCacheOnUpdate;
}
ValueExpression ve = getValueExpression("resetCacheOnUpdate");
if (ve != null) {
try {
return Boolean.valueOf(Boolean.TRUE.equals(ve.getValue(getFacesContext().getELContext())));
} catch (ELException e) {
throw new FacesException(e);
}
} else {
// default value
return Boolean.FALSE;
}
}
/**
* @since 5.6
*/
public void setResetCacheOnUpdate(Boolean resetCacheOnUpdate) {
this.resetCacheOnUpdate = resetCacheOnUpdate;
}
/**
* Override update method to reset cached value and ensure good re-render
* in ajax
*
* @since 5.6
*/
public void processUpdates(FacesContext context) {
super.processUpdates(context);
if (Boolean.TRUE.equals(getResetCacheOnUpdate()) && isValid()) {
componentValue = new Selection[0];
}
}
/**
* Reset the chain select cached model
*
* @since 5.7
*/
@Override
public void resetCachedModel() {
if (getValueExpression("value") != null) {
setValue(null);
setLocalValueSet(false);
}
setSubmittedValue(null);
setComponentValue(null);
}
}