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