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