/*
* (C) Copyright 2010 Nuxeo SAS (http://nuxeo.com/) and contributors.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public License
* (LGPL) version 2.1 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* Contributors:
* Nuxeo - initial API and implementation
*
* $Id: $
*/
package org.nuxeo.ecm.platform.ui.web.binding.alias;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.el.ELException;
import javax.el.ExpressionFactory;
import javax.el.ValueExpression;
import javax.el.VariableMapper;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.component.UIViewRoot;
import javax.faces.context.FacesContext;
import javax.faces.view.facelets.ComponentConfig;
import javax.faces.view.facelets.ComponentHandler;
import javax.faces.view.facelets.FaceletContext;
import javax.faces.view.facelets.FaceletException;
import javax.faces.view.facelets.FaceletHandler;
import javax.faces.view.facelets.TagAttribute;
import javax.faces.view.facelets.TagException;
import org.nuxeo.runtime.api.Framework;
import com.sun.faces.facelets.tag.jsf.ComponentSupport;
/**
* Tag handler that exposes variables to the variable map. Behaviour is close
* to the c:set tag handler except:
* <ul>
* <li>It handles several variables</li>
* <li>It allows caching a variable using cache parameter: variable will be
* resolved the first time is is called and will be put in the context after</li>
* <li>The resolved variable is removed from context when tag is closed to
* avoid filling the context with it</li>
* <li>Variables are made available in the request context after the JSF
* component tree build thanks to a backing component.</li>
* </ul>
* <p>
* The backing component value expressions are changed even if the component
* was found to ensure a good resolution even when re-rendering the tag using
* ajax.
*
* @author <a href="mailto:at@nuxeo.com">Anahide Tchertchian</a>
* @since 5.4
*/
public class AliasTagHandler extends ComponentHandler {
/**
* @since 6.0
*/
public static String ANCHOR_ENABLED_VARIABLE = "nuxeoAliasAnchorEnabled";
protected final TagAttribute cache;
protected final TagAttribute id;
protected final Map<String, ValueExpression> variables;
protected final List<String> blockedPatterns;
/**
* @since 6.0
*/
protected final TagAttribute anchor;
public AliasTagHandler(ComponentConfig config,
Map<String, ValueExpression> variables) {
this(config, variables, null);
}
/**
* @since 5.6
*/
public AliasTagHandler(ComponentConfig config,
Map<String, ValueExpression> variables, List<String> blockedPatterns) {
super(config);
id = getAttribute("id");
cache = getAttribute("cache");
anchor = getAttribute("anchor");
this.variables = variables;
this.blockedPatterns = blockedPatterns;
}
public void apply(FaceletContext ctx, UIComponent parent)
throws IOException, FacesException, FaceletException, ELException {
// make sure our parent is not null
if (parent == null) {
throw new TagException(this.tag, "Parent UIComponent was null");
}
// handle variable expression
boolean cacheValue = false;
if (cache != null) {
cacheValue = cache.getBoolean(ctx);
}
AliasVariableMapper target = new AliasVariableMapper();
target.setBlockedPatterns(blockedPatterns);
if (variables != null) {
for (Map.Entry<String, ValueExpression> var : variables.entrySet()) {
if (cacheValue) {
// resolve value and put it as is in variables
Object res = var.getValue().getValue(ctx);
target.setVariable(
var.getKey(),
ctx.getExpressionFactory().createValueExpression(
res, Object.class));
} else {
target.setVariable(var.getKey(), var.getValue());
}
}
}
// generate id before applying
target.setId(ctx.generateUniqueId(this.tagId));
apply(ctx, parent, target, this.nextHandler);
}
protected void apply(FaceletContext ctx, UIComponent parent,
AliasVariableMapper alias, FaceletHandler nextHandler)
throws IOException, FacesException, FaceletException, ELException {
if (Framework.isBooleanPropertyTrue("nuxeo.jsf.removeAliasOptims")) {
applyCompat(ctx, parent, alias, nextHandler);
return;
}
// resolve the "anchor" attribute to decide whether variable should be
// anchored in the tree as a UIAliasHolder
boolean createComponent = isAnchored(ctx);
applyAliasHandler(ctx, parent, alias, nextHandler, createComponent);
}
protected boolean isAnchored(FaceletContext ctx) {
ExpressionFactory eFactory = ctx.getExpressionFactory();
ValueExpression ve = eFactory.createValueExpression(ctx,
String.format("#{%s}", ANCHOR_ENABLED_VARIABLE), Boolean.class);
if (Boolean.TRUE.equals(ve.getValue(ctx))) {
return true;
}
if (anchor != null) {
return anchor.getBoolean(ctx);
}
return false;
}
protected void applyAliasHandler(FaceletContext ctx, UIComponent parent,
AliasVariableMapper alias, FaceletHandler nextHandler,
boolean createComponent) throws IOException, FacesException,
FaceletException, ELException {
if (createComponent) {
// start by removing component from tree if it is already there, to
// make sure it's recreated next
String id = ctx.generateUniqueId(getTagId());
UIComponent c = ComponentSupport.findChildByTagId(parent, id);
if (c != null && c.getParent() != parent) {
c.getParent().getChildren().remove(c);
}
}
String id = alias.getId();
VariableMapper orig = ctx.getVariableMapper();
VariableMapper vm = alias.getVariableMapperForBuild(orig);
ctx.setVariableMapper(vm);
FacesContext facesContext = ctx.getFacesContext();
try {
AliasVariableMapper.exposeAliasesToRequest(facesContext, alias);
if (createComponent) {
super.apply(ctx, parent);
} else {
nextHandler.apply(ctx, parent);
}
} finally {
AliasVariableMapper.removeAliasesExposedToRequest(facesContext, id);
ctx.setVariableMapper(orig);
}
}
/**
* Compatibility application of facelet handler, used to preserve behaviour
* while optimizing and improving variables exposure and resolution.
*/
protected void applyCompat(FaceletContext ctx, UIComponent parent,
AliasVariableMapper alias, FaceletHandler nextHandler)
throws IOException, FacesException, FaceletException, ELException {
String id = alias.getId();
VariableMapper orig = ctx.getVariableMapper();
VariableMapper vm = alias.getVariableMapperForBuild(orig);
ctx.setVariableMapper(vm);
// create component
UIComponent c = ComponentSupport.findChildByTagId(parent, id);
boolean componentFound = false;
if (c != null) {
componentFound = true;
// mark all children for cleaning
ComponentSupport.markForDeletion(c);
} else {
c = new UIAliasHolder();
// mark it owned by a facelet instance
c.getAttributes().put(ComponentSupport.MARK_CREATED, id);
// assign our unique id
if (this.id != null) {
c.setId(this.id.getValue(ctx));
} else {
UIViewRoot root = ComponentSupport.getViewRoot(ctx, parent);
if (root != null) {
String uid = root.createUniqueId();
c.setId(uid);
}
}
}
// update value held by component
((UIAliasHolder) c).setAlias(alias);
FacesContext facesContext = ctx.getFacesContext();
try {
AliasVariableMapper.exposeAliasesToRequest(facesContext, alias);
// first allow c to get populated
nextHandler.apply(ctx, c);
} finally {
AliasVariableMapper.removeAliasesExposedToRequest(facesContext, id);
ctx.setVariableMapper(orig);
}
// finish cleaning up orphaned children
if (componentFound) {
ComponentSupport.finalizeForDeletion(c);
}
// add to the tree afterwards
// this allows children to determine if it's
// been part of the tree or not yet
parent.getChildren().add(c);
}
}