blob: 862997491c5d05760c02bd76751814a734332445 [file] [log] [blame]
// Copyright 2004, 2008 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.apache.tapestry.binding;
import ognl.Node;
import ognl.Ognl;
import ognl.OgnlException;
import ognl.TypeConverter;
import ognl.enhance.ExpressionAccessor;
import org.apache.tapestry.*;
import org.apache.tapestry.engine.ExpressionEvaluator;
import org.apache.tapestry.spec.BeanLifecycle;
import org.apache.tapestry.spec.IApplicationSpecification;
import org.apache.tapestry.spec.IBeanSpecification;
import org.apache.tapestry.util.StringSplitter;
import java.util.Map;
/**
* Implements a dynamic binding, based on getting and fetching values using JavaBeans property access. This is built
* upon the <a href="http://www.ognl.org">OGNL</a> library.
* <p/>
* <p><b>Optimization of the Expression</b>
* <p/>
* <p>There's a lot of room for optimization here because we can count on some portions of the expression to be
* effectively static. Note that we type the root object as {@link IComponent}. We have some expectations that certain
* properties of the root (and properties reachable from the root) will be constant for the lifetime of the binding. For
* example, components never change thier page or container. This means that certain property prefixes can be
* optimized:
* <p/>
* <ul> <li>page <li>container <li>components.<i>name</i> </ul>
* <p/>
* <p>This means that once an ExpressionBinding has been triggered, the {@link #toString()} method may return different
* values for the root component and the expression than was originally set.
* <p/>
* <p><b>Identifying Invariants</b>
* <p/>
* <p>Most expressions are fully dynamic; they must be resolved each time they are accessed. This can be somewhat
* inefficient. Tapestry can identify certain paths as invariant:
* <p/>
* <ul> <li>A component within the page hierarchy <li>An {@link org.apache.tapestry.IAsset} from then assets map
* (property <code>assets</code>) <li>A {@link org.apache.tapestry.IActionListener} from the listener map (property
* <code>listeners</code>) <li>A bean with a {@link org.apache.tapestry.spec.BeanLifecycle#PAGE} lifecycle (property
* <code>beans</code>) <li>A binding (property <code>bindings</code>) </ul>
* <p/>
* <p/>
* These optimizations have some inherent dangers; they assume that the components have not overidden the specified
* properties; the last one (concerning helper beans) assumes that the component does inherit from {@link
* org.apache.tapestry.AbstractComponent}. If this becomes a problem in the future, it may be necessary to have the
* component itself involved in these determinations.
*
* @author Howard Lewis Ship
* @version $Id$
* @since 2.2
*/
public class ExpressionBinding extends AbstractBinding
{
/**
* The root object against which the nested property name is evaluated.
*/
private IComponent _root;
/**
* The OGNL expression, as a string.
*/
private String _expression;
/**
* If true, then the binding is invariant, and cachedValue is the ultimate value.
*/
private boolean _invariant = false;
/**
* Stores the cached value for the binding, if invariant is true.
*/
private Object _cachedValue;
/**
* Parsed OGNL expression.
*/
private Node _parsedExpression;
/**
* Flag set once the binding has initialized. _cachedValue, _invariant and _final value for _expression are not
* valid until after initialization.
*/
private boolean _initialized;
private IResourceResolver _resolver;
/**
* The OGNL context for this binding. It is retained for the lifespan of the binding once created.
*/
private Map _context;
/**
* Central OGNL expression manager.
*/
private ExpressionEvaluator _evaluator;
/**
* Compiled OGNL expression.
*/
private ExpressionAccessor _accessor;
/**
* Used to detect previous failed attempts at writing values when compiling expressions so that as many expressions
* as possible can be fully compiled into their java byte form when all objects in the expression are available.
*/
private boolean _writeFailed;
/**
* This is current set to false, to prevent any attempts at bytecode compilation of OGNL expressions.
*/
private static final boolean EXPRESSION_EVALUATION_ENABLED = false;
/**
* Creates a {@link ExpressionBinding} from the root object and an OGNL expression.
*/
public ExpressionBinding(
IResourceResolver resolver,
IComponent root,
String expression,
ILocation location)
{
super(location);
_resolver = resolver;
_root = root;
_expression = expression;
_evaluator = root.getPage().getEngine().getExpressionEvaluator();
}
public String getExpression()
{
return _expression;
}
public IComponent getRoot()
{
return _root;
}
/**
* Gets the value of the property path, with the assistance of a OGNL.
*
* @throws BindingException if an exception is thrown accessing the property.
*/
public Object getObject()
{
initialize();
if (_invariant)
return _cachedValue;
return resolveProperty();
}
private Object resolveProperty()
{
try
{
if (EXPRESSION_EVALUATION_ENABLED && _evaluator.isCompileEnabled() && _accessor == null && !_writeFailed)
{
_evaluator.compileExpression(_root, _parsedExpression, _expression);
_accessor = _parsedExpression.getAccessor();
}
if (_accessor != null)
return _evaluator.read(_root, _accessor);
return _evaluator.readCompiled(_root, _parsedExpression);
}
catch (Throwable t)
{
throw new BindingException(
Tapestry.format(
"ExpressionBinding.unable-to-resolve-expression",
_expression,
_root),
this,
t);
}
}
/**
* Creates an OGNL context used to get or set a value. We may extend this in the future to set additional context
* variables (such as page, request cycle and engine). An optional type converter will be added to the OGNL context
* if it is specified as an application extension with the name {@link Tapestry#OGNL_TYPE_CONVERTER}.
*/
private Map getOgnlContext()
{
if (_context == null)
_context = Ognl.createDefaultContext(_root, _resolver);
if (_root.getPage() != null)
{
if (_root.getPage().getEngine() != null)
{
IApplicationSpecification appSpec = _root.getPage().getEngine().getSpecification();
if (appSpec != null && appSpec.checkExtension(Tapestry.OGNL_TYPE_CONVERTER))
{
TypeConverter typeConverter = (TypeConverter) appSpec.getExtension(
Tapestry.OGNL_TYPE_CONVERTER,
TypeConverter.class);
Ognl.setTypeConverter(_context, typeConverter);
}
}
}
return _context;
}
/**
* Returns true if the binding is expected to always return the same value.
*/
public boolean isInvariant()
{
initialize();
return _invariant;
}
public void setBoolean(boolean value)
{
setObject(value ? Boolean.TRUE : Boolean.FALSE);
}
public void setInt(int value)
{
setObject(new Integer(value));
}
public void setDouble(double value)
{
setObject(new Double(value));
}
public void setString(String value)
{
setObject(value);
}
/**
* Sets up the helper object, but also optimizes the property path and determines if the binding is invarant.
*/
private void initialize()
{
if (_initialized)
return;
_initialized = true;
try
{
_parsedExpression = _evaluator.parse(_root, _expression);
}
catch (Exception ex)
{
throw new BindingException(ex.getMessage(), this, ex);
}
if (checkForConstant())
return;
try
{
if (!Ognl.isSimpleNavigationChain(_parsedExpression, getOgnlContext()))
return;
}
catch (OgnlException ex)
{
throw new BindingException(ex.getMessage(), this, ex);
}
// Split the expression into individual property names.
// We then optimize what we can from the expression. This will
// shorten the expression and, in some cases, eliminate
// it. We also check to see if the binding can be an invariant.
String[] split = new StringSplitter('.').splitToArray(_expression);
int count = optimizeRootObject(split);
// We'ver removed some or all of the initial elements of split
// but have to account for anything left over.
if (count == split.length)
{
// The property path was something like "page" or "component.foo"
// and was completely eliminated.
_expression = null;
_parsedExpression = null;
_invariant = true;
_cachedValue = _root;
return;
}
_expression = reassemble(count, split);
_parsedExpression = _evaluator.parse(_root, _expression);
checkForInvariant(count, split);
}
/**
* Looks for common prefixes on the expression (provided pre-split) that are recognized as references to other
* components.
*
* @return the number of leading elements of the split expression that have been removed.
*/
private int optimizeRootObject(String[] split)
{
int i;
for (i = 0; i < split.length; i++)
{
if (split[i].equals("page"))
{
_root = _root.getPage();
continue;
}
if (split[i].equals("container"))
{
_root = _root.getContainer();
continue;
}
// Here's the tricky one ... if its of the form
// "components.foo" we can get the named component
// directly.
if (split[i].equals("components") && i + 1 < split.length)
{
_root = _root.getComponent(split[i + 1]);
i++;
continue;
}
// Not a recognized prefix, break the loop
break;
}
return i;
}
private boolean checkForConstant()
{
if (_evaluator.isConstant(_root, _expression))
{
_invariant = true;
_cachedValue = resolveProperty();
return true;
}
return false;
}
/**
* Reassembles the remainder of the split property path from the start point.
*/
private String reassemble(int start, String[] split)
{
int count = split.length - start;
if (count == 0)
return null;
if (count == 1)
return split[split.length - 1];
StringBuffer buffer = new StringBuffer();
for (int i = start; i < split.length; i++)
{
if (i > start)
buffer.append('.');
buffer.append(split[i]);
}
return buffer.toString();
}
/**
* Checks to see if the binding can be converted to an invariant.
*/
private void checkForInvariant(int start, String[] split)
{
// For now, all of our conditions are two properties
// from a root component.
if (split.length - start != 2)
return;
try
{
if (!Ognl.isSimpleNavigationChain(_parsedExpression, getOgnlContext()))
return;
}
catch (OgnlException ex)
{
throw new BindingException(
Tapestry.format(
"ExpressionBinding.unable-to-resolve-expression",
_expression,
_root),
this,
ex);
}
String first = split[start];
if (first.equals("listeners"))
{
_invariant = true;
// Could cast to AbstractComponent, get listenersMap, etc.,
// but this is easier.
_cachedValue = resolveProperty();
return;
}
if (first.equals("assets"))
{
String name = split[start + 1];
_invariant = true;
_cachedValue = _root.getAsset(name);
return;
}
if (first.equals("beans"))
{
String name = split[start + 1];
IBeanSpecification bs = _root.getSpecification().getBeanSpecification(name);
if (bs == null || bs.getLifecycle() != BeanLifecycle.PAGE)
return;
// Again, could cast to AbstractComponent, but this
// is easier.
_invariant = true;
_cachedValue = resolveProperty();
return;
}
if (first.equals("bindings"))
{
String name = split[start + 1];
_invariant = true;
_cachedValue = _root.getBinding(name);
return;
}
// Not a recognized pattern for conversion
// to invariant.
}
/**
* Updates the property for the binding to the given value.
*
* @throws BindingException if the property can't be updated (typically due to an security problem, or a missing
* mutator method).
* @throws BindingException if the binding is invariant.
*/
public void setObject(Object value)
{
initialize();
if (_invariant)
throw createReadOnlyBindingException(this);
try
{
if (_accessor != null)
{
_evaluator.write(_root, _accessor, value);
}
else if (EXPRESSION_EVALUATION_ENABLED && _evaluator.isCompileEnabled() && _accessor == null)
{
//_evaluator.compileExpression(_root, _parsedExpression, _expression);
//_accessor = _parsedExpression.getAccessor();
if (!_writeFailed)
{
// re-parse expression as compilation may be possible now that it potentially has a value
try
{
_evaluator.compileExpression(_root, _parsedExpression, _expression);
_accessor = _parsedExpression.getAccessor();
}
catch (Throwable t)
{
// ignore re-read failures as they aren't supposed to be happening now anyways
// and a more user friendly version will be available if someone actually calls
// getObject
// if writing fails then we're probably screwed...so don't do it again
if (value != null)
_writeFailed = true;
}
}
}
else
{
_evaluator.writeCompiled(_root, _parsedExpression, value);
}
}
catch (Throwable ex)
{
throw new BindingException(
Tapestry.format(
"ExpressionBinding.unable-to-update-expression",
_expression,
_root,
value),
this,
ex);
}
}
/**
* Returns the a String representing the property path. This includes the {@link IComponent#getExtendedId()
* extended id} of the root component and the property path ... once the binding is used, these may change due to
* optimization of the property path.
*/
public String toString()
{
StringBuffer buffer = new StringBuffer();
buffer.append("ExpressionBinding[");
buffer.append(_root.getExtendedId());
if (_expression != null)
{
buffer.append(' ');
buffer.append(_expression);
}
if (_invariant)
{
buffer.append(" cachedValue=");
buffer.append(_cachedValue);
}
buffer.append(']');
return buffer.toString();
}
}