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.ComponentConfig;
039import javax.faces.view.facelets.ComponentHandler;
040import javax.faces.view.facelets.FaceletContext;
041import javax.faces.view.facelets.FaceletHandler;
042import javax.faces.view.facelets.TagAttribute;
043import javax.faces.view.facelets.TagAttributes;
044import javax.faces.view.facelets.TagConfig;
045import javax.faces.view.facelets.TagException;
046import javax.faces.view.facelets.TagHandler;
047
048import org.apache.commons.lang.StringUtils;
049import org.apache.commons.logging.Log;
050import org.apache.commons.logging.LogFactory;
051import org.nuxeo.ecm.platform.forms.layout.api.Layout;
052import org.nuxeo.ecm.platform.forms.layout.api.LayoutDefinition;
053import org.nuxeo.ecm.platform.forms.layout.api.Widget;
054import org.nuxeo.ecm.platform.forms.layout.facelets.dev.DevTagHandler;
055import org.nuxeo.ecm.platform.forms.layout.facelets.dev.LayoutDevTagHandler;
056import org.nuxeo.ecm.platform.forms.layout.service.WebLayoutManager;
057import org.nuxeo.ecm.platform.ui.web.binding.BlockingVariableMapper;
058import org.nuxeo.ecm.platform.ui.web.tag.handler.TagConfigFactory;
059import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
060import org.nuxeo.ecm.platform.ui.web.util.FaceletDebugTracer;
061import org.nuxeo.runtime.api.Framework;
062
063import com.sun.faces.facelets.el.VariableMapperWrapper;
064import com.sun.faces.facelets.tag.TagAttributeImpl;
065import com.sun.faces.facelets.tag.TagAttributesImpl;
066import com.sun.faces.facelets.tag.ui.ComponentRef;
067import com.sun.faces.facelets.tag.ui.ComponentRefHandler;
068import com.sun.faces.facelets.tag.ui.DecorateHandler;
069
070/**
071 * Layout tag handler.
072 * <p>
073 * Computes a layout in given facelet context, for given mode and value attributes. The layout can either be computed
074 * from a layout definition, or by a layout name, where the layout service will lookup the corresponding definition.
075 * <p>
076 * If a template is found for this layout, include the corresponding facelet and use facelet template features to
077 * iterate over rows and widgets.
078 * <p>
079 * Since 5.6, the layout name attribute also accepts a comma separated list of layout names.
080 *
081 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
082 */
083public class LayoutTagHandler extends TagHandler {
084
085    private static final Log log = LogFactory.getLog(LayoutTagHandler.class);
086
087    protected final TagConfig config;
088
089    /**
090     * The layout instance to render, instead of resolving it from a name or definition
091     *
092     * @since 5.7
093     */
094    protected final TagAttribute layout;
095
096    protected final TagAttribute name;
097
098    /**
099     * @since 5.5.
100     */
101    protected final TagAttribute category;
102
103    /**
104     * @since 5.4.2
105     */
106    protected final TagAttribute definition;
107
108    protected final TagAttribute mode;
109
110    protected final TagAttribute value;
111
112    protected final TagAttribute template;
113
114    protected final TagAttribute selectedRows;
115
116    protected final TagAttribute selectedColumns;
117
118    protected final TagAttribute selectAllByDefault;
119
120    /**
121     * Parameter used to specify that layout should not be rendered, only resolved and exposed to the context.
122     *
123     * @since 5.7
124     */
125    protected final TagAttribute resolveOnly;
126
127    protected final TagAttribute[] vars;
128
129    protected final String[] reservedVarsArray = { "id", "layout", "name", "category", "definition", "mode", "value",
130            "template", "selectedRows", "selectedColumns", "selectAllByDefault", "resolveOnly" };
131
132    public LayoutTagHandler(TagConfig config) {
133        super(config);
134        this.config = config;
135        name = getAttribute("name");
136        category = getAttribute("category");
137        definition = getAttribute("definition");
138        layout = getAttribute("layout");
139        if (name == null && definition == null && layout == null) {
140            throw new TagException(this.tag, "At least one of attributes 'name', 'layout' or 'definition' is required");
141        }
142        mode = getAttribute("mode");
143        value = getRequiredAttribute("value");
144        if (layout == null && (name != null || definition != null)) {
145            if (mode == null) {
146                throw new TagException(this.tag, "Attribute 'mode' is required when using attribute"
147                        + " 'name' or 'definition' so that the layout instance can be resolved");
148            }
149        }
150        template = getAttribute("template");
151        selectedRows = getAttribute("selectedRows");
152        selectedColumns = getAttribute("selectedColumns");
153        if (selectedRows != null && selectedColumns != null) {
154            throw new TagException(this.tag, "Attributes 'selectedRows' "
155                    + "and 'selectedColumns' are aliases: only one of them should be filled");
156        }
157        selectAllByDefault = getAttribute("selectAllByDefault");
158        resolveOnly = getAttribute("resolveOnly");
159        vars = tag.getAttributes().getAll();
160    }
161
162    @SuppressWarnings("unchecked")
163    // TODO: add javadoc about variables exposed
164    public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException {
165        if (!FaceletHandlerHelper.isAliasOptimEnabled()) {
166            applyCompat(ctx, parent);
167            return;
168        }
169
170        long start = FaceletDebugTracer.start();
171        String logId = null;
172        try {
173
174            WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class);
175
176            // add additional properties put on tag
177            Map<String, Serializable> additionalProps = new HashMap<String, Serializable>();
178            List<String> reservedVars = Arrays.asList(reservedVarsArray);
179            for (TagAttribute var : vars) {
180                String localName = var.getLocalName();
181                if (!reservedVars.contains(localName)) {
182                    // resolve value as there's no alias value expression exposed
183                    // for layout properties
184                    additionalProps.put(localName, (Serializable) var.getObject(ctx));
185                }
186            }
187
188            VariableMapper orig = ctx.getVariableMapper();
189
190            try {
191                // expose some layout variables before layout creation so that they
192                // can be used in mode expressions
193                BlockingVariableMapper vm = new BlockingVariableMapper(orig);
194                ctx.setVariableMapper(vm);
195
196                FaceletHandlerHelper helper = new FaceletHandlerHelper(config);
197                ExpressionFactory eFactory = ctx.getExpressionFactory();
198                Layout layoutInstance = null;
199
200                String valueName = value.getValue();
201                if (ComponentTagUtils.isStrictValueReference(valueName)) {
202                    valueName = ComponentTagUtils.getBareValueName(valueName);
203                }
204
205                String templateValue = null;
206                if (template != null) {
207                    templateValue = template.getValue(ctx);
208                }
209
210                boolean resolveOnlyValue = false;
211                if (resolveOnly != null) {
212                    resolveOnlyValue = resolveOnly.getBoolean(ctx);
213                }
214
215                if (layout != null) {
216                    // resolve layout instance given as attribute
217                    layoutInstance = (Layout) layout.getObject(ctx, Layout.class);
218                    if (layoutInstance == null) {
219                        String errMsg = "Layout instance not found";
220                        applyErrorHandler(ctx, parent, helper, errMsg);
221                    } else {
222                        fillVariablesForLayoutBuild(ctx, eFactory, vm, layoutInstance.getMode());
223                        layoutInstance.setValueName(valueName);
224                        applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
225                                additionalProps, vm, resolveOnlyValue);
226                    }
227                } else {
228                    // build layout instance from other attributes
229                    String modeValue = mode.getValue(ctx);
230
231                    List<String> selectedRowsValue = null;
232                    boolean selectAllByDefaultValue = false;
233
234                    fillVariablesForLayoutBuild(ctx, eFactory, vm, modeValue);
235
236                    if (selectedRows != null || selectedColumns != null) {
237                        if (selectedRows != null) {
238                            selectedRowsValue = (List<String>) selectedRows.getObject(ctx, List.class);
239                        } else if (selectedColumns != null) {
240                            List<String> selectedColumnsList = (List<String>) selectedColumns.getObject(ctx,
241                                    List.class);
242                            // Handle empty selected columns list as null to
243                            // display all columns.
244                            if (selectedColumnsList != null && selectedColumnsList.isEmpty()) {
245                                selectedColumnsList = null;
246                            }
247                            selectedRowsValue = selectedColumnsList;
248                        }
249                    }
250                    if (selectAllByDefault != null) {
251                        selectAllByDefaultValue = selectAllByDefault.getBoolean(ctx);
252                    }
253
254                    if (name != null) {
255                        String layoutCategory = null;
256                        if (category != null) {
257                            layoutCategory = category.getValue(ctx);
258                        }
259
260                        String nameValue = name.getValue(ctx);
261                        List<String> layoutNames = resolveLayoutNames(nameValue);
262                        logId = layoutNames.toString();
263                        for (String layoutName : layoutNames) {
264                            layoutInstance = layoutService.getLayout(ctx, layoutName, layoutCategory, modeValue,
265                                    valueName, selectedRowsValue, selectAllByDefaultValue);
266                            if (layoutInstance == null) {
267                                String errMsg = "Layout '" + layoutName + "' not found";
268                                applyErrorHandler(ctx, parent, helper, errMsg);
269                            } else {
270                                applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
271                                        additionalProps, vm, resolveOnlyValue);
272                            }
273                        }
274                    }
275
276                    if (definition != null) {
277                        LayoutDefinition layoutDef = (LayoutDefinition) definition.getObject(ctx,
278                                LayoutDefinition.class);
279
280                        if (layoutDef == null) {
281                            String errMsg = "Layout definition resolved to null";
282                            applyErrorHandler(ctx, parent, helper, errMsg);
283                        } else {
284                            layoutInstance = layoutService.getLayout(ctx, layoutDef, modeValue, valueName,
285                                    selectedRowsValue, selectAllByDefaultValue);
286                            applyLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
287                                    additionalProps, vm, resolveOnlyValue);
288                            if (layoutInstance != null) {
289                                logId = layoutInstance.getId();
290                            } else {
291                                logId = layoutDef.getName() + " (def)";
292                            }
293                        }
294                    }
295                }
296
297            } finally {
298                // layout resolved => cleanup variable mapper
299                ctx.setVariableMapper(orig);
300            }
301
302        } finally {
303            FaceletDebugTracer.trace(start, config.getTag(), logId);
304        }
305    }
306
307    /**
308     * Resolves layouts names, splitting on character "," and trimming resulting names, and allowing empty strings if
309     * the whole string is not empty to ease up rendering of layout names using variables.
310     * <p>
311     * For instance, if value is null or empty, will return a single empty layout name "". If value is "," it will
312     * return an empty list, triggering no error for usage like <nxl:layout name="#{myLayout}, #{myOtherLayout}" [...]
313     * />
314     */
315    protected List<String> resolveLayoutNames(String nameValue) {
316        List<String> res = new ArrayList<String>();
317        if (nameValue != null) {
318            String[] split = nameValue.split(",|\\s");
319            if (split != null) {
320                for (String item : split) {
321                    if (!StringUtils.isBlank(item)) {
322                        res.add(item.trim());
323                    }
324                }
325            }
326        }
327        return res;
328    }
329
330    protected void applyLayoutHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper,
331            WebLayoutManager layoutService, Layout layoutInstance, String templateValue,
332            Map<String, Serializable> additionalProps, BlockingVariableMapper vm, boolean resolveOnly)
333                    throws IOException, FacesException, ELException {
334
335        // set unique id on layout, unless layout is only resolved
336        if (!resolveOnly) {
337            layoutInstance.setId(FaceletHandlerHelper.generateLayoutId(ctx, layoutInstance.getName()));
338        }
339
340        // add additional properties put on tag
341        Map<String, Serializable> layoutProps = layoutInstance.getProperties();
342        if (additionalProps != null && !additionalProps.isEmpty()) {
343            for (Map.Entry<String, Serializable> entry : additionalProps.entrySet()) {
344                // XXX: do not override with empty property values if already
345                // set on the layout properties
346                String key = entry.getKey();
347                Serializable value = entry.getValue();
348                if (layoutProps.containsKey(key)
349                        && (value == null || ((value instanceof String) && StringUtils.isBlank((String) value)))) {
350                    // do not override property on layout
351                    if (log.isDebugEnabled()) {
352                        log.debug(String.format(
353                                "Do not override property '%s' with " + "empty value on layout named '%s'", key,
354                                layoutInstance.getName()));
355                    }
356                } else {
357                    layoutInstance.setProperty(key, value);
358                }
359            }
360        }
361
362        if (StringUtils.isBlank(templateValue)) {
363            templateValue = layoutInstance.getTemplate();
364        }
365
366        if (!resolveOnly) {
367            boolean scaffold = Boolean.parseBoolean(String.valueOf(layoutInstance.getProperty("scaffold")));
368            if (scaffold) {
369                // generate ids on widgets
370                Map<String, Widget> widgetMap = layoutInstance.getWidgetMap();
371                if (widgetMap != null) {
372                    for (Widget widget : widgetMap.values()) {
373                        if (widget != null && (widget.getId() == null)) {
374                            WidgetTagHandler.generateWidgetId(ctx, helper, widget, false);
375                        }
376                    }
377                }
378            }
379        }
380
381        // expose rendering variables
382        fillVariablesForLayoutRendering(ctx, ctx.getExpressionFactory(), layoutService, vm, layoutInstance);
383
384        final String layoutTagConfigId = layoutInstance.getTagConfigId();
385
386        if (resolveOnly) {
387            nextHandler.apply(ctx, parent);
388        } else {
389            if (!StringUtils.isBlank(templateValue)) {
390                TagAttribute srcAttr = helper.createAttribute("template", templateValue);
391                TagConfig config = TagConfigFactory.createTagConfig(this.config, layoutTagConfigId,
392                        FaceletHandlerHelper.getTagAttributes(srcAttr), nextHandler);
393                FaceletHandler templateHandler = new DecorateHandler(config);
394                // NXP-18639: always wrap next include handler in a component ref for tagConfigId to be taken into
395                // account and anchored in the view with this id.
396                ComponentConfig ref = TagConfigFactory.createComponentConfig(this.config, layoutTagConfigId,
397                        new TagAttributesImpl(new TagAttributeImpl[] {}), templateHandler, ComponentRef.COMPONENT_TYPE,
398                        null);
399                FaceletHandler includeHandler = new ComponentRefHandler(ref);
400                if (FaceletHandlerHelper.isDevModeEnabled(ctx)) {
401                    // decorate handler with dev handler
402                    FaceletHandler devHandler = getDevFaceletHandler(ctx, helper, config, layoutInstance);
403                    FaceletHandler nextHandler;
404                    if (devHandler == null) {
405                        nextHandler = includeHandler;
406                    } else {
407                        nextHandler = new DevTagHandler(config, layoutInstance.getName(), includeHandler, devHandler);
408                    }
409                    nextHandler.apply(ctx, parent);
410                } else {
411                    includeHandler.apply(ctx, parent);
412                }
413            } else {
414                String errMsg = "Missing template property for layout '" + layoutInstance.getName() + "'";
415                applyErrorHandler(ctx, parent, helper, errMsg);
416            }
417        }
418    }
419
420    protected void fillVariablesForLayoutBuild(FaceletContext ctx, ExpressionFactory eFactory,
421            BlockingVariableMapper vm, String modeValue) {
422        ValueExpression valueExpr = value.getValueExpression(ctx, Object.class);
423        vm.setVariable(RenderVariables.globalVariables.value.name(), valueExpr);
424        vm.setVariable(RenderVariables.globalVariables.layoutValue.name(), valueExpr);
425        ValueExpression modeVe = eFactory.createValueExpression(modeValue, String.class);
426        vm.setVariable(RenderVariables.globalVariables.layoutMode.name(), modeVe);
427        // mode as alias to layoutMode
428        vm.setVariable(RenderVariables.globalVariables.mode.name(), modeVe);
429    }
430
431    /**
432     * Computes variables for rendering, making available the layout instance and its properties to the context.
433     */
434    protected void fillVariablesForLayoutRendering(FaceletContext ctx, ExpressionFactory eFactory,
435            WebLayoutManager layoutService, BlockingVariableMapper vm, Layout layoutInstance) {
436        // expose layout value
437        ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class);
438        vm.setVariable(RenderVariables.layoutVariables.layout.name(), layoutVe);
439        vm.addBlockedPattern(RenderVariables.layoutVariables.layout.name());
440
441        // expose layout properties too
442        for (Map.Entry<String, Serializable> prop : layoutInstance.getProperties().entrySet()) {
443            String key = prop.getKey();
444            String name = RenderVariables.layoutVariables.layoutProperty.name() + "_" + key;
445            vm.setVariable(name, eFactory.createValueExpression(prop.getValue(), Object.class));
446        }
447        vm.addBlockedPattern(RenderVariables.layoutVariables.layoutProperty.name() + "_*");
448
449        // expose layout row count for row variables reference
450        Integer rowCount = null;
451        if (layoutInstance.getRows() != null) {
452            rowCount = layoutInstance.getRows().length;
453        }
454        vm.setVariable(RenderVariables.layoutVariables.layoutRowCount.name(),
455                eFactory.createValueExpression(rowCount, Integer.class));
456        vm.addBlockedPattern(RenderVariables.layoutVariables.layoutRowCount.name());
457    }
458
459    protected void applyErrorHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper,
460            String message) throws IOException {
461        log.error(message);
462        ComponentHandler output = helper.getErrorComponentHandler(null, message);
463        output.apply(ctx, parent);
464    }
465
466    protected FaceletHandler getDevFaceletHandler(FaceletContext ctx, FaceletHandlerHelper helper, TagConfig config,
467            Layout layout) {
468        if (StringUtils.isBlank(layout.getDevTemplate())) {
469            return null;
470        }
471        // use the default dev handler for widget types
472        TagAttribute attr = helper.createAttribute("layout",
473                "#{" + RenderVariables.layoutVariables.layout.name() + "}");
474        TagAttributes devWidgetAttributes = FaceletHandlerHelper.getTagAttributes(attr);
475        TagConfig devWidgetConfig = TagConfigFactory.createTagConfig(config, layout.getTagConfigId(),
476                devWidgetAttributes, new org.nuxeo.ecm.platform.ui.web.tag.handler.LeafFaceletHandler());
477        return new LayoutDevTagHandler(devWidgetConfig);
478    }
479
480    /**
481     * Compatibility methods
482     */
483
484    @SuppressWarnings("unchecked")
485    public void applyCompat(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, ELException {
486        long start = FaceletDebugTracer.start();
487        String logId = null;
488        try {
489
490            WebLayoutManager layoutService = Framework.getService(WebLayoutManager.class);
491
492            // add additional properties put on tag
493            Map<String, Serializable> additionalProps = new HashMap<String, Serializable>();
494            List<String> reservedVars = Arrays.asList(reservedVarsArray);
495            for (TagAttribute var : vars) {
496                String localName = var.getLocalName();
497                if (!reservedVars.contains(localName)) {
498                    // resolve value as there's no alias value expression exposed
499                    // for layout properties
500                    additionalProps.put(localName, (Serializable) var.getObject(ctx));
501                }
502            }
503
504            // expose some layout variables before layout creation so that they
505            // can be used in mode expressions
506            VariableMapper orig = ctx.getVariableMapper();
507
508            FaceletHandlerHelper helper = new FaceletHandlerHelper(config);
509            try {
510                VariableMapper vm = new VariableMapperWrapper(orig);
511                ctx.setVariableMapper(vm);
512
513                Layout layoutInstance = null;
514
515                String valueName = value.getValue();
516                if (ComponentTagUtils.isStrictValueReference(valueName)) {
517                    valueName = ComponentTagUtils.getBareValueName(valueName);
518                }
519
520                String templateValue = null;
521                if (template != null) {
522                    templateValue = template.getValue(ctx);
523                }
524
525                boolean resolveOnlyValue = false;
526                if (resolveOnly != null) {
527                    resolveOnlyValue = resolveOnly.getBoolean(ctx);
528                }
529
530                if (layout != null) {
531                    // resolve layout instance given as attribute
532                    layoutInstance = (Layout) layout.getObject(ctx, Layout.class);
533                    if (layoutInstance == null) {
534                        String errMsg = "Layout instance not found";
535                        applyErrorHandler(ctx, parent, helper, errMsg);
536                    } else {
537                        Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, layoutInstance.getMode());
538                        for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
539                            vm.setVariable(var.getKey(), var.getValue());
540                        }
541                        layoutInstance.setValueName(valueName);
542                        applyCompatLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
543                                additionalProps, vars, resolveOnlyValue);
544                    }
545                } else {
546                    // build layout instance from other attributes
547                    String modeValue = mode.getValue(ctx);
548
549                    List<String> selectedRowsValue = null;
550                    boolean selectAllByDefaultValue = false;
551
552                    Map<String, ValueExpression> vars = getVariablesForLayoutBuild(ctx, modeValue);
553                    for (Map.Entry<String, ValueExpression> var : vars.entrySet()) {
554                        vm.setVariable(var.getKey(), var.getValue());
555                    }
556
557                    if (selectedRows != null || selectedColumns != null) {
558                        if (selectedRows != null) {
559                            selectedRowsValue = (List<String>) selectedRows.getObject(ctx, List.class);
560                        } else if (selectedColumns != null) {
561                            List<String> selectedColumnsList = (List<String>) selectedColumns.getObject(ctx,
562                                    List.class);
563                            // Handle empty selected columns list as null to
564                            // display all columns.
565                            if (selectedColumnsList != null && selectedColumnsList.isEmpty()) {
566                                selectedColumnsList = null;
567                            }
568                            selectedRowsValue = selectedColumnsList;
569                        }
570                    }
571                    if (selectAllByDefault != null) {
572                        selectAllByDefaultValue = selectAllByDefault.getBoolean(ctx);
573                    }
574
575                    if (name != null) {
576                        String layoutCategory = null;
577                        if (category != null) {
578                            layoutCategory = category.getValue(ctx);
579                        }
580
581                        String nameValue = name.getValue(ctx);
582                        List<String> layoutNames = resolveLayoutNames(nameValue);
583                        logId = layoutNames.toString();
584                        for (String layoutName : layoutNames) {
585                            layoutInstance = layoutService.getLayout(ctx, layoutName, layoutCategory, modeValue,
586                                    valueName, selectedRowsValue, selectAllByDefaultValue);
587                            if (layoutInstance == null) {
588                                String errMsg = "Layout '" + layoutName + "' not found";
589                                applyErrorHandler(ctx, parent, helper, errMsg);
590                            } else {
591                                applyCompatLayoutHandler(ctx, parent, helper, layoutService, layoutInstance,
592                                        templateValue, additionalProps, vars, resolveOnlyValue);
593                            }
594                        }
595                    }
596
597                    if (definition != null) {
598                        LayoutDefinition layoutDef = (LayoutDefinition) definition.getObject(ctx,
599                                LayoutDefinition.class);
600
601                        if (layoutDef == null) {
602                            String errMsg = "Layout definition resolved to null";
603                            applyErrorHandler(ctx, parent, helper, errMsg);
604                        } else {
605                            layoutInstance = layoutService.getLayout(ctx, layoutDef, modeValue, valueName,
606                                    selectedRowsValue, selectAllByDefaultValue);
607                            applyCompatLayoutHandler(ctx, parent, helper, layoutService, layoutInstance, templateValue,
608                                    additionalProps, vars, resolveOnlyValue);
609                            if (layoutInstance != null) {
610                                logId = layoutInstance.getId();
611                            } else {
612                                logId = layoutDef.getName() + " (def)";
613                            }
614                        }
615                    }
616                }
617
618            } finally {
619                // layout resolved => cleanup variable mapper
620                ctx.setVariableMapper(orig);
621            }
622
623        } finally {
624            FaceletDebugTracer.trace(start, config.getTag(), logId);
625        }
626    }
627
628    protected void applyCompatLayoutHandler(FaceletContext ctx, UIComponent parent, FaceletHandlerHelper helper,
629            WebLayoutManager layoutService, Layout layoutInstance, String templateValue,
630            Map<String, Serializable> additionalProps, Map<String, ValueExpression> vars, boolean resolveOnly)
631                    throws IOException, FacesException, ELException {
632
633        // set unique id on layout, unless layout is only resolved
634        if (!resolveOnly) {
635            layoutInstance.setId(FaceletHandlerHelper.generateLayoutId(ctx, layoutInstance.getName()));
636        }
637
638        // add additional properties put on tag
639        Map<String, Serializable> layoutProps = layoutInstance.getProperties();
640        if (additionalProps != null && !additionalProps.isEmpty()) {
641            for (Map.Entry<String, Serializable> entry : additionalProps.entrySet()) {
642                // XXX: do not override with empty property values if already
643                // set on the layout properties
644                String key = entry.getKey();
645                Serializable value = entry.getValue();
646                if (layoutProps.containsKey(key)
647                        && (value == null || ((value instanceof String) && StringUtils.isBlank((String) value)))) {
648                    // do not override property on layout
649                    if (log.isDebugEnabled()) {
650                        log.debug(String.format(
651                                "Do not override property '%s' with " + "empty value on layout named '%s'", key,
652                                layoutInstance.getName()));
653                    }
654                } else {
655                    layoutInstance.setProperty(key, value);
656                }
657            }
658        }
659
660        if (StringUtils.isBlank(templateValue)) {
661            templateValue = layoutInstance.getTemplate();
662        }
663
664        if (!resolveOnly) {
665            boolean scaffold = Boolean.parseBoolean(String.valueOf(layoutInstance.getProperty("scaffold")));
666            if (scaffold) {
667                // generate ids on widgets
668                Map<String, Widget> widgetMap = layoutInstance.getWidgetMap();
669                if (widgetMap != null) {
670                    for (Widget widget : widgetMap.values()) {
671                        if (widget != null && (widget.getId() == null)) {
672                            WidgetTagHandler.generateWidgetId(ctx, helper, widget, false);
673                        }
674                    }
675                }
676            }
677        }
678
679        // expose layout instance to variable mapper to ensure good
680        // resolution of properties
681        ExpressionFactory eFactory = ctx.getExpressionFactory();
682        ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class);
683        ctx.getVariableMapper().setVariable(RenderVariables.layoutVariables.layout.name(), layoutVe);
684
685        // expose all variables through an alias tag handler
686        vars.putAll(getVariablesForLayoutRendering(ctx, layoutService, layoutInstance));
687
688        List<String> blockedPatterns = new ArrayList<String>();
689        blockedPatterns.add(RenderVariables.layoutVariables.layout.name());
690        blockedPatterns.add(RenderVariables.layoutVariables.layoutProperty.name() + "_*");
691
692        final String layoutTagConfigId = layoutInstance.getTagConfigId();
693
694        if (resolveOnly) {
695            FaceletHandler handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns,
696                    nextHandler);
697            // apply
698            handler.apply(ctx, parent);
699        } else {
700            if (!StringUtils.isBlank(templateValue)) {
701                TagAttribute srcAttr = helper.createAttribute("template", templateValue);
702                TagConfig config = TagConfigFactory.createTagConfig(this.config, layoutTagConfigId,
703                        FaceletHandlerHelper.getTagAttributes(srcAttr), nextHandler);
704                FaceletHandler includeHandler = new DecorateHandler(config);
705                FaceletHandler handler;
706                if (FaceletHandlerHelper.isDevModeEnabled(ctx)) {
707                    // decorate handler with dev handler
708                    FaceletHandler devHandler = getDevFaceletHandler(ctx, helper, config, layoutInstance);
709                    FaceletHandler nextHandler;
710                    if (devHandler == null) {
711                        nextHandler = includeHandler;
712                    } else {
713                        nextHandler = new DevTagHandler(config, layoutInstance.getName(), includeHandler, devHandler);
714                    }
715                    handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns, nextHandler);
716                } else {
717                    handler = helper.getAliasFaceletHandler(layoutTagConfigId, vars, blockedPatterns, includeHandler);
718                }
719                // apply
720                handler.apply(ctx, parent);
721            } else {
722                String errMsg = "Missing template property for layout '" + layoutInstance.getName() + "'";
723                applyErrorHandler(ctx, parent, helper, errMsg);
724            }
725        }
726    }
727
728    protected Map<String, ValueExpression> getVariablesForLayoutBuild(FaceletContext ctx, String modeValue) {
729        Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
730        ValueExpression valueExpr = value.getValueExpression(ctx, Object.class);
731        vars.put(RenderVariables.globalVariables.value.name(), valueExpr);
732        // vars.put(RenderVariables.globalVariables.document.name(),
733        // valueExpr);
734        vars.put(RenderVariables.globalVariables.layoutValue.name(), valueExpr);
735        ExpressionFactory eFactory = ctx.getExpressionFactory();
736        ValueExpression modeVe = eFactory.createValueExpression(modeValue, String.class);
737        vars.put(RenderVariables.globalVariables.layoutMode.name(), modeVe);
738        // mode as alias to layoutMode
739        vars.put(RenderVariables.globalVariables.mode.name(), modeVe);
740        return vars;
741    }
742
743    /**
744     * Computes variables for rendering, making available the layout instance and its properties to the context.
745     */
746    protected Map<String, ValueExpression> getVariablesForLayoutRendering(FaceletContext ctx,
747            WebLayoutManager layoutService, Layout layoutInstance) {
748        Map<String, ValueExpression> vars = new HashMap<String, ValueExpression>();
749        ExpressionFactory eFactory = ctx.getExpressionFactory();
750
751        // expose layout value
752        ValueExpression layoutVe = eFactory.createValueExpression(layoutInstance, Layout.class);
753        vars.put(RenderVariables.layoutVariables.layout.name(), layoutVe);
754
755        // expose layout properties too
756        for (Map.Entry<String, Serializable> prop : layoutInstance.getProperties().entrySet()) {
757            String key = prop.getKey();
758            String name = RenderVariables.layoutVariables.layoutProperty.name() + "_" + key;
759            String value;
760            Serializable valueInstance = prop.getValue();
761            if (!layoutService.referencePropertyAsExpression(key, valueInstance, null, null, null, null)) {
762                // FIXME: this will not be updated correctly using ajax
763                value = (String) valueInstance;
764            } else {
765                // create a reference so that it's a real expression and it's
766                // not kept (cached) in a component value on ajax refresh
767                value = "#{" + RenderVariables.layoutVariables.layout.name() + ".properties." + key + "}";
768            }
769            vars.put(name, eFactory.createValueExpression(ctx, value, Object.class));
770        }
771
772        return vars;
773    }
774
775    /**
776     * End of compatibility methods
777     */
778
779}