001/*
002 * (C) Copyright 2010 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 *     Nuxeo - initial API and implementation
016 *
017 * $Id: $
018 */
019
020package org.nuxeo.ecm.platform.ui.web.binding.alias;
021
022import java.io.IOException;
023import java.util.List;
024import java.util.Map;
025
026import javax.el.ELException;
027import javax.el.ExpressionFactory;
028import javax.el.ValueExpression;
029import javax.el.VariableMapper;
030import javax.faces.FacesException;
031import javax.faces.component.UIComponent;
032import javax.faces.component.UIViewRoot;
033import javax.faces.context.FacesContext;
034import javax.faces.view.facelets.ComponentConfig;
035import javax.faces.view.facelets.ComponentHandler;
036import javax.faces.view.facelets.FaceletContext;
037import javax.faces.view.facelets.FaceletException;
038import javax.faces.view.facelets.FaceletHandler;
039import javax.faces.view.facelets.TagAttribute;
040import javax.faces.view.facelets.TagException;
041
042import org.nuxeo.runtime.api.Framework;
043import org.nuxeo.runtime.services.config.ConfigurationService;
044
045import com.sun.faces.facelets.tag.jsf.ComponentSupport;
046
047/**
048 * Tag handler that exposes variables to the variable map. Behaviour is close to the c:set tag handler except:
049 * <ul>
050 * <li>It handles several variables</li>
051 * <li>It allows caching a variable using cache parameter: variable will be resolved the first time is is called and
052 * will be put in the context after</li>
053 * <li>The resolved variable is removed from context when tag is closed to avoid filling the context with it</li>
054 * <li>Variables are made available in the request context after the JSF component tree build thanks to a backing
055 * component.</li>
056 * </ul>
057 * <p>
058 * The backing component value expressions are changed even if the component was found to ensure a good resolution even
059 * when re-rendering the tag using ajax.
060 *
061 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
062 * @since 5.4
063 */
064public class AliasTagHandler extends ComponentHandler {
065
066    /**
067     * @since 6.0
068     */
069    public static String ANCHOR_ENABLED_VARIABLE = "nuxeoAliasAnchorEnabled";
070
071    protected final TagAttribute cache;
072
073    protected final TagAttribute id;
074
075    protected final Map<String, ValueExpression> variables;
076
077    protected final List<String> blockedPatterns;
078
079    /**
080     * @since 6.0
081     */
082    protected final TagAttribute anchor;
083
084    public AliasTagHandler(ComponentConfig config, Map<String, ValueExpression> variables) {
085        this(config, variables, null);
086    }
087
088    /**
089     * @since 5.6
090     */
091    public AliasTagHandler(ComponentConfig config, Map<String, ValueExpression> variables, List<String> blockedPatterns) {
092        super(config);
093        id = getAttribute("id");
094        cache = getAttribute("cache");
095        anchor = getAttribute("anchor");
096        this.variables = variables;
097        this.blockedPatterns = blockedPatterns;
098    }
099
100    @Override
101    public void apply(FaceletContext ctx, UIComponent parent) throws IOException, FacesException, FaceletException,
102            ELException {
103        // make sure our parent is not null
104        if (parent == null) {
105            throw new TagException(tag, "Parent UIComponent was null");
106        }
107
108        // handle variable expression
109        boolean cacheValue = false;
110        if (cache != null) {
111            cacheValue = cache.getBoolean(ctx);
112        }
113        AliasVariableMapper target = new AliasVariableMapper();
114        target.setBlockedPatterns(blockedPatterns);
115        if (variables != null) {
116            for (Map.Entry<String, ValueExpression> var : variables.entrySet()) {
117                if (cacheValue) {
118                    // resolve value and put it as is in variables
119                    Object res = var.getValue().getValue(ctx);
120                    target.setVariable(var.getKey(),
121                            ctx.getExpressionFactory().createValueExpression(res, Object.class));
122                } else {
123                    target.setVariable(var.getKey(), var.getValue());
124                }
125            }
126        }
127
128        // generate id before applying
129        target.setId(ctx.generateUniqueId(tagId));
130        apply(ctx, parent, target, nextHandler);
131    }
132
133    protected void apply(FaceletContext ctx, UIComponent parent, AliasVariableMapper alias, FaceletHandler nextHandler)
134            throws IOException, FacesException, FaceletException, ELException {
135        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
136        if (configurationService.isBooleanPropertyTrue("nuxeo.jsf.removeAliasOptims")) {
137            applyCompat(ctx, parent, alias, nextHandler);
138            return;
139        }
140
141        // resolve the "anchor" attribute to decide whether variable should be
142        // anchored in the tree as a UIAliasHolder
143        boolean createComponent = isAnchored(ctx);
144        applyAliasHandler(ctx, parent, alias, nextHandler, createComponent);
145    }
146
147    protected boolean isAnchored(FaceletContext ctx) {
148        ExpressionFactory eFactory = ctx.getExpressionFactory();
149        ValueExpression ve = eFactory.createValueExpression(ctx, String.format("#{%s}", ANCHOR_ENABLED_VARIABLE),
150                Boolean.class);
151        if (Boolean.TRUE.equals(ve.getValue(ctx))) {
152            return true;
153        }
154        if (anchor != null) {
155            return anchor.getBoolean(ctx);
156        }
157        return false;
158    }
159
160    protected void applyAliasHandler(FaceletContext ctx, UIComponent parent, AliasVariableMapper alias,
161            FaceletHandler nextHandler, boolean createComponent) throws IOException, FacesException, FaceletException,
162            ELException {
163        if (createComponent) {
164            // start by removing component from tree if it is already there, to
165            // make sure it's recreated next
166            String id = ctx.generateUniqueId(getTagId());
167            UIComponent c = ComponentSupport.findChildByTagId(parent, id);
168            if (c != null && c.getParent() != parent) {
169                c.getParent().getChildren().remove(c);
170            }
171        }
172
173        String id = alias.getId();
174        VariableMapper orig = ctx.getVariableMapper();
175        VariableMapper vm = alias.getVariableMapperForBuild(orig);
176        ctx.setVariableMapper(vm);
177        FacesContext facesContext = ctx.getFacesContext();
178        try {
179            AliasVariableMapper.exposeAliasesToRequest(facesContext, alias);
180            if (createComponent) {
181                super.apply(ctx, parent);
182            } else {
183                nextHandler.apply(ctx, parent);
184            }
185        } finally {
186            AliasVariableMapper.removeAliasesExposedToRequest(facesContext, id);
187            ctx.setVariableMapper(orig);
188        }
189    }
190
191    /**
192     * Compatibility application of facelet handler, used to preserve behaviour while optimizing and improving variables
193     * exposure and resolution.
194     */
195    protected void applyCompat(FaceletContext ctx, UIComponent parent, AliasVariableMapper alias,
196            FaceletHandler nextHandler) throws IOException, FacesException, FaceletException, ELException {
197        String id = alias.getId();
198
199        VariableMapper orig = ctx.getVariableMapper();
200        VariableMapper vm = alias.getVariableMapperForBuild(orig);
201        ctx.setVariableMapper(vm);
202
203        // create component
204        UIComponent c = ComponentSupport.findChildByTagId(parent, id);
205        boolean componentFound = false;
206        if (c != null) {
207            componentFound = true;
208            // mark all children for cleaning
209            ComponentSupport.markForDeletion(c);
210        } else {
211            c = new UIAliasHolder();
212
213            // mark it owned by a facelet instance
214            c.getAttributes().put(ComponentSupport.MARK_CREATED, id);
215
216            // assign our unique id
217            if (this.id != null) {
218                c.setId(this.id.getValue(ctx));
219            } else {
220                UIViewRoot root = ComponentSupport.getViewRoot(ctx, parent);
221                if (root != null) {
222                    String uid = root.createUniqueId();
223                    c.setId(uid);
224                }
225            }
226        }
227
228        // update value held by component
229        ((UIAliasHolder) c).setAlias(alias);
230
231        FacesContext facesContext = ctx.getFacesContext();
232        try {
233            AliasVariableMapper.exposeAliasesToRequest(facesContext, alias);
234            // first allow c to get populated
235            nextHandler.apply(ctx, c);
236        } finally {
237            AliasVariableMapper.removeAliasesExposedToRequest(facesContext, id);
238            ctx.setVariableMapper(orig);
239        }
240
241        // finish cleaning up orphaned children
242        if (componentFound) {
243            ComponentSupport.finalizeForDeletion(c);
244        }
245
246        // add to the tree afterwards
247        // this allows children to determine if it's
248        // been part of the tree or not yet
249        parent.getChildren().add(c);
250    }
251
252}