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 *     <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
018 *
019 * $Id: LayoutTagHandler.java 30553 2008-02-24 15:51:31Z atchertchian $
020 */
021
022package org.nuxeo.ecm.platform.forms.layout.facelets;
023
024import java.io.IOException;
025import java.io.Serializable;
026import java.util.ArrayList;
027import java.util.Arrays;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031
032import javax.el.ELException;
033import javax.el.ExpressionFactory;
034import javax.el.ValueExpression;
035import javax.el.VariableMapper;
036import javax.faces.FacesException;
037import javax.faces.component.UIComponent;
038import javax.faces.view.facelets.ComponentHandler;
039import javax.faces.view.facelets.FaceletContext;
040import javax.faces.view.facelets.FaceletHandler;
041import javax.faces.view.facelets.TagAttribute;
042import javax.faces.view.facelets.TagAttributes;
043import javax.faces.view.facelets.TagConfig;
044import javax.faces.view.facelets.TagException;
045import javax.faces.view.facelets.TagHandler;
046
047import org.apache.commons.lang.StringUtils;
048import org.apache.commons.logging.Log;
049import org.apache.commons.logging.LogFactory;
050import org.nuxeo.ecm.platform.forms.layout.api.Layout;
051import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition;
052import org.nuxeo.ecm.platform.forms.layout.api.Widget;
053import org.nuxeo.ecm.platform.forms.layout.facelets.dev.DevTagHandler;
054import org.nuxeo.ecm.platform.forms.layout.facelets.dev.LayoutDevTagHandler;
055import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
056import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory;
057import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
058import org.nuxeo.runtime.api.Framework;
059
060import com.sun.faces.facelets.el.VariableMapperWrapper;
061import com.sun.faces.facelets.tag.ui.DecorateHandler;
062
063/**
064 * Layout tag handler.
065 * <p>
066 * Computes a layout in given facelet context, for given mode and value attributes. The layout can either be computed
067 * from a layout definition, or by a layout name, where the layout service will lookup the corresponding definition.
068 * <p>
069 * If a template is found for this layout, include the corresponding facelet and use facelet template features to
070 * iterate over rows and widgets.
071 * <p>
072 * Since 5.6, the layout name attribute also accepts a comma separated list of layout names.
073 *
074 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
075 */
076public class LayoutTagHandler extends TagHandler {
077
078    private static final Log log = LogFactory.getLog(LayoutTagHandler.class);
079
080    protected final TagConfig config;
081
082    /**
083     * The layout instance to render, instead of resolving it from a name or definition
084     *
085     * @since 5.7
086     */
087    protected final TagAttribute layout;
088
089    protected final TagAttribute name;
090
091    /**
092     * @since 5.5.
093     */
094    protected final TagAttribute category;
095
096    /**
097     * @since 5.4.2
098     */
099    protected final TagAttribute definition;
100
101    protected final TagAttribute mode;
102
103    protected final TagAttribute value;
104
105    protected final TagAttribute template;
106
107    protected final TagAttribute selectedRows;
108
109    protected final TagAttribute selectedColumns;
110
111    protected final TagAttribute selectAllByDefault;
112
113    /**
114     * Parameter used to specify that layout should not be rendered, only resolved and exposed to the context.
115     *
116     * @since 5.7
117     */
118    protected final TagAttribute resolveOnly;
119
120    protected final TagAttribute[] vars;
121
122    protected final String[] reservedVarsArray = { "id", "layout", "name", "category", "definition", "mode", "value",
123            "template", "selectedRows", "selectedColumns", "selectAllByDefault", "resolveOnly" };
124
125    public LayoutTagHandler(TagConfig config) {
126        super(config);
127        this.config = config;
128        name = getAttribute("name");
129        category = getAttribute("category");
130        definition = getAttribute("definition");
131        layout = getAttribute("layout");
132        if (name == null && definition == null && layout == null) {
133            throw new TagException(this.tag, "At least one of attributes 'name', 'layout' or 'definition'"
134                    + " is required");
135        }
136        mode = getAttribute("mode");
137        value = getRequiredAttribute("value");
138        if (layout == null && (name != null || definition != null)) {
139            if (mode == null) {
140                throw new TagException(this.tag, "Attribute 'mode' is required when using attribute"
141                        + " 'name' or 'definition' so that the " + "layout instance can be resolved");
142            }
143        }
144        template = getAttribute("template");
145        selectedRows = getAttribute("selectedRows");
146        selectedColumns = getAttribute("selectedColumns");
147        if (selectedRows != null && selectedColumns != null) {
148            throw new TagException(this.tag, "Attributes 'selectedRows' "
149                    + "and 'selectedColumns' are aliases: only one of " + "them should be filled");
150        }
151        selectAllByDefault = getAttribute("selectAllByDefault");
152        resolveOnly = getAttribute("resolveOnly");
153        vars = tag.getAttributes().getAll();
154    }
155
156    @SuppressWarnings("unchecked")
157    // TODO: add javadoc about variables exposed
158    public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException {
159        WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class);
160
161        // add additional properties put on tag
162        Map<String, Serializable> additionalProps = new HashMap<String, Serializable>();
163        List<String> reservedVars = Arrays.asList(reservedVarsArray);
164        for (TagAttribute var : vars) {
165            String localName = var.getLocalName();
166            if (!reservedVars.contains(localName)) {
167                // resolve value as there's no alias value expression exposed
168                // for layout properties
169                additionalProps.put(localName, (Serializable) var.getObject(ctx));
170            }
171        }
172
173        // expose some layout variables before layout creation so that they
174        // can be used in mode expressions
175        VariableMapper orig = ctx.getVariableMapper();
176        VariableMapper vm = new VariableMapperWrapper(orig);
177        ctx.setVariableMapper(vm);
178
179        FaceletHandlerHelper helper = new FaceletHandlerHelper(config);
180        try {
181            Layout layoutInstance = null;
182
183            String valueName = value.getValue();
184            if (ComponentTagUtils.isStrictValueReference(valueName)) {
185                valueName = ComponentTagUtils.getBareValueName(valueName);
186            }
187
188            String templateValue = null;
189            if (template != null) {
190                templateValue = template.getValue(ctx);
191            }
192
193            boolean resolveOnlyValue = false;
194            if (resolveOnly != null) {
195                resolveOnlyValue = resolveOnly.getBoolean(ctx);
196            }
197
198            if (layout != null) {
199                // resolve layout instance given as attribute
200                layoutInstance = (Layout) layout.getObject(ctx, Layout.class);
201                if (layoutInstance == null) {
202                    String errMsg = "Layout instance not found";
203                    applyErrorHandler(ctx, parent, helper, errMsg);
204                } else {
205                    Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, layoutInstance.getMode());
206                    for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
207                        vm.setVariable(var.getKey(), var.getValue());
208                    }
209                    layoutInstance.setValueName(valueName);
210                    applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
211                            additionalProps, vars, resolveOnlyValue);
212                }
213            } else {
214                // build layout instance from other attributes
215                String modeValue = mode.getValue(ctx);
216
217                List<String> selectedRowsValue = null;
218                boolean selectAllByDefaultValue = false;
219
220                Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, modeValue);
221                for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
222                    vm.setVariable(var.getKey(), var.getValue());
223                }
224
225                if (selectedRows != null || selectedColumns != null) {
226                    if (selectedRows != null) {
227                        selectedRowsValue = (List<String>) selectedRows.getObject(ctx, List.class);
228                    } else if (selectedColumns != null) {
229                        List<String> selectedColumnsList = (List<String>) selectedColumns.getObject(ctx, List.class);
230                        // Handle empty selected columns list as null to
231                        // display all columns.
232                        if (selectedColumnsList != null && selectedColumnsList.isEmpty()) {
233                            selectedColumnsList = null;
234                        }
235                        selectedRowsValue = selectedColumnsList;
236                    }
237                }
238                if (selectAllByDefault != null) {
239                    selectAllByDefaultValue = selectAllByDefault.getBoolean(ctx);
240                }
241
242                if (name != null) {
243                    String layoutCategory = null;
244                    if (category != null) {
245                        layoutCategory = category.getValue(ctx);
246                    }
247
248                    String nameValue = name.getValue(ctx);
249                    List<String> layoutNames = resolveLayoutNames(nameValue);
250                    for (String layoutName : layoutNames) {
251                        layoutInstance = layoutService.getLayout(ctx, layoutName, layoutCategory, modeValue, valueName,
252                                selectedRowsValue, selectAllByDefaultValue);
253                        if (layoutInstance == null) {
254                            String errMsg = "Layout '" + layoutName + "' not found";
255                            applyErrorHandler(ctx, parent, helper, errMsg);
256                        } else {
257                            applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
258                                    additionalProps, vars, resolveOnlyValue);
259                        }
260                    }
261                }
262
263                if (definition != null) {
264                    LayoutDefinition layoutDef = (LayoutDefinition) definition.getObject(ctx, LayoutDefinition.class);
265
266                    if (layoutDef == null) {
267                        String errMsg = "Layout definition resolved to null";
268                        applyErrorHandler(ctx, parent, helper, errMsg);
269                    } else {
270                        layoutInstance = layoutService.getLayout(ctx, layoutDef, modeValue, valueName,
271                                selectedRowsValue, selectAllByDefaultValue);
272                        applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
273                                additionalProps, vars, resolveOnlyValue);
274                    }
275                }
276            }
277
278        } finally {
279            // layout resolved => cleanup variable mapper
280            ctx.setVariableMapper(orig);
281        }
282
283    }
284
285    /**
286     * Resolves layouts names, splitting on character "," and trimming resulting names, and allowing empty strings if
287     * the whole string is not empty to ease up rendering of layout names using variables.
288     * <p>
289     * For instance, if value is null or empty, will return a single empty layout name "". If value is "," it will
290     * return an empty list, triggering no error for usage like <nxl:layout name="#{myLayout}, #{myOtherLayout}" [...]
291     * />
292     */
293    protected List<String> resolveLayoutNames(String nameValue) {
294        List<String> res = new ArrayList<String>();
295        if (nameValue != null) {
296            String[] split = nameValue.split(",|\\s");
297            if (split != null) {
298                for (String item : split) {
299                    if (!StringUtils.isBlank(item)) {
300                        res.add(item.trim());
301                    }
302                }
303            }
304        }
305        return res;
306    }
307
308    protected void applyLayoutHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper,
309            WebLayoutManager layoutService, Layout layoutInstance, String templateValue,
310            Map<String, Serializable> additionalProps, Map<String, ValueExpression> vars, boolean resolveOnly)
311            throws IOException, FacesException, ELException {
312
313        // set unique id on layout, unless layout is only resolved
314        if (!resolveOnly) {
315            layoutInstance.setId(FaceletHandlerHelper.generateLayoutId(ctx, layoutInstance.getName()));
316        }
317
318        // add additional properties put on tag
319        Map<String, Serializable> layoutProps = layoutInstance.getProperties();
320        if (additionalProps != null && !additionalProps.isEmpty()) {
321            for (Map.Entry<String, Serializable> entry : additionalProps.entrySet()) {
322                // XXX: do not override with empty property values if already
323                // set on the layout properties
324                String key = entry.getKey();
325                Serializable value = entry.getValue();
326                if (layoutProps.containsKey(key)
327                        && (value == null || ((value instanceof String) && StringUtils.isBlank((String) value)))) {
328                    // do not override property on layout
329                    if (log.isDebugEnabled()) {
330                        log.debug(String.format("Do not override property '%s' with "
331                                + "empty value on layout named '%s'", key, layoutInstance.getName()));
332                    }
333                } else {
334                    layoutInstance.setProperty(key, value);
335                }
336            }
337        }
338
339        if (StringUtils.isBlank(templateValue)) {
340            templateValue = layoutInstance.getTemplate();
341        }
342
343        if (!resolveOnly) {
344            boolean scaffold = Boolean.parseBoolean(String.valueOf(layoutInstance.getProperty("scaffold")));
345            if (scaffold) {
346                // generate ids on widgets
347                Map<String, Widget> widgetMap = layoutInstance.getWidgetMap();
348                if (widgetMap != null) {
349                    for (Widget widget : widgetMap.values()) {
350                        if (widget != null && (widget.getId() == null)) {
351                            WidgetTagHandler.generateWidgetId(ctx, helper, widget, false);
352                        }
353                    }
354                }
355            }
356        }
357
358        // expose layout instance to variable mapper to ensure good
359        // resolution of properties
360        ExpressionFactory eFactory = ctx.getExpressionFactory();
361        ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class);
362        ctx.getVariableMapper().setVariable(RenderVariables.layoutVariables.layout.name(), layoutVe);
363
364        // expose all variables through an alias tag handler
365        vars.putAll(getVariablesForLayoutRendering(ctx, layoutService, layoutInstance));
366
367        List<String> blockedPatterns = new ArrayList<String>();
368        blockedPatterns.add(RenderVariables.layoutVariables.layout.name());
369        blockedPatterns.add(RenderVariables.layoutVariables.layoutProperty.name() + "_*");
370
371        final String layoutTagConfigId = layoutInstance.getTagConfigId();
372
373        if (resolveOnly) {
374            FaceletHandler handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns,
375                    nextHandler);
376            // apply
377            handler.apply(ctx, parent);
378        } else {
379            if (!StringUtils.isBlank(templateValue)) {
380                TagAttribute srcAttr = helper.createAttribute("template", templateValue);
381                TagConfig config = TagConfigFactory.createTagConfig(this.config, layoutTagConfigId,
382                        FaceletHandlerHelper.getTagAttributes(srcAttr), nextHandler);
383                FaceletHandler includeHandler = new DecorateHandler(config);
384                FaceletHandler handler;
385                if (FaceletHandlerHelper.isDevModeEnabled(ctx)) {
386                    // decorate handler with dev handler
387                    FaceletHandler devHandler = getDevFaceletHandler(ctx, helper, config, layoutInstance);
388                    FaceletHandler nextHandler;
389                    if (devHandler == null) {
390                        nextHandler = includeHandler;
391                    } else {
392                        nextHandler = new DevTagHandler(config, layoutInstance.getName(), includeHandler, devHandler);
393                    }
394                    handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns, nextHandler);
395                } else {
396                    handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns, includeHandler);
397                }
398                // apply
399                handler.apply(ctx, parent);
400            } else {
401                String errMsg = "Missing template property for layout '" + layoutInstance.getName() + "'";
402                applyErrorHandler(ctx, parent, helper, errMsg);
403            }
404        }
405    }
406
407    protected Map<String, ValueExpression> getVariablesForLayoutBuild(FaceletContext ctx, String modeValue) {
408        Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
409        ValueExpression valueExpr = value.getValueExpression(ctx, Object.class);
410        vars.put(RenderVariables.globalVariables.value.name(), valueExpr);
411        // vars.put(RenderVariables.globalVariables.document.name(),
412        // valueExpr);
413        vars.put(RenderVariables.globalVariables.layoutValue.name(), valueExpr);
414        ExpressionFactory eFactory = ctx.getExpressionFactory();
415        ValueExpression modeVe = eFactory.createValueExpression(modeValue, String.class);
416        vars.put(RenderVariables.globalVariables.layoutMode.name(), modeVe);
417        // mode as alias to layoutMode
418        vars.put(RenderVariables.globalVariables.mode.name(), modeVe);
419        return vars;
420    }
421
422    /**
423     * Computes variables for rendering, making available the layout instance and its properties to the context.
424     */
425    protected Map<String, ValueExpression> getVariablesForLayoutRendering(FaceletContext ctx,
426            WebLayoutManager layoutService, Layout layoutInstance) {
427        Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
428        ExpressionFactory eFactory = ctx.getExpressionFactory();
429
430        // expose layout value
431        ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class);
432        vars.put(RenderVariables.layoutVariables.layout.name(), layoutVe);
433
434        // expose layout properties too
435        for (Map.Entry<String, Serializable> prop : layoutInstance.getProperties().entrySet()) {
436            String key = prop.getKey();
437            String name = RenderVariables.layoutVariables.layoutProperty.name() + "_" + key;
438            String value;
439            Serializable valueInstance = prop.getValue();
440            if (!layoutService.referencePropertyAsExpression(key, valueInstance, null, null, null, null)) {
441                // FIXME: this will not be updated correctly using ajax
442                value = (String) valueInstance;
443            } else {
444                // create a reference so that it's a real expression and it's
445                // not kept (cached) in a component value on ajax refresh
446                value = "#{" + RenderVariables.layoutVariables.layout.name() + ".properties." + key + "}";
447            }
448            vars.put(name, eFactory.createValueExpression(ctx, value, Object.class));
449        }
450
451        return vars;
452    }
453
454    protected void applyErrorHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper, String message)
455            throws IOException {
456        log.error(message);
457        ComponentHandler output = helper.getErrorComponentHandler(null, message);
458        output.apply(ctx, parent);
459    }
460
461    protected FaceletHandler getDevFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, TagConfig config,
462            Layout layout) {
463        if (StringUtils.isBlank(layout.getDevTemplate())) {
464            return null;
465        }
466        // use the default dev handler for widget types
467        TagAttribute attr = helper.createAttribute("layout",
468                "#{" + RenderVariables.layoutVariables.layout.name() + "}");
469        TagAttributes devWidgetAttributes = FaceletHandlerHelper.getTagAttributes(attr);
470        TagConfig devWidgetConfig = TagConfigFactory.createTagConfig(config, layout.getTagConfigId(),
471                devWidgetAttributes, new org.nuxeo.ecm.platform.ui.web.tag.handler.LeafFaceletHandler());
472        return new LayoutDevTagHandler(devWidgetConfig);
473    }
474}