001/*
002 * (C) Copyright 2006-2007 Nuxeo SAS (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     Nuxeo - initial API and implementation
016 *
017 * $Id$
018 */
019
020package org.nuxeo.ecm.platform.ui.web.directory;
021
022import java.io.Serializable;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031
032import javax.faces.component.UIComponent;
033import javax.faces.component.UIInput;
034import javax.faces.component.behavior.ClientBehaviorHolder;
035import javax.faces.context.FacesContext;
036import javax.faces.el.ValueBinding;
037
038import org.apache.commons.lang.StringUtils;
039import org.apache.commons.logging.Log;
040import org.apache.commons.logging.LogFactory;
041import org.nuxeo.common.utils.i18n.I18NUtils;
042
043/**
044 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a>
045 */
046public class ChainSelectListboxComponent extends UIInput implements ClientBehaviorHolder {
047
048    public static final String COMPONENT_TYPE = "nxdirectory.chainSelectListbox";
049
050    public static final String COMPONENT_FAMILY = "nxdirectory.chainSelectListbox";
051
052    private static final Log log = LogFactory.getLog(ChainSelectListboxComponent.class);
053
054    public boolean ajaxUpdated = false;
055
056    private String directoryName;
057
058    private VocabularyEntryList directoryValues;
059
060    private Boolean displayIdAndLabel = false;
061
062    private Boolean displayObsoleteEntries = false;
063
064    private String onchange;
065
066    private int index;
067
068    private String displayIdAndLabelSeparator = " ";
069
070    private String cssStyle;
071
072    private String cssStyleClass;
073
074    private String size;
075
076    private Boolean localize = false;
077
078    private String displayValueOnlySeparator;
079
080    private String ordering;
081
082    private String display;
083
084    public ChainSelectListboxComponent() {
085        setRendererType(COMPONENT_TYPE);
086    }
087
088    @Override
089    public String getFamily() {
090        return COMPONENT_FAMILY;
091    }
092
093    public boolean isMultiSelect() {
094        ChainSelect chain = getChain();
095        if (chain != null) {
096            boolean isLastSelect = getIndex() == chain.getSize() - 1;
097            boolean isChainMultiSelect = chain.getBooleanProperty("multiSelect", false);
098            return isLastSelect && isChainMultiSelect;
099        }
100        return false;
101    }
102
103    public String getDisplayIdAndLabelSeparator() {
104        return displayIdAndLabelSeparator;
105    }
106
107    public void setDisplayIdAndLabelSeparator(String displayIdAndLabelSeparator) {
108        this.displayIdAndLabelSeparator = displayIdAndLabelSeparator;
109    }
110
111    @Override
112    public void restoreState(FacesContext context, Object state) {
113        Object[] values = (Object[]) state;
114        super.restoreState(context, values[0]);
115        index = (Integer) values[1];
116        displayIdAndLabel = (Boolean) values[2];
117        displayIdAndLabelSeparator = (String) values[3];
118        displayObsoleteEntries = (Boolean) values[4];
119        ajaxUpdated = (Boolean) values[5];
120        directoryName = (String) values[6];
121        localize = (Boolean) values[7];
122        displayValueOnlySeparator = (String) values[10];
123        onchange = (String) values[11];
124        cssStyle = (String) values[12];
125        cssStyleClass = (String) values[13];
126        size = (String) values[14];
127        directoryValues = (VocabularyEntryList) values[15];
128        ordering = (String) values[16];
129        display = (String) values[17];
130    }
131
132    @Override
133    public Object saveState(FacesContext context) {
134        Object[] values = new Object[18];
135        values[0] = super.saveState(context);
136        values[1] = getIndex();
137        values[2] = displayIdAndLabel;
138        values[3] = displayIdAndLabelSeparator;
139        values[4] = displayObsoleteEntries;
140        values[5] = ajaxUpdated;
141        values[6] = directoryName;
142        values[7] = localize;
143        values[10] = displayValueOnlySeparator;
144        values[11] = onchange;
145        values[12] = cssStyle;
146        values[13] = cssStyleClass;
147        values[14] = size;
148        values[15] = directoryValues;
149        values[16] = ordering;
150        values[17] = display;
151        return values;
152    }
153
154    public String getDirectoryName() {
155        ValueBinding vb = getValueBinding("directoryName");
156        if (vb != null) {
157            return (String) vb.getValue(FacesContext.getCurrentInstance());
158        } else {
159            return directoryName;
160        }
161    }
162
163    public void setDirectoryName(String newDirectory) {
164        directoryName = newDirectory;
165    }
166
167    public VocabularyEntryList getDirectoryValues() {
168        ValueBinding vb = getValueBinding("directoryValues");
169        if (vb != null) {
170            return (VocabularyEntryList) vb.getValue(FacesContext.getCurrentInstance());
171        } else {
172            return null;
173        }
174    }
175
176    public void setDirectoryValues(VocabularyEntryList directoryValues) {
177        this.directoryValues = directoryValues;
178    }
179
180    public Map<String, DirectorySelectItem> getOptions() {
181        index = getIndex();
182        if (index == 0 || getChain().getSelection(0).getColumnValue(index - 1) != null) {
183            return rebuildOptions();
184        }
185        return new HashMap<String, DirectorySelectItem>();
186    }
187
188    public Boolean getDisplayIdAndLabel() {
189        return displayIdAndLabel;
190    }
191
192    public void setDisplayIdAndLabel(Boolean displayIdAndLabel) {
193        this.displayIdAndLabel = displayIdAndLabel;
194    }
195
196    public Boolean getDisplayObsoleteEntries() {
197        return displayObsoleteEntries;
198    }
199
200    public void setDisplayObsoleteEntries(Boolean showObsolete) {
201        displayObsoleteEntries = showObsolete;
202    }
203
204    public void setOnchange(String onchange) {
205        this.onchange = onchange;
206    }
207
208    public String getOnchange() {
209        return onchange;
210    }
211
212    public ChainSelect getChain() {
213        UIComponent component = getParent();
214        while (component != null && !(component instanceof ChainSelect)) {
215            component = component.getParent();
216        }
217        return (ChainSelect) component;
218    }
219
220    public Object getProperty(String name) {
221        ValueBinding vb = getValueBinding(name);
222        if (vb != null) {
223            return vb.getValue(FacesContext.getCurrentInstance());
224        } else {
225            return getAttributes().get(name);
226        }
227    }
228
229    public String getStringProperty(String name, String defaultValue) {
230        String value = (String) getProperty(name);
231        return value != null ? value : defaultValue;
232    }
233
234    public Boolean getBooleanProperty(String name, Boolean defaultValue) {
235        Boolean value = (Boolean) getProperty(name);
236        return value != null ? value : defaultValue;
237    }
238
239    public String getDisplayValueOnlySeparator() {
240        return displayValueOnlySeparator;
241    }
242
243    public void setDisplayValueOnlySeparator(String displayValueOnlySeparator) {
244        this.displayValueOnlySeparator = displayValueOnlySeparator;
245    }
246
247    public boolean isAjaxUpdated() {
248        return ajaxUpdated;
249    }
250
251    public void setAjaxUpdated(boolean ajaxUpdated) {
252        this.ajaxUpdated = ajaxUpdated;
253    }
254
255    /**
256     * @return position of this component in the parent children list
257     */
258    public Integer getIndex() {
259        // return index;
260        ValueBinding vb = getValueBinding("index");
261        if (vb != null) {
262            return (Integer) vb.getValue(FacesContext.getCurrentInstance());
263        } else {
264            return index;
265        }
266    }
267
268    public void setIndex(Integer index) {
269        this.index = index;
270    }
271
272    public String getCssStyle() {
273        return cssStyle;
274    }
275
276    public void setCssStyle(String cssStyle) {
277        this.cssStyle = cssStyle;
278    }
279
280    public String getCssStyleClass() {
281        return cssStyleClass;
282    }
283
284    public void setCssStyleClass(String cssStyleClass) {
285        this.cssStyleClass = cssStyleClass;
286    }
287
288    public String getSize() {
289        return size;
290    }
291
292    public void setSize(String size) {
293        this.size = size;
294    }
295
296    public Boolean getLocalize() {
297        return localize;
298    }
299
300    public void setLocalize(Boolean localize) {
301        this.localize = localize;
302    }
303
304    /**
305     * Reload listbox values based on previous selections in the chain. (functionality moved from ChainSelect)
306     */
307    public LinkedHashMap<String, DirectorySelectItem> rebuildOptions() {
308
309        index = getIndex();
310
311        Map<String, Serializable> filter = new HashMap<String, Serializable>();
312        Boolean displayObsolete = getBooleanProperty("displayObsoleteEntries", Boolean.FALSE);
313        if (!displayObsolete) {
314            filter.put("obsolete", 0);
315        }
316
317        String directoryName = getDirectoryName();
318        String defaultRootKey = getChain().getDefaultRootKey();
319        if (index == 0) {
320            if (directoryName != null) {
321                if (DirectoryHelper.instance().hasParentColumn(directoryName)) {
322                    filter.put("parent", defaultRootKey);
323                }
324            } else {
325                filter.put("parent", defaultRootKey);
326            }
327        } else {
328            boolean qualifiedParentKeys = getChain().isQualifiedParentKeys();
329            String keySeparator = getChain().getKeySeparator();
330            Selection sel = getChain().getSelections()[0];
331            String parentValue = sel.getParentKey(index, qualifiedParentKeys, keySeparator);
332            if (parentValue == null) {
333                // use default parent key
334                parentValue = defaultRootKey;
335            }
336            filter.put("parent", parentValue);
337        }
338
339        VocabularyEntryList directoryValues = getDirectoryValues();
340
341        List<DirectorySelectItem> list;
342        if (directoryName != null) {
343            list = DirectoryHelper.instance().getSelectItems(directoryName, filter);
344        } else {
345            list = DirectoryHelper.getSelectItems(directoryValues, filter);
346        }
347
348        for (DirectorySelectItem item : list) {
349            String id = (String) item.getValue();
350            String label = item.getLabel();
351            String translatedLabel = label;
352            Boolean localize = getBooleanProperty("localize", Boolean.FALSE);
353            if (localize) {
354                translatedLabel = translate(label);
355            }
356            item.setLocalizedLabel(translatedLabel);
357
358            String displayedLabel = translatedLabel;
359            Boolean displayIdAndLabel = getBooleanProperty("displayIdAndLabel", Boolean.FALSE);
360            if (displayIdAndLabel) {
361                displayedLabel = id + displayIdAndLabelSeparator + label;
362            }
363            item.setDisplayedLabel(displayedLabel);
364        }
365        String ordering = getStringProperty("ordering", "");
366        if (ordering != null && !"".equals(ordering)) {
367            Collections.sort(list, new DirectorySelectItemComparator(ordering));
368        }
369
370        LinkedHashMap<String, DirectorySelectItem> options = new LinkedHashMap<String, DirectorySelectItem>();
371        options.clear();
372        for (DirectorySelectItem item : list) {
373            options.put((String) item.getValue(), item);
374        }
375
376        return options;
377    }
378
379    private static String translate(String label) {
380        FacesContext context = FacesContext.getCurrentInstance();
381        String bundleName = context.getApplication().getMessageBundle();
382        Locale locale = context.getViewRoot().getLocale();
383        label = I18NUtils.getMessageString(bundleName, label, null, locale);
384        return label;
385    }
386
387    /**
388     * This method reads submitted data and rebuilds the current list of values based on selections in the parent
389     * components.
390     */
391    @Override
392    public void decode(FacesContext context) {
393        // FIXME: this code is nonsense, what it's doing and why is
394        // perfectly unclear
395
396        ChainSelect chain = getChain();
397        if (chain.getDisplayValueOnly()) {
398            return;
399        }
400
401        index = getIndex();
402
403        chain.setCompAtIndex(index, this);
404        List<String> keyList = chain.getSelectionKeyList();
405        int size = chain.getSize();
406        String formerValue = chain.getSelection(0).getColumnValue(index);
407
408        if (index == 0) {
409            chain.setLastSelectedComponentIndex(Integer.MAX_VALUE);
410            keyList.clear();
411        }
412
413        if (chain.getLastSelectedComponentIndex() < index) {
414            for (int i = index; i < size; i++) {
415                chain.setOptions(i, null);
416            }
417            Map<String, DirectorySelectItem> options = rebuildOptions();
418            chain.setOptions(index, options);
419            return;
420        }
421
422        Map<String, String> requestMap = context.getExternalContext().getRequestParameterMap();
423        Map<String, String[]> requestValueMap = context.getExternalContext().getRequestParameterValuesMap();
424
425        String name = getClientId(context);
426
427        String value = requestMap.get(name);
428        if (value == null || value.length() == 0) {
429            if (chain.getLastSelectedComponentIndex() > index) {
430                chain.setLastSelectedComponentIndex(index);
431            }
432
433            for (int i = index; i < chain.getSize(); i++) {
434                chain.setOptions(i, null);
435            }
436            Map<String, DirectorySelectItem> options = rebuildOptions();
437            chain.setOptions(index, options);
438        } else {
439            keyList.add(value);
440        }
441
442        if (!StringUtils.equals(value, formerValue)) {
443            chain.setLastSelectedComponentIndex(index);
444
445            for (int i = index; i < chain.getSize(); i++) {
446                chain.setOptions(i, null);
447            }
448        }
449
450        String[] lastValues = requestValueMap.get(name);
451
452        boolean lastValueIsOk = lastValues != null && lastValues.length != 0 && !StringUtils.isEmpty(lastValues[0]);
453
454        Selection[] selections;
455
456        boolean stop = chain.getLastSelectedComponentIndex() < index;
457        if (index == size - 1 && lastValueIsOk && !stop) {
458            String[] keyListArray = new String[size];
459            selections = new Selection[lastValues.length];
460            keyListArray = keyList.toArray(keyListArray);
461            for (int i = 0; i < lastValues.length; i++) {
462                keyListArray[size - 1] = lastValues[i];
463                selections[i] = chain.createSelection(keyListArray);
464            }
465        } else {
466            selections = new Selection[1];
467            String[] columns = keyList.toArray(new String[0]);
468            selections[0] = chain.createSelection(columns);
469        }
470
471        if (chain.getLastSelectedComponentIndex() == index) {
472            chain.setSelections(selections);
473        }
474
475        Map<String, DirectorySelectItem> options = rebuildOptions();
476        chain.setOptions(index, options);
477    }
478
479    public String getOrdering() {
480        return ordering;
481    }
482
483    public void setOrdering(String ordering) {
484        this.ordering = ordering;
485    }
486
487    public String getDisplay() {
488        if (display != null) {
489            return display;
490        } else if (Boolean.TRUE.equals(displayIdAndLabel)) {
491            return "idAndLabel";
492        }
493        return "label";
494    }
495
496    public void setDisplay(String display) {
497        this.display = display;
498    }
499
500    /**
501     * @since 6.0
502     */
503    private static final Collection<String> EVENT_NAMES = Collections.unmodifiableCollection(Arrays.asList("blur",
504            "change", "valueChange", "click", "dblclick", "focus", "keydown", "keypress", "keyup", "mousedown",
505            "mousemove", "mouseout", "mouseover", "mouseup", "select"));
506
507    @Override
508    public Collection<String> getEventNames() {
509        return EVENT_NAMES;
510    }
511
512    @Override
513    public String getDefaultEventName() {
514        return "valueChange";
515    }
516
517}