| // Copyright 2004 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><b>Optimization of the Expression</b> |
| * |
| * <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: |
| * |
| * <ul> |
| * <li>page |
| * <li>container |
| * <li>components.<i>name</i> |
| * </ul> |
| * |
| * <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><b>Identifying Invariants</b> |
| * |
| * <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: |
| * |
| * <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> |
| * 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; |
| |
| /** |
| * 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 (false && _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 (false && _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(); |
| } |
| |
| } |