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