001/*
002 * (C) Copyright 2010 Nuxeo SA (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 *     <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
016 *     <a href="mailto:gr@nuxeo.com">Georges Racinet</a>
017 */
018package org.nuxeo.ecm.platform.ui.web.util;
019
020import java.io.Serializable;
021
022import javax.faces.component.EditableValueHolder;
023import javax.faces.component.UIComponent;
024import javax.faces.component.UISelectItems;
025import javax.faces.component.UISelectMany;
026import javax.faces.component.ValueHolder;
027import javax.faces.event.ActionEvent;
028import javax.faces.event.AjaxBehaviorEvent;
029import javax.faces.event.FacesEvent;
030import javax.faces.model.SelectItem;
031
032import org.apache.commons.logging.Log;
033import org.apache.commons.logging.LogFactory;
034import org.jboss.seam.ScopeType;
035import org.jboss.seam.annotations.Name;
036import org.jboss.seam.annotations.Scope;
037import org.jboss.seam.annotations.web.RequestParameter;
038import org.nuxeo.ecm.platform.ui.web.component.list.UIEditableList;
039
040/**
041 * Helper for selection actions, useful when performing ajax calls on a "liste shuttle" widget for instance, or to
042 * retrieve the selected value on a JSF component and set it on another.
043 */
044@Name("selectionActions")
045@Scope(ScopeType.EVENT)
046public class SelectionActionsBean implements Serializable {
047
048    private static final long serialVersionUID = 1L;
049
050    private static final Log log = LogFactory.getLog(SelectionActionsBean.class);
051
052    public enum ShiftType {
053        FIRST, UP, DOWN, LAST
054    }
055
056    @RequestParameter
057    protected String leftSelect;
058
059    @RequestParameter
060    protected String leftItems;
061
062    @RequestParameter
063    protected String rightSelect;
064
065    @RequestParameter
066    protected String rightItems;
067
068    @RequestParameter
069    protected String submittedList;
070
071    /**
072     * Id of the input selector
073     * <p>
074     * Component must be an instance of {@link ValueHolder}
075     */
076    @RequestParameter
077    protected String selectorId;
078
079    /**
080     * Id of the value holder that will receive the selected value.
081     * <p>
082     * Component must be an instance of {@link ValueHolder}
083     */
084    @RequestParameter
085    protected String valueHolderId;
086
087    /**
088     * Lookup level request parameter (defaults to 1, means search is done in r first parent naming container)
089     *
090     * @since 5.6
091     */
092    @RequestParameter
093    protected Integer lookupLevel;
094
095    /**
096     * Lookup level field (defaults to 1, means search is done in first parent naming container)
097     * <p>
098     * Useful as fallback when posting a button where request parameter {@link #lookupLevel} cannot be set.
099     *
100     * @since 5.6
101     */
102    protected String lookupLevelValue;
103
104    /**
105     * @since 5.6
106     */
107    public String getLookupLevelValue() {
108        return lookupLevelValue;
109    }
110
111    /**
112     * @since 5.6
113     */
114    public void setLookupLevelValue(String lookupLevelValue) {
115        this.lookupLevelValue = lookupLevelValue;
116    }
117
118    /**
119     * @since 5.6
120     */
121    protected int computeLookupLevel() {
122        if (lookupLevel != null) {
123            return lookupLevel.intValue();
124        }
125        String setValue = getLookupLevelValue();
126        if (setValue != null) {
127            return Integer.valueOf(setValue).intValue();
128        }
129        return 1;
130    }
131
132    /**
133     * Value held temporarily by this bean to be set on JSF components.
134     *
135     * @since 5.5
136     */
137    @RequestParameter
138    protected String selectedValue;
139
140    public String getSelectedValue() {
141        return selectedValue;
142    }
143
144    public void setSelectedValue(String selectedValue) {
145        this.selectedValue = selectedValue;
146    }
147
148    /**
149     * Value component id held temporarily by this bean to be retrieved from the JSF component tree.
150     * <p>
151     * this is an alternative to {@link #valueHolderId} request parameter usage, to make it possible to set this value
152     * easily from command buttons (as only command links do take request parameters into account).
153     *
154     * @since 5.6
155     */
156    protected String selectedValueHolder;
157
158    public String getSelectedValueHolder() {
159        return selectedValueHolder;
160    }
161
162    public void setSelectedValueHolder(String selectedValueHolder) {
163        this.selectedValueHolder = selectedValueHolder;
164    }
165
166    public SelectItem[] getEmptySelection() {
167        return new SelectItem[0];
168    }
169
170    protected boolean checkRightComponents() {
171        String logPrefix = "Check right components: ";
172        if (rightSelect == null) {
173            log.error(logPrefix + "No select component name");
174            return false;
175        }
176        if (rightItems == null) {
177            log.error(logPrefix + "No items component name");
178            return false;
179        }
180        return true;
181    }
182
183    protected boolean checkLeftComponents() {
184        String logPrefix = "Check left components: ";
185        if (leftSelect == null) {
186            log.error(logPrefix + "No select component name");
187            return false;
188        }
189        if (leftItems == null) {
190            log.error(logPrefix + "No items component name");
191            return false;
192        }
193        return true;
194    }
195
196    protected boolean checkSubmittedList() {
197        String logPrefix = "Check submitted list: ";
198        if (submittedList == null) {
199            log.error(logPrefix + "No component name");
200            return false;
201        }
202        return true;
203    }
204
205    public void shiftSelected(ShiftType stype, ActionEvent event) {
206        if (!checkRightComponents() || !checkSubmittedList()) {
207            return;
208        }
209        UIComponent eventComp = event.getComponent();
210        UIComponent rightItemsComp = eventComp.findComponent(rightItems);
211        UIComponent rightSelectComp = eventComp.findComponent(rightSelect);
212        UIComponent hiddenTargetListComp = eventComp.findComponent(submittedList);
213        if (rightSelectComp instanceof UISelectMany && rightItemsComp instanceof UISelectItems
214                && hiddenTargetListComp instanceof UIEditableList) {
215            UISelectItems targetItems = (UISelectItems) rightItemsComp;
216            UISelectMany targetComp = (UISelectMany) rightSelectComp;
217            UIEditableList hiddenTargetList = (UIEditableList) hiddenTargetListComp;
218            switch (stype) {
219            case UP:
220                ComponentUtils.shiftItemsUp(targetComp, targetItems, hiddenTargetList);
221                break;
222            case DOWN:
223                ComponentUtils.shiftItemsDown(targetComp, targetItems, hiddenTargetList);
224                break;
225            case FIRST:
226                ComponentUtils.shiftItemsFirst(targetComp, targetItems, hiddenTargetList);
227                break;
228            case LAST:
229                ComponentUtils.shiftItemsLast(targetComp, targetItems, hiddenTargetList);
230                break;
231            }
232        }
233    }
234
235    public void shiftSelectedUp(ActionEvent event) {
236        shiftSelected(ShiftType.UP, event);
237    }
238
239    public void shiftSelectedDown(ActionEvent event) {
240        shiftSelected(ShiftType.DOWN, event);
241    }
242
243    public void shiftSelectedFirst(ActionEvent event) {
244        shiftSelected(ShiftType.FIRST, event);
245    }
246
247    public void shiftSelectedLast(ActionEvent event) {
248        shiftSelected(ShiftType.LAST, event);
249    }
250
251    public void addToSelection(ActionEvent event) {
252        if (!checkLeftComponents() || !checkRightComponents() || !checkSubmittedList()) {
253            return;
254        }
255        UIComponent eventComp = event.getComponent();
256        UIComponent leftSelectComp = eventComp.findComponent(leftSelect);
257        UIComponent leftItemsComp = eventComp.findComponent(leftItems);
258        UIComponent rightItemsComp = eventComp.findComponent(rightItems);
259        UIComponent hiddenTargetListComp = eventComp.findComponent(submittedList);
260        if (leftSelectComp instanceof UISelectMany && leftItemsComp instanceof UISelectItems
261                && rightItemsComp instanceof UISelectItems && hiddenTargetListComp instanceof UIEditableList) {
262            UISelectMany sourceSelect = (UISelectMany) leftSelectComp;
263            UISelectItems sourceItems = (UISelectItems) leftItemsComp;
264            UISelectItems targetItems = (UISelectItems) rightItemsComp;
265            UIEditableList hiddenTargetList = (UIEditableList) hiddenTargetListComp;
266            ComponentUtils.moveItems(sourceSelect, sourceItems, targetItems, hiddenTargetList, true);
267        }
268    }
269
270    public void removeFromSelection(ActionEvent event) {
271        if (!checkLeftComponents() || !checkRightComponents() || !checkSubmittedList()) {
272            return;
273        }
274        UIComponent eventComp = event.getComponent();
275        UIComponent leftItemsComp = eventComp.findComponent(leftItems);
276        UIComponent rightSelectComp = eventComp.findComponent(rightSelect);
277        UIComponent rightItemsComp = eventComp.findComponent(rightItems);
278        UIComponent hiddenTargetListComp = eventComp.findComponent(submittedList);
279        if (leftItemsComp instanceof UISelectItems && rightSelectComp instanceof UISelectMany
280                && rightItemsComp instanceof UISelectItems && hiddenTargetListComp instanceof UIEditableList) {
281            UISelectItems leftItems = (UISelectItems) leftItemsComp;
282            UISelectMany rightSelect = (UISelectMany) rightSelectComp;
283            UISelectItems rightItems = (UISelectItems) rightItemsComp;
284            UIEditableList hiddenTargetList = (UIEditableList) hiddenTargetListComp;
285            ComponentUtils.moveItems(rightSelect, rightItems, leftItems, hiddenTargetList, false);
286        }
287    }
288
289    public void addAllToSelection(ActionEvent event) {
290        if (!checkLeftComponents() || !checkRightComponents() || !checkSubmittedList()) {
291            return;
292        }
293        UIComponent eventComp = event.getComponent();
294        UIComponent leftItemsComp = eventComp.findComponent(leftItems);
295        UIComponent rightItemsComp = eventComp.findComponent(rightItems);
296        UIComponent hiddenTargetListComp = eventComp.findComponent(submittedList);
297        if (leftItemsComp instanceof UISelectItems && rightItemsComp instanceof UISelectItems
298                && hiddenTargetListComp instanceof UIEditableList) {
299            UISelectItems sourceItems = (UISelectItems) leftItemsComp;
300            UISelectItems targetItems = (UISelectItems) rightItemsComp;
301            UIEditableList hiddenTargetList = (UIEditableList) hiddenTargetListComp;
302            ComponentUtils.moveAllItems(sourceItems, targetItems, hiddenTargetList, true);
303        }
304    }
305
306    public UISelectMany getSourceSelectComponent(ActionEvent event) {
307        if (leftSelect == null) {
308            log.warn("Unable to find leftSelect component. Param 'leftSelect' not sent in request");
309            return null;
310        }
311        UIComponent eventComp = event.getComponent();
312        UIComponent leftSelectComp = eventComp.findComponent(leftSelect);
313
314        if (leftSelectComp instanceof UISelectMany) {
315            return (UISelectMany) leftSelectComp;
316        }
317        return null;
318    }
319
320    public UISelectItems getSourceSelectItems(ActionEvent event) {
321        if (leftItems == null) {
322            log.warn("Unable to find leftItems component. Param 'leftItems' not sent in request");
323            return null;
324        }
325        UIComponent eventComp = event.getComponent();
326        UIComponent leftItemsComp = eventComp.findComponent(leftItems);
327
328        if (leftItemsComp instanceof UISelectItems) {
329            return (UISelectItems) leftItemsComp;
330        }
331        return null;
332    }
333
334    public void removeAllFromSelection(ActionEvent event) {
335        if (!checkLeftComponents() || !checkRightComponents() || !checkSubmittedList()) {
336            return;
337        }
338        UIComponent eventComp = event.getComponent();
339        UIComponent leftItemsComp = eventComp.findComponent(leftItems);
340        UIComponent rightItemsComp = eventComp.findComponent(rightItems);
341        UIComponent hiddenTargetListComp = eventComp.findComponent(submittedList);
342        if (leftItemsComp instanceof UISelectItems && rightItemsComp instanceof UISelectItems
343                && hiddenTargetListComp instanceof UIEditableList) {
344            UISelectItems leftItems = (UISelectItems) leftItemsComp;
345            UISelectItems rightItems = (UISelectItems) rightItemsComp;
346            UIEditableList hiddenTargetList = (UIEditableList) hiddenTargetListComp;
347            ComponentUtils.moveAllItems(rightItems, leftItems, hiddenTargetList, false);
348        }
349    }
350
351    /**
352     * Adds selection retrieved from a selector to another component
353     * <p>
354     * Must pass request parameters "selectorId" holding the id of component holding the value to pass to the other
355     * component, and "valueHolderId" holding the other component id.
356     *
357     * @since 5.5
358     * @param event
359     * @deprecated since 6.0: use {@link #onSelection(AjaxBehaviorEvent)} instead.
360     */
361    @Deprecated
362    public void onSelection(ActionEvent event) {
363        log.warn(String.format("The method #onSelection(ActionEvent) on component "
364                + "'selectionActions' at '%s' is deprecated, please " + "use #onSelection(AjaxBehaviorEvent) instead",
365                this.getClass().getName()));
366        onSelection((FacesEvent) event);
367    }
368
369    /**
370     * @since 6.0
371     */
372    public void onSelection(AjaxBehaviorEvent event) {
373        onSelection((FacesEvent) event);
374    }
375
376    protected void onSelection(FacesEvent event) {
377        UIComponent component = event.getComponent();
378        Object value = retrieveSourceComponentValue(component, selectorId);
379        UIComponent base = ComponentUtils.getBase(component);
380        ValueHolder valueHolderComp = ComponentUtils.getComponent(base, valueHolderId, ValueHolder.class);
381        setTargetComponentValue(valueHolderComp, value);
382    }
383
384    /**
385     * Adds value retrieved from {@link #getSelectedValue()} to a component
386     * <p>
387     * Must pass request parameters "valueHolderId" holding the id of the bound component, and call
388     * {@link #setSelectedValue(String)} prior to this call.
389     * <p>
390     * As an alternative, must call {@link #setSelectedValueHolder(String)} with the id of the bound component, and call
391     * {@link #setSelectedValue(String)} prior to this call (this makes it possible to use the same logic in command
392     * buttons that do not make it possible to pass request parameters).
393     *
394     * @deprecated since 6.0: use {@link #onClick(AjaxBehaviorEvent)} instead.
395     * @since 5.5
396     * @param event
397     */
398    @Deprecated
399    public void onClick(ActionEvent event) {
400        log.warn(String.format("The method #onClick(ActionEvent) on component "
401                + "'selectionActions' at '%s' is deprecated, please " + "use #onClick(AjaxBehaviorEvent) instead",
402                this.getClass().getName()));
403        onClick((FacesEvent) event);
404    }
405
406    public void onClick(AjaxBehaviorEvent event) {
407        onClick((FacesEvent) event);
408    }
409
410    protected void onClick(FacesEvent event) {
411        UIComponent component = event.getComponent();
412        if (component == null) {
413            return;
414        }
415        EditableValueHolder hiddenSelector = null;
416        UIComponent base = retrieveBase(component, computeLookupLevel());
417        if (valueHolderId != null) {
418            hiddenSelector = ComponentUtils.getComponent(base, valueHolderId, EditableValueHolder.class);
419        }
420        if (hiddenSelector == null) {
421            String selectedValueHolder = getSelectedValueHolder();
422            if (selectedValueHolder != null) {
423                hiddenSelector = ComponentUtils.getComponent(base, selectedValueHolder, EditableValueHolder.class);
424            }
425        }
426        if (hiddenSelector != null) {
427            String selectedValue = getSelectedValue();
428            setTargetComponentValue(hiddenSelector, selectedValue);
429        }
430    }
431
432    protected UIComponent retrieveBase(UIComponent anchor, int lookupLevel) {
433        UIComponent base = ComponentUtils.getBase(anchor);
434        if (lookupLevel > 1) {
435            for (int i = 0; i < (lookupLevel - 1); i++) {
436                base = ComponentUtils.getBase(base);
437            }
438        }
439        return base;
440    }
441
442    /**
443     * Retrieves a value from another component and sets it on the target component.
444     * <p>
445     * Source component id must be passed in the event component attributes with id "sourceComponentId".
446     * <p>
447     * Target component id must be passed in the event component attributes with id "targetComponentId". If target
448     * component is an {@link EditableValueHolder}, its submitted value is set. Otherwise, its local value is set.
449     *
450     * @since 6.0
451     * @param event
452     */
453    public void setValueFromComponent(AjaxBehaviorEvent event) {
454        setValueFromComponent((FacesEvent) event);
455    }
456
457    /**
458     * @see #setValueFromComponent(ActionEvent)
459     * @since 6.0
460     */
461    public void setValueFromComponent(ActionEvent event) {
462        setValueFromComponent((FacesEvent) event);
463    }
464
465    protected void setValueFromComponent(FacesEvent event) {
466        UIComponent anchor = event.getComponent();
467        String sourceCompId = getStringAttribute(anchor, "sourceComponentId", true);
468        Object value = retrieveSourceComponentValue(anchor, sourceCompId);
469        String targetCompId = getStringAttribute(anchor, "targetComponentId", true);
470        ValueHolder targetComp = ComponentUtils.getComponent(anchor, targetCompId, ValueHolder.class);
471        setTargetComponentValue(targetComp, value);
472    }
473
474    /**
475     * Retrieves a value passed as an attribute with id "selectedValue" on the event component attributes and sets it on
476     * the target component.
477     * <p>
478     * Target component id must be passed in the event component attributes with id "targetComponentId". If target
479     * component is an {@link EditableValueHolder}, its submitted value is set. Otherwise, its local value is set.
480     *
481     * @since 6.0
482     * @param event
483     */
484    public void setStaticValue(AjaxBehaviorEvent event) {
485        setStaticValue((FacesEvent) event);
486    }
487
488    /**
489     * @see #setStaticValue(ActionEvent)
490     * @since 6.0
491     */
492    public void setStaticValue(ActionEvent event) {
493        setStaticValue((FacesEvent) event);
494    }
495
496    protected void setStaticValue(FacesEvent event) {
497        UIComponent anchor = event.getComponent();
498        Object value = anchor.getAttributes().get("selectedValue");
499        String targetCompId = getStringAttribute(anchor, "targetComponentId", true);
500        ValueHolder targetComp = ComponentUtils.getComponent(anchor, targetCompId, ValueHolder.class);
501        setTargetComponentValue(targetComp, value);
502    }
503
504    protected String getStringAttribute(UIComponent component, String name, boolean required) {
505        Object value = component.getAttributes().get(name);
506        if (required && value == null) {
507            throw new IllegalArgumentException(String.format("Component attribute with name '%s' cannot be null: %s",
508                    name, value));
509        }
510        if (value == null || value instanceof String) {
511            return (String) value;
512        }
513        throw new IllegalArgumentException(String.format("Component attribute with name '%s' is not a String: %s",
514                name, value));
515    }
516
517    protected Object retrieveSourceComponentValue(UIComponent base, String targetId) {
518        ValueHolder selectComp = ComponentUtils.getComponent(base, targetId, ValueHolder.class);
519        if (selectComp != null) {
520            Object value;
521            if (selectComp instanceof EditableValueHolder) {
522                value = ((EditableValueHolder) selectComp).getSubmittedValue();
523                if (value == null) {
524                    value = selectComp.getValue();
525                }
526            } else {
527                value = selectComp.getValue();
528            }
529            return value;
530        }
531        return null;
532    }
533
534    protected void setTargetComponentValue(ValueHolder target, Object value) {
535        if (target != null) {
536            if (target instanceof EditableValueHolder) {
537                ((EditableValueHolder) target).setSubmittedValue(value);
538            } else {
539                target.setValue(value);
540            }
541        }
542    }
543
544}