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