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