001/*
002 * (C) Copyright 2011 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 *     Anahide Tchertchian
018 */
019package org.nuxeo.ecm.platform.forms.layout.facelets.plugins;
020
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.HashMap;
025import java.util.List;
026import java.util.Map;
027
028import javax.faces.component.UIComponent;
029import javax.faces.component.html.HtmlSelectManyCheckbox;
030import javax.faces.component.html.HtmlSelectManyListbox;
031import javax.faces.component.html.HtmlSelectManyMenu;
032import javax.faces.view.facelets.ComponentHandler;
033import javax.faces.view.facelets.CompositeFaceletHandler;
034import javax.faces.view.facelets.FaceletContext;
035import javax.faces.view.facelets.FaceletHandler;
036import javax.faces.view.facelets.TagAttributes;
037import javax.faces.view.facelets.TagConfig;
038import javax.faces.view.facelets.TagHandler;
039
040import org.apache.commons.lang3.ArrayUtils;
041import org.nuxeo.ecm.platform.forms.layout.api.BuiltinWidgetModes;
042import org.nuxeo.ecm.platform.forms.layout.api.Widget;
043import org.nuxeo.ecm.platform.forms.layout.api.WidgetSelectOption;
044import org.nuxeo.ecm.platform.forms.layout.api.WidgetSelectOptions;
045import org.nuxeo.ecm.platform.forms.layout.api.exceptions.WidgetException;
046import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetSelectOptionImpl;
047import org.nuxeo.ecm.platform.forms.layout.api.impl.WidgetSelectOptionsImpl;
048import org.nuxeo.ecm.platform.forms.layout.facelets.FaceletHandlerHelper;
049import org.nuxeo.ecm.platform.ui.web.component.UISelectItem;
050import org.nuxeo.ecm.platform.ui.web.component.UISelectItems;
051import org.nuxeo.ecm.platform.ui.web.tag.handler.LeafFaceletHandler;
052import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
053
054/**
055 * Helper class for options generation depending on the widget definition
056 *
057 * @since 5.4.2
058 */
059public abstract class AbstractSelectWidgetTypeHandler extends AbstractWidgetTypeHandler {
060
061    protected enum SelectPropertyMappings {
062        selectOptions, var, itemLabel, resolveItemLabelTwice, itemLabelPrefix, itemLabelPrefixSeparator,
063        //
064        itemLabelSuffix, itemLabelSuffixSeparator, itemValue,
065        //
066        itemRendered, itemDisabled, itemEscaped, ordering, caseSensitive,
067        //
068        displayIdAndLabel, displayIdAndLabelSeparator, notDisplayDefaultOption,
069        //
070        localize, dbl10n;
071    }
072
073    public AbstractSelectWidgetTypeHandler(TagConfig config) {
074        super(config);
075    }
076
077    // ease up override of behavior without impacting default options
078    // management
079    protected Map<String, Serializable> getOptionProperties(FaceletContext ctx, Widget widget,
080            WidgetSelectOption selectOption) {
081        Map<String, Serializable> props = new HashMap<>();
082        for (SelectPropertyMappings mapping : SelectPropertyMappings.values()) {
083            if (widget.getProperties().containsKey(mapping.name())) {
084                props.put(mapping.name(), widget.getProperty(mapping.name()));
085            }
086        }
087        return props;
088    }
089
090    protected String getOptionComponentType(WidgetSelectOption selectOption) {
091        if (selectOption instanceof WidgetSelectOptions) {
092            return UISelectItems.COMPONENT_TYPE;
093        } else {
094            return UISelectItem.COMPONENT_TYPE;
095        }
096    }
097
098    protected FaceletHandler getOptionFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget,
099            WidgetSelectOption selectOption, FaceletHandler nextHandler) {
100        String componentType = getOptionComponentType(selectOption);
101        TagAttributes attrs = helper.getTagAttributes(selectOption, getOptionProperties(ctx, widget, selectOption));
102        return helper.getHtmlComponentHandler(widget.getTagConfigId(), attrs, nextHandler, componentType, null);
103    }
104
105    // not impacted by custom behaviour by default
106    protected FaceletHandler getBareOptionFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper,
107            Widget widget, WidgetSelectOption selectOption, FaceletHandler nextHandler) {
108        String componentType = getOptionComponentType(selectOption);
109        TagAttributes attrs = helper.getTagAttributes(selectOption);
110        return helper.getHtmlComponentHandler(widget.getTagConfigId(), attrs, nextHandler, componentType, null);
111    }
112
113    /**
114     * Adds a default disabled "select a value" option if widget is not required.
115     *
116     * @since 6.0
117     */
118    protected FaceletHandler getFirstHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget,
119            FaceletHandler nextHandler) {
120        Object doNotDisplay = widget.getProperty(SelectPropertyMappings.notDisplayDefaultOption.name());
121        if (doNotDisplay != null) {
122            if (Boolean.TRUE.equals(doNotDisplay)) {
123                return null;
124            }
125            if (doNotDisplay instanceof String) {
126                Object res = ComponentTagUtils.resolveElExpression(ctx, (String) doNotDisplay);
127                if ((res instanceof Boolean && Boolean.TRUE.equals(res))
128                        || (res instanceof String && Boolean.parseBoolean((String) res))) {
129                    return null;
130                }
131            }
132        }
133        String bundleName = ctx.getFacesContext().getApplication().getMessageBundle();
134        String localizedExpression = "#{" + bundleName + "['label.vocabulary.selectValue']}";
135        WidgetSelectOption selectOption = new WidgetSelectOptionImpl("", "", localizedExpression, "", Boolean.FALSE,
136                Boolean.TRUE);
137        return getBareOptionFaceletHandler(ctx, helper, widget, selectOption, nextHandler);
138    }
139
140    /**
141     * Returns true if widget properties should generate a default tag handler for select options.
142     * <p>
143     * This default implementation requires the selectOptions widget property to be filled.
144     *
145     * @since 6.0
146     */
147    protected boolean shouldAddWidgetPropsHandler(Widget widget) {
148        if (widget.getProperties().containsKey(SelectPropertyMappings.selectOptions.name())) {
149            return true;
150        }
151        return false;
152    }
153
154    /**
155     * Computes select options from widget properties.
156     *
157     * @since 6.0
158     */
159    protected FaceletHandler getWidgetPropsHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget,
160            FaceletHandler nextHandler) {
161        if (shouldAddWidgetPropsHandler(widget)) {
162            WidgetSelectOptionsImpl selectOption = new WidgetSelectOptionsImpl(
163                    widget.getProperty(SelectPropertyMappings.selectOptions.name()),
164                    (String) widget.getProperty(SelectPropertyMappings.var.name()),
165                    (String) widget.getProperty(SelectPropertyMappings.itemLabel.name()),
166                    (String) widget.getProperty(SelectPropertyMappings.itemValue.name()),
167                    widget.getProperty(SelectPropertyMappings.itemDisabled.name()),
168                    widget.getProperty(SelectPropertyMappings.itemRendered.name()));
169            return getOptionFaceletHandler(ctx, helper, widget, selectOption, nextHandler);
170        }
171        return null;
172    }
173
174    protected FaceletHandler getOptionsFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget,
175            WidgetSelectOption[] selectOptions) {
176        FaceletHandler leaf = new LeafFaceletHandler();
177        List<FaceletHandler> selectItems = new ArrayList<FaceletHandler>();
178        FaceletHandler firstItem = getFirstHandler(ctx, helper, widget, leaf);
179        if (firstItem != null) {
180            selectItems.add(firstItem);
181        }
182        FaceletHandler widgetPropsHandler = getWidgetPropsHandler(ctx, helper, widget, leaf);
183        if (widgetPropsHandler != null) {
184            selectItems.add(widgetPropsHandler);
185        }
186        if (selectOptions != null && selectOptions.length > 0) {
187            for (WidgetSelectOption selectOption : selectOptions) {
188                if (selectOption == null) {
189                    continue;
190                }
191                FaceletHandler h = getBareOptionFaceletHandler(ctx, helper, widget, selectOption, leaf);
192                if (h != null) {
193                    selectItems.add(h);
194                }
195            }
196        }
197        return new CompositeFaceletHandler(selectItems.toArray(new FaceletHandler[0]));
198    }
199
200    protected FaceletHandler getOptionsFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget) {
201        return getOptionsFaceletHandler(ctx, helper, widget, widget.getSelectOptions());
202    }
203
204    /**
205     * Returns properties useful for select items, not to be reported on the select component.
206     */
207    protected List<String> getExcludedProperties() {
208        List<String> excludedProps = new ArrayList<>();
209        // BBB
210        excludedProps.add("cssStyle");
211        excludedProps.add("cssStyleClass");
212        for (SelectPropertyMappings mapping : SelectPropertyMappings.values()) {
213            excludedProps.add(mapping.name());
214        }
215        return excludedProps;
216    }
217
218    protected void apply(FaceletContext ctx, UIComponent parent, Widget widget, String componentType)
219            throws WidgetException, IOException {
220        apply(ctx, parent, widget, componentType, null);
221    }
222
223    protected TagHandler getComponentFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, Widget widget,
224            TagHandler componentHandler) {
225        return componentHandler;
226    }
227
228    protected void apply(FaceletContext ctx, UIComponent parent, Widget widget, String componentType,
229            String rendererType) throws WidgetException, IOException {
230        FaceletHandlerHelper helper = new FaceletHandlerHelper(tagConfig);
231        String mode = widget.getMode();
232        String widgetId = widget.getId();
233        String widgetName = widget.getName();
234        String widgetTagConfigId = widget.getTagConfigId();
235        List<String> excludedProps = getExcludedProperties();
236        TagAttributes attributes = helper.getTagAttributes(widget, excludedProps, true);
237        // BBB for CSS style classes on directory select components
238        if (widget.getProperty("cssStyle") != null) {
239            attributes = FaceletHandlerHelper.addTagAttribute(attributes,
240                    helper.createAttribute("style", (String) widget.getProperty("cssStyle")));
241        }
242        if (widget.getProperty("cssStyleClass") != null) {
243            attributes = FaceletHandlerHelper.addTagAttribute(attributes,
244                    helper.createAttribute("styleClass", (String) widget.getProperty("cssStyleClass")));
245        }
246        if (!BuiltinWidgetModes.isLikePlainMode(mode)) {
247            attributes = FaceletHandlerHelper.addTagAttribute(attributes, helper.createAttribute("id", widgetId));
248        }
249        if (BuiltinWidgetModes.EDIT.equals(mode)) {
250            FaceletHandler optionsHandler = getOptionsFaceletHandler(ctx, helper, widget);
251            FaceletHandler[] nextHandlers = new FaceletHandler[] {};
252            nextHandlers = (FaceletHandler[]) ArrayUtils.add(nextHandlers, optionsHandler);
253            FaceletHandler leaf = getNextHandler(ctx, tagConfig, widget, nextHandlers, helper, true, true);
254            // maybe add convert handler for easier integration of select2
255            // widgets handling multiple values
256            if (HtmlSelectManyListbox.COMPONENT_TYPE.equals(componentType)
257                    || HtmlSelectManyCheckbox.COMPONENT_TYPE.equals(componentType)
258                    || HtmlSelectManyMenu.COMPONENT_TYPE.equals(componentType)) {
259                // add hint for value conversion to collection
260                attributes = FaceletHandlerHelper.addTagAttribute(attributes,
261                        helper.createAttribute("collectionType", ArrayList.class.getName()));
262            }
263
264            ComponentHandler input = helper.getHtmlComponentHandler(widgetTagConfigId, attributes, leaf, componentType,
265                    rendererType);
266            String msgId = FaceletHandlerHelper.generateMessageId(ctx, widgetName);
267            ComponentHandler message = helper.getMessageComponentHandler(widgetTagConfigId, msgId, widgetId, null);
268            FaceletHandler[] handlers = { getComponentFaceletHandler(ctx, helper, widget, input), message };
269            FaceletHandler h = new CompositeFaceletHandler(handlers);
270            h.apply(ctx, parent);
271        } else {
272            // TODO
273            return;
274        }
275    }
276}