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 *     George Lefter
018 *
019 * $Id$
020 */
021
022package org.nuxeo.ecm.platform.ui.web.directory;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.Collections;
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.LinkedHashMap;
032import java.util.List;
033import java.util.Map;
034import java.util.Set;
035
036import javax.el.ValueExpression;
037import javax.faces.application.FacesMessage;
038import javax.faces.component.NamingContainer;
039import javax.faces.component.UIComponent;
040import javax.faces.component.UIInput;
041import javax.faces.component.UISelectItem;
042import javax.faces.component.html.HtmlSelectOneListbox;
043import javax.faces.context.FacesContext;
044
045import org.apache.commons.lang3.StringUtils;
046import org.apache.commons.logging.Log;
047import org.apache.commons.logging.LogFactory;
048import org.nuxeo.ecm.core.api.DocumentModel;
049import org.nuxeo.ecm.core.api.DocumentModelList;
050import org.nuxeo.ecm.directory.Session;
051import org.nuxeo.ecm.directory.api.DirectoryService;
052import org.nuxeo.ecm.platform.ui.web.util.ComponentUtils;
053
054/**
055 * @author <a href="mailto:glefter@nuxeo.com">George Lefter</a>
056 */
057public abstract class ChainSelectBase extends UIInput implements NamingContainer {
058
059    private static final Log log = LogFactory.getLog(ChainSelect.class);
060
061    protected static final String DISPLAY_LABEL = "label";
062
063    protected static final String DISPLAY_ID = "id";
064
065    protected static final String DISPLAY_IDLABEL = "idAndLabel";
066
067    protected static final String DEFAULT_KEYSEPARATOR = "/";
068
069    protected static final String SELECT = "selectListbox";
070
071    public static final String VOCABULARY_SCHEMA = "vocabulary";
072
073    /** Directory with a parent column. */
074    public static final String XVOCABULARY_SCHEMA = "xvocabulary";
075
076    /**
077     * Parent column.
078     *
079     * @since 9.3
080     */
081    public static final String PARENT_COLUMN = "parent";
082
083    protected String directoryNames;
084
085    protected String keySeparator = DEFAULT_KEYSEPARATOR;
086
087    protected boolean qualifiedParentKeys = false;
088
089    protected int depth;
090
091    protected String display = DISPLAY_LABEL;
092
093    protected boolean translate;
094
095    protected boolean showObsolete;
096
097    protected String style;
098
099    protected String styleClass;
100
101    protected int listboxSize;
102
103    protected boolean allowBranchSelection;
104
105    protected String reRender;
106
107    private boolean displayValueOnly;
108
109    protected String defaultRootKey;
110
111    protected Map<String, String[]> selectionMap = new HashMap<String, String[]>();
112
113    protected ChainSelectBase() {
114        HtmlSelectOneListbox select = new HtmlSelectOneListbox();
115        getFacets().put(SELECT, select);
116    }
117
118    public String getDirectory(int level) {
119        String[] directories = getDirectories();
120        if (isRecursive()) {
121            return directories[0];
122        } else {
123            if (level < directories.length) {
124                return directories[level];
125            } else {
126                return null;
127            }
128        }
129    }
130
131    @Override
132    @SuppressWarnings("unchecked")
133    public void restoreState(FacesContext context, Object state) {
134        Object[] values = (Object[]) state;
135        super.restoreState(context, values[0]);
136        ChainSelectState chainState = (ChainSelectState) values[1];
137        selectionMap = (Map<String, String[]>) values[2];
138
139        depth = chainState.getDepth();
140        display = chainState.getDisplay();
141        directoryNames = chainState.getDirectoryNames();
142        keySeparator = chainState.getKeySeparator();
143        qualifiedParentKeys = chainState.getQualifiedParentKeys();
144        showObsolete = chainState.getShowObsolete();
145        listboxSize = chainState.getListboxSize();
146        style = chainState.getStyle();
147        styleClass = chainState.getStyleClass();
148        translate = chainState.getTranslate();
149        allowBranchSelection = chainState.getAllowBranchSelection();
150        reRender = chainState.getReRender();
151        displayValueOnly = chainState.getDisplayValueOnly();
152        defaultRootKey = chainState.getDefaultRootKey();
153    }
154
155    @Override
156    public Object saveState(FacesContext context) {
157        ChainSelectState chainState = new ChainSelectState();
158        chainState.setDepth(depth);
159        chainState.setDisplay(display);
160        chainState.setDirectoryNames(directoryNames);
161        chainState.setKeySeparator(keySeparator);
162        chainState.setQualifiedParentKeys(qualifiedParentKeys);
163        chainState.setShowObsolete(showObsolete);
164        chainState.setStyle(style);
165        chainState.setStyleClass(styleClass);
166        chainState.setTranslate(translate);
167        chainState.setListboxSize(listboxSize);
168        chainState.setAllowBranchSelection(allowBranchSelection);
169        chainState.setReRender(reRender);
170        chainState.setDisplayValueOnly(displayValueOnly);
171        chainState.setDefaultRootKey(defaultRootKey);
172
173        Object[] values = new Object[3];
174        values[0] = super.saveState(context);
175        values[1] = chainState;
176        values[2] = selectionMap;
177
178        return values;
179    }
180
181    protected HtmlSelectOneListbox getListbox(FacesContext context, int level) {
182        String componentId = getComponentId(level);
183
184        HtmlSelectOneListbox listbox = new HtmlSelectOneListbox();
185        getChildren().add(listbox);
186
187        listbox.setId(componentId);
188        listbox.getChildren().clear();
189
190        String reRender = getReRender();
191        if (reRender == null) {
192            reRender = getId();
193        }
194
195        UIComponent support = context.getApplication().createComponent("org.ajax4jsf.ajax.Support");
196        support.getAttributes().put("event", "onchange");
197        support.getAttributes().put("reRender", reRender);
198        support.getAttributes().put("immediate", Boolean.TRUE);
199        support.getAttributes().put("id", componentId + "_a4jSupport");
200        listbox.getChildren().add(support);
201
202        return listbox;
203    }
204
205    protected void encodeListbox(FacesContext context, int level, String[] selectedKeys) throws IOException {
206        HtmlSelectOneListbox listbox = getListbox(context, level);
207        listbox.setSize(getListboxSize());
208
209        List<DirectoryEntry> items;
210        if (level <= selectedKeys.length) {
211            items = getDirectoryEntries(level, selectedKeys);
212        } else {
213            items = new ArrayList<DirectoryEntry>();
214        }
215
216        UISelectItem emptyItem = new UISelectItem();
217        emptyItem.setItemLabel(ComponentUtils.translate(context, "label.vocabulary.selectValue"));
218        emptyItem.setItemValue("");
219        emptyItem.setId(context.getViewRoot().createUniqueId());
220        listbox.getChildren().add(emptyItem);
221
222        for (DirectoryEntry child : items) {
223            UISelectItem selectItem = new UISelectItem();
224            String itemValue = child.getId();
225            String itemLabel = child.getLabel();
226            itemLabel = computeItemLabel(context, itemValue, itemLabel);
227
228            selectItem.setItemValue(itemValue);
229            selectItem.setItemLabel(itemLabel);
230            selectItem.setId(context.getViewRoot().createUniqueId());
231            listbox.getChildren().add(selectItem);
232        }
233
234        if (level < selectedKeys.length) {
235            listbox.setValue(selectedKeys[level]);
236        }
237
238        ComponentUtils.encodeComponent(context, listbox);
239    }
240
241    public String[] getDirectories() {
242        return StringUtils.split(getDirectoryNames(), ",");
243    }
244
245    public boolean isRecursive() {
246        return getDirectories().length != getDepth();
247    }
248
249    /**
250     * Computes the items that should be displayed for the nth listbox, depending on the options that have been selected
251     * in the previous ones.
252     *
253     * @param level the index of the listbox for which to compute the items
254     * @param selectedKeys the keys for the items selected on the previous levels
255     * @return a list of directory items
256     */
257    public List<DirectoryEntry> getDirectoryEntries(int level, String[] selectedKeys) {
258
259        assert level <= selectedKeys.length;
260
261        List<DirectoryEntry> result = new ArrayList<DirectoryEntry>();
262        String directoryName = getDirectory(level);
263
264        DirectoryService service = DirectoryHelper.getDirectoryService();
265        try (Session session = service.open(directoryName)) {
266            String schema = service.getDirectorySchema(directoryName);
267            Map<String, Serializable> filter = new HashMap<String, Serializable>();
268
269            if (level == 0) {
270                if (schema.equals(XVOCABULARY_SCHEMA)) {
271                    filter.put(PARENT_COLUMN, null);
272                }
273            } else {
274                if (getQualifiedParentKeys()) {
275                    Iterator<String> iter = Arrays.asList(selectedKeys).subList(0, level).iterator();
276                    String fullPath = StringUtils.join(iter, getKeySeparator());
277                    filter.put(PARENT_COLUMN, fullPath);
278                } else {
279                    filter.put(PARENT_COLUMN, selectedKeys[level - 1]);
280                }
281            }
282
283            if (!getShowObsolete()) {
284                filter.put("obsolete", "0");
285            }
286
287            Set<String> emptySet = Collections.emptySet();
288            Map<String, String> orderBy = new LinkedHashMap<String, String>();
289
290            // adding sorting suport
291            if (schema.equals(VOCABULARY_SCHEMA) || schema.equals(XVOCABULARY_SCHEMA)) {
292                orderBy.put("ordering", "asc");
293                orderBy.put("id", "asc");
294            }
295
296            DocumentModelList entries = session.query(filter, emptySet, orderBy);
297            for (DocumentModel entry : entries) {
298                DirectoryEntry newNode = new DirectoryEntry(schema, entry);
299                result.add(newNode);
300            }
301        }
302
303        return result;
304    }
305
306    /**
307     * Resolves a list of keys (a selection) to a list of coresponding directory items. Example: [a, b, c] is resolved
308     * to [getNode(a), getNode(b), getNode(c)]
309     *
310     * @param keys
311     * @return
312     */
313    public List<DirectoryEntry> resolveKeys(String[] keys) {
314        List<DirectoryEntry> result = new ArrayList<DirectoryEntry>();
315
316        DirectoryService service = DirectoryHelper.getDirectoryService();
317        for (int level = 0; level < keys.length; level++) {
318            String directoryName = getDirectory(level);
319            try (Session session = service.open(directoryName)) {
320                String schema = service.getDirectorySchema(directoryName);
321                Map<String, Serializable> filter = new HashMap<>();
322
323                if (level == 0) {
324                    if (schema.equals(XVOCABULARY_SCHEMA)) {
325                        filter.put(PARENT_COLUMN, null);
326                    }
327                } else {
328                    if (getQualifiedParentKeys()) {
329                        Iterator<String> iter = Arrays.asList(keys).subList(0, level).iterator();
330                        String fullPath = StringUtils.join(iter, getKeySeparator());
331                        filter.put(PARENT_COLUMN, fullPath);
332                    } else {
333                        filter.put(PARENT_COLUMN, keys[level - 1]);
334                    }
335                }
336                filter.put("id", keys[level]);
337
338                DocumentModelList entries = session.query(filter);
339                if (entries == null || entries.isEmpty()) {
340                    log.warn("keyList could not be resolved at level " + level);
341                    break;
342                }
343                DirectoryEntry node = new DirectoryEntry(schema, entries.get(0));
344                result.add(node);
345
346            }
347        }
348        return result;
349    }
350
351    public String getComponentId(int level) {
352        String directory = getDirectory(level);
353        if (isRecursive()) {
354            return directory + '_' + level;
355        } else {
356            return directory + '_' + level;
357        }
358    }
359
360    public String getKeySeparator() {
361        ValueExpression ve = getValueExpression("keySeparator");
362        if (ve != null) {
363            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
364        } else {
365            return keySeparator;
366        }
367    }
368
369    public void setKeySeparator(String keySeparator) {
370        this.keySeparator = keySeparator;
371    }
372
373    public String getDefaultRootKey() {
374        ValueExpression ve = getValueExpression("defaultRootKey");
375        if (ve != null) {
376            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
377        } else {
378            return defaultRootKey;
379        }
380    }
381
382    public void setDefaultRootKey(String defaultRootKey) {
383        this.defaultRootKey = defaultRootKey;
384    }
385
386    public boolean getDisplayValueOnly() {
387        ValueExpression ve = getValueExpression("displayValueOnly");
388        if (ve != null) {
389            Boolean value = (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext());
390            return value == null ? false : value;
391        } else {
392            return displayValueOnly;
393        }
394    }
395
396    public void setDisplayValueOnly(boolean displayValueOnly) {
397        this.displayValueOnly = displayValueOnly;
398    }
399
400    public int getListboxSize() {
401        ValueExpression ve = getValueExpression("listboxSize");
402        if (ve != null) {
403            return (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext());
404        } else {
405            return listboxSize;
406        }
407    }
408
409    public void setListboxSize(int listboxSize) {
410        this.listboxSize = listboxSize;
411    }
412
413    public String getDisplay() {
414        ValueExpression ve = getValueExpression("display");
415        if (ve != null) {
416            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
417        } else {
418            return display != null ? display : DISPLAY_LABEL;
419        }
420    }
421
422    public void setDisplay(String display) {
423        this.display = display;
424    }
425
426    public boolean getQualifiedParentKeys() {
427        ValueExpression ve = getValueExpression("qualifiedParentKeys");
428        if (ve != null) {
429            return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext());
430        } else {
431            return qualifiedParentKeys;
432        }
433    }
434
435    public String getDirectoryNames() {
436        ValueExpression ve = getValueExpression("directoryNames");
437        if (ve != null) {
438            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
439        } else {
440            return directoryNames;
441        }
442    }
443
444    public void setDirectoryNames(String directoryNames) {
445        this.directoryNames = directoryNames;
446    }
447
448    public int getDepth() {
449        int myDepth;
450        ValueExpression ve = getValueExpression("depth");
451        if (ve != null) {
452            myDepth = (Integer) ve.getValue(FacesContext.getCurrentInstance().getELContext());
453        } else {
454            myDepth = depth;
455        }
456
457        return myDepth != 0 ? myDepth : getDirectories().length;
458    }
459
460    public void setDepth(int depth) {
461        this.depth = depth;
462    }
463
464    public String getStyle() {
465        ValueExpression ve = getValueExpression("style");
466        if (ve != null) {
467            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
468        } else {
469            return style;
470        }
471    }
472
473    public void setStyle(String style) {
474        this.style = style;
475    }
476
477    public String getStyleClass() {
478        ValueExpression ve = getValueExpression("styleClass");
479        if (ve != null) {
480            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
481        } else {
482            return styleClass;
483        }
484    }
485
486    public void setStyleClass(String styleClass) {
487        this.styleClass = styleClass;
488    }
489
490    public boolean getTranslate() {
491        ValueExpression ve_translate = getValueExpression("translate");
492        if (ve_translate != null) {
493            return (Boolean) ve_translate.getValue(FacesContext.getCurrentInstance().getELContext());
494        } else {
495            return translate;
496        }
497    }
498
499    public void setTranslate(boolean translate) {
500        this.translate = translate;
501    }
502
503    public boolean getShowObsolete() {
504        ValueExpression ve = getValueExpression("showObsolete");
505        if (ve != null) {
506            return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext());
507        } else {
508            return showObsolete;
509        }
510    }
511
512    public void setShowObsolete(boolean showObsolete) {
513        this.showObsolete = showObsolete;
514    }
515
516    public boolean getAllowBranchSelection() {
517        ValueExpression ve = getValueExpression("allowBranchSelection");
518        if (ve != null) {
519            return (Boolean) ve.getValue(FacesContext.getCurrentInstance().getELContext());
520        } else {
521            return allowBranchSelection;
522        }
523    }
524
525    public void setAllowBranchSelection(boolean allowBranchSelection) {
526        this.allowBranchSelection = allowBranchSelection;
527    }
528
529    public String getReRender() {
530        ValueExpression ve = getValueExpression("reRender");
531        if (ve != null) {
532            return (String) ve.getValue(FacesContext.getCurrentInstance().getELContext());
533        } else {
534            return reRender;
535        }
536    }
537
538    public void setReRender(String reRender) {
539        this.reRender = reRender;
540    }
541
542    protected String[] getValueAsArray(String value) {
543        if (value == null) {
544            return new String[0];
545        }
546        return StringUtils.split(value, getKeySeparator());
547    }
548
549    protected String getValueAsString(String[] ar) {
550        return StringUtils.join(ar, getKeySeparator());
551    }
552
553    protected String computeItemLabel(FacesContext context, String id, String label) {
554        boolean translate = getTranslate();
555        String display = getDisplay();
556
557        String translatedLabel = label;
558        if (translate) {
559            translatedLabel = ComponentUtils.translate(context, label);
560        }
561
562        if (DISPLAY_ID.equals(display)) {
563            return id;
564        } else if (DISPLAY_LABEL.equals(display)) {
565            return translatedLabel;
566        } else if (DISPLAY_IDLABEL.equals(display)) {
567            return id + ' ' + translatedLabel;
568        } else {
569            throw new RuntimeException(
570                    "invalid value for attribute 'display'; should be either 'id', 'label' or 'idAndLabel'");
571        }
572    }
573
574    public abstract String[] getSelection();
575
576    protected void decodeSelection(FacesContext context) {
577        List<String> selectedKeyList = new ArrayList<String>();
578        Map<String, String> parameters = context.getExternalContext().getRequestParameterMap();
579
580        String[] selection = getSelection();
581        for (int level = 0; level < getDepth(); level++) {
582            String clientId = getClientId(context) + SEPARATOR_CHAR + getComponentId(level);
583            String value = parameters.get(clientId);
584            if (StringUtils.isEmpty(value)) {
585                break;
586            }
587            selectedKeyList.add(value);
588
589            // compare the old value with the new one; if they differ
590            // the new list of keys is finished
591            if (level >= selection.length) {
592                break;
593            }
594            String oldValue = selection[level];
595            if (!value.equals(oldValue)) {
596                break;
597            }
598        }
599        selection = selectedKeyList.toArray(new String[selectedKeyList.size()]);
600        setSelection(selection);
601    }
602
603    protected void setSelection(String[] selection) {
604        String clientId = getClientId(FacesContext.getCurrentInstance());
605        selectionMap.put(clientId, selection);
606    }
607
608    protected boolean validateEntry(FacesContext context, String[] keys) {
609        if (!getAllowBranchSelection() && keys.length != getDepth()) {
610            String messageStr = ComponentUtils.translate(context, "label.chainSelect.incomplete_selection");
611            FacesMessage message = new FacesMessage(FacesMessage.SEVERITY_ERROR, messageStr, messageStr);
612            context.addMessage(getClientId(context), message);
613            setValid(false);
614            return false;
615        } else {
616            return true;
617        }
618    }
619
620}