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