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