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.tag.handler;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.List;
027
028import javax.el.ELException;
029import javax.el.ValueExpression;
030import javax.el.VariableMapper;
031import javax.faces.FacesException;
032import javax.faces.component.UIComponent;
033import javax.faces.view.facelets.ComponentConfig;
034import javax.faces.view.facelets.FaceletContext;
035import javax.faces.view.facelets.FaceletException;
036import javax.faces.view.facelets.FaceletHandler;
037import javax.faces.view.facelets.TagAttribute;
038import javax.faces.view.facelets.TagException;
039
040import org.apache.commons.lang.StringUtils;
041import org.apache.commons.logging.Log;
042import org.apache.commons.logging.LogFactory;
043import org.nuxeo.ecm.platform.ui.web.binding.BlockingVariableMapper;
044import org.nuxeo.ecm.platform.ui.web.binding.MetaValueExpression;
045import org.nuxeo.ecm.platform.ui.web.binding.alias.AliasTagHandler;
046import org.nuxeo.ecm.platform.ui.web.binding.alias.AliasVariableMapper;
047import org.nuxeo.ecm.platform.ui.web.util.ComponentTagUtils;
048import org.nuxeo.ecm.platform.ui.web.util.FaceletDebugTracer;
049
050/**
051 * Tag handler that exposes a variable to the variable map. Behaviour is close to the c:set tag handler except:
052 * <ul>
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>Since 5.4, variables are made available in the request context after the JSF component tree build thanks to a
057 * backing component.</li>
058 * </ul>
059 *
060 * @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
061 * @since 5.3.1
062 */
063public class SetTagHandler extends AliasTagHandler {
064
065    private static final Log log = LogFactory.getLog(SetTagHandler.class);
066
067    protected final TagAttribute var;
068
069    protected final TagAttribute value;
070
071    /**
072     * @since 5.5
073     */
074    protected final TagAttribute resolveTwice;
075
076    /**
077     * @since 5.6
078     */
079    protected final TagAttribute blockPatterns;
080
081    /**
082     * @since 5.9.2
083     */
084    protected final TagAttribute blockMerge;
085
086    /**
087     * Determines in which context expression will be evaluated when expression is not cached and resolved twice (build
088     * time by default, render time if local).
089     *
090     * @since 7.10
091     */
092    protected final TagAttribute local;
093
094    /**
095     * Force using of {@link AliasVariableMapper} logics, exposing a reference of to a value that might change between
096     * "restore view" and "render response" phase, to make sure it's not cached by components and resolved again at
097     * "render response" phase.
098     *
099     * @since 8.2
100     */
101    protected final TagAttribute useAlias;
102
103    public SetTagHandler(ComponentConfig config) {
104        super(config, null);
105        var = getRequiredAttribute("var");
106        value = getAttribute("value");
107        resolveTwice = getAttribute("resolveTwice");
108        blockPatterns = getAttribute("blockPatterns");
109        blockMerge = getAttribute("blockMerge");
110        local = getAttribute("local");
111        useAlias = getAttribute("useAlias");
112    }
113
114    @Override
115    public void apply(FaceletContext ctx, UIComponent parent)
116            throws IOException, FacesException, FaceletException, ELException {
117        long start = FaceletDebugTracer.start();
118        String varStr = null;
119        try {
120
121            // make sure our parent is not null
122            if (parent == null) {
123                throw new TagException(tag, "Parent UIComponent was null");
124            }
125
126            boolean useAliasBool = false;
127            if (useAlias != null) {
128                useAliasBool = useAlias.getBoolean(ctx);
129            }
130
131            if (!useAliasBool && isOptimizedAgain()) {
132                varStr = var.getValue(ctx);
133                VariableMapper orig = ctx.getVariableMapper();
134                boolean done = false;
135                if (orig instanceof BlockingVariableMapper) {
136                    BlockingVariableMapper vm = (BlockingVariableMapper) orig;
137                    if (isAcceptingMerge(ctx, vm, varStr)) {
138                        FaceletHandler next = applyOptimized(ctx, parent, vm, varStr);
139                        next.apply(ctx, parent);
140                        done = true;
141                    }
142                }
143                if (!done) {
144                    try {
145                        BlockingVariableMapper vm = new BlockingVariableMapper(orig);
146                        ctx.setVariableMapper(vm);
147                        FaceletHandler next = applyOptimized(ctx, parent, vm, varStr);
148                        next.apply(ctx, parent);
149                    } finally {
150                        ctx.setVariableMapper(orig);
151                    }
152                }
153            } else {
154                applyAlias(ctx, parent);
155            }
156        } finally {
157            FaceletDebugTracer.trace(start, getTag(), var.getValue());
158        }
159    }
160
161    public FaceletHandler getNextHandler() {
162        return nextHandler;
163    }
164
165    public boolean isAcceptingMerge(FaceletContext ctx, BlockingVariableMapper vm, String var) {
166        // avoid overriding variable already in the mapper
167        if (vm.hasVariable(var)) {
168            return false;
169        }
170        return isAcceptingMerge(ctx);
171    }
172
173    public boolean isAcceptingMerge(FaceletContext ctx) {
174        if (useAlias != null && useAlias.getBoolean(ctx)) {
175            return false;
176        }
177        if (blockMerge != null && blockMerge.getBoolean(ctx)) {
178            return false;
179        }
180        if (blockPatterns != null) {
181            String blocked = blockPatterns.getValue(ctx);
182            if (!StringUtils.isEmpty(blocked)) {
183                return false;
184            }
185        }
186        return true;
187    }
188
189    public FaceletHandler applyOptimized(FaceletContext ctx, UIComponent parent, BlockingVariableMapper vm)
190            throws IOException {
191        String varStr = var.getValue(ctx);
192        return applyOptimized(ctx, parent, vm, varStr);
193    }
194
195    public FaceletHandler applyOptimized(FaceletContext ctx, UIComponent parent, BlockingVariableMapper vm,
196            String varStr) throws IOException {
197
198        // handle variable expression
199        boolean cacheValue = false;
200        if (cache != null) {
201            cacheValue = cache.getBoolean(ctx);
202        }
203        boolean resolveTwiceBool = false;
204        if (resolveTwice != null) {
205            resolveTwiceBool = resolveTwice.getBoolean(ctx);
206        }
207
208        ValueExpression ve;
209        if (cacheValue) {
210            // resolve value and put it as is in variable mapper
211            Object res = value.getObject(ctx);
212            if (resolveTwiceBool && res instanceof String && ComponentTagUtils.isValueReference((String) res)) {
213                ve = ctx.getExpressionFactory().createValueExpression(ctx, (String) res, Object.class);
214                res = ve.getValue(ctx);
215            }
216            ve = ctx.getExpressionFactory().createValueExpression(res, Object.class);
217        } else {
218            ve = value.getValueExpression(ctx, Object.class);
219            if (resolveTwiceBool) {
220                boolean localBool = false;
221                if (local != null) {
222                    localBool = local.getBoolean(ctx);
223                }
224                if (localBool) {
225                    ve = new MetaValueExpression(ve);
226                } else {
227                    ve = new MetaValueExpression(ve, ctx.getFunctionMapper(), vm);
228                }
229            }
230        }
231
232        vm.setVariable(varStr, ve);
233
234        if (blockPatterns != null) {
235            String blockedValue = blockPatterns.getValue(ctx);
236            if (!StringUtils.isEmpty(blockedValue)) {
237                // split on "," character
238                vm.setBlockedPatterns(resolveBlockPatterns(blockedValue));
239            }
240        }
241
242        FaceletHandler nextHandler = this.nextHandler;
243        if (nextHandler instanceof SetTagHandler) {
244            // try merging with next handler
245            SetTagHandler next = (SetTagHandler) nextHandler;
246            if (next.isAcceptingMerge(ctx)) {
247                nextHandler = next.applyOptimized(ctx, parent, vm);
248            }
249        }
250
251        return nextHandler;
252    }
253
254    public void applyAlias(FaceletContext ctx, UIComponent parent) throws IOException {
255        FaceletHandler nextHandler = this.nextHandler;
256        VariableMapper orig = ctx.getVariableMapper();
257        AliasVariableMapper target = new AliasVariableMapper();
258        // generate id before applying (and before generating next handler, in
259        // case of merge of variables, as parent aliases will be exposed to
260        // request then).
261        target.setId(ctx.generateUniqueId(tagId));
262
263        VariableMapper vm = target.getVariableMapperForBuild(orig);
264        ctx.setVariableMapper(vm);
265        try {
266            nextHandler = getAliasVariableMapper(ctx, target);
267        } finally {
268            ctx.setVariableMapper(orig);
269        }
270        applyAliasHandler(ctx, parent, target, nextHandler);
271    }
272
273    public FaceletHandler getAliasVariableMapper(FaceletContext ctx, AliasVariableMapper target) {
274        String varStr = var.getValue(ctx);
275        // avoid overriding variable already in the mapper
276        if (target.hasVariables(varStr)) {
277            return nextHandler;
278        }
279
280        // handle variable expression
281        boolean cacheValue = false;
282        if (cache != null) {
283            cacheValue = cache.getBoolean(ctx);
284        }
285        boolean resolveTwiceBool = false;
286        if (resolveTwice != null) {
287            resolveTwiceBool = resolveTwice.getBoolean(ctx);
288        }
289
290        ValueExpression ve;
291        if (cacheValue) {
292            // resolve value and put it as is in variable mapper
293            Object res = value.getObject(ctx);
294            if (resolveTwiceBool && res instanceof String && ComponentTagUtils.isValueReference((String) res)) {
295                ve = ctx.getExpressionFactory().createValueExpression(ctx, (String) res, Object.class);
296                res = ve.getValue(ctx);
297            }
298            ve = ctx.getExpressionFactory().createValueExpression(res, Object.class);
299        } else {
300            ve = value.getValueExpression(ctx, Object.class);
301            if (resolveTwiceBool) {
302                boolean localBool = false;
303                if (local != null) {
304                    localBool = local.getBoolean(ctx);
305                }
306                if (localBool) {
307                    ve = new MetaValueExpression(ve);
308                } else {
309                    ve = new MetaValueExpression(ve, ctx.getFunctionMapper(), ctx.getVariableMapper());
310                }
311            }
312        }
313
314        target.setVariable(varStr, ve);
315
316        if (blockPatterns != null) {
317            String blockedValue = blockPatterns.getValue(ctx);
318            if (!StringUtils.isEmpty(blockedValue)) {
319                // split on "," character
320                target.setBlockedPatterns(resolveBlockPatterns(blockedValue));
321            }
322        }
323
324        FaceletHandler nextHandler = this.nextHandler;
325        if (nextHandler instanceof SetTagHandler) {
326            // try merging with next handler
327            SetTagHandler next = (SetTagHandler) nextHandler;
328            if (next.isAcceptingMerge(ctx)) {
329                // make sure referenced vars will be resolved in this context
330                ctx.getVariableMapper().setVariable(varStr, ve);
331                try {
332                    AliasVariableMapper.exposeAliasesToRequest(ctx.getFacesContext(), target);
333                    nextHandler = next.getAliasVariableMapper(ctx, target);
334                } finally {
335                    AliasVariableMapper.removeAliasesExposedToRequest(ctx.getFacesContext(), target.getId());
336                }
337            }
338        }
339
340        return nextHandler;
341    }
342
343    protected List<String> resolveBlockPatterns(String value) {
344        List<String> res = new ArrayList<String>();
345        if (value != null) {
346            String[] split = StringUtils.split(value, ',');
347            if (split != null) {
348                for (String item : split) {
349                    res.add(item.trim());
350                }
351            }
352        }
353        return res;
354    }
355
356}