    1   /*
    2    * Copyright 1999-2004 The Apache Software Foundation
    3    *
    4    * Licensed under the Apache License, Version 2.0 (the "License");
    5    * you may not use this file except in compliance with the License.
    6    * You may obtain a copy of the License at
    7    *
    8    *     http://www.apache.org/licenses/LICENSE-2.0
    9    *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  package org.apache.commons.jxpath.ri;
    17
    18
    19  import java.lang.ref.SoftReference;
    20  import java.util.ArrayList;
    21  import java.util.Arrays;
    22  import java.util.Collections;
    23  import java.util.Comparator;
    24  import java.util.HashMap;
    25  import java.util.Iterator;
    26  import java.util.Map;
    27  import java.util.Vector;
    28  import java.util.Map.Entry;
    29
    30  import org.apache.commons.jxpath.CompiledExpression;
    31  import org.apache.commons.jxpath.Function;
    32  import org.apache.commons.jxpath.Functions;
    33  import org.apache.commons.jxpath.JXPathContext;
    34  import org.apache.commons.jxpath.JXPathException;
    35  import org.apache.commons.jxpath.Pointer;
    36  import org.apache.commons.jxpath.Variables;
    37  import org.apache.commons.jxpath.ri.axes.InitialContext;
    38  import org.apache.commons.jxpath.ri.axes.RootContext;
    39  import org.apache.commons.jxpath.ri.compiler.Expression;
    40  import org.apache.commons.jxpath.ri.compiler.LocationPath;
    41  import org.apache.commons.jxpath.ri.compiler.Path;
    42  import org.apache.commons.jxpath.ri.compiler.TreeCompiler;
    43  import org.apache.commons.jxpath.ri.model.NodePointer;
    44  import org.apache.commons.jxpath.ri.model.NodePointerFactory;
    45  import org.apache.commons.jxpath.ri.model.VariablePointer;
    46  import org.apache.commons.jxpath.ri.model.beans.BeanPointerFactory;
    47  import org.apache.commons.jxpath.ri.model.beans.CollectionPointerFactory;
    48  import org.apache.commons.jxpath.ri.model.container.ContainerPointerFactory;
    49  import org.apache.commons.jxpath.ri.model.dynamic.DynamicPointerFactory;
    50  import org.apache.commons.jxpath.util.TypeUtils;
    51
    52  /***
    53   * The reference implementation of JXPathContext.
    54   *
    55   * @author Dmitri Plotnikov
    56   * @version $Revision: 1.43 $ $Date: 2004/04/04 23:16:23 $
    57   */
    58  public class JXPathContextReferenceImpl extends JXPathContext {
    59
    60      /***
    61       * Change this to <code>false</code> to disable soft caching of
    62       * CompiledExpressions.
    63       */
    64      public static final boolean USE_SOFT_CACHE = true;
    65
    66      private static final Compiler COMPILER = new TreeCompiler();
    67      private static Map compiled = new HashMap();
    68      private static int cleanupCount = 0;
    69
    70      private static Vector nodeFactories = new Vector();
    71      private static NodePointerFactory nodeFactoryArray[] = null;
    72      static {
    73          nodeFactories.add(new CollectionPointerFactory());
    74          nodeFactories.add(new BeanPointerFactory());
    75          nodeFactories.add(new DynamicPointerFactory());
    76
    77          // DOM  factory is only registered if DOM support is on the classpath
    78          Object domFactory = allocateConditionally(
    79                  "org.apache.commons.jxpath.ri.model.dom.DOMPointerFactory",
    80                  "org.w3c.dom.Node");
    81          if (domFactory != null) {
    82              nodeFactories.add(domFactory);
    83          }
    84
    85          // JDOM  factory is only registered if JDOM is on the classpath
    86          Object jdomFactory = allocateConditionally(
    87                  "org.apache.commons.jxpath.ri.model.jdom.JDOMPointerFactory",
    88                  "org.jdom.Document");
    89          if (jdomFactory != null) {
    90              nodeFactories.add(jdomFactory);
    91          }
    92
    93          // DynaBean factory is only registered if BeanUtils are on the classpath
    94          Object dynaBeanFactory =
    95              allocateConditionally(
    96                  "org.apache.commons.jxpath.ri.model.dynabeans."
    97                      + "DynaBeanPointerFactory",
    98                  "org.apache.commons.beanutils.DynaBean");
    99          if (dynaBeanFactory != null) {
    100             nodeFactories.add(dynaBeanFactory);
    101         }
    102
    103         nodeFactories.add(new ContainerPointerFactory());
    104         createNodeFactoryArray();
    105     }
    106
    107     private Pointer rootPointer;
    108     private Pointer contextPointer;
    109
    110     protected NamespaceResolver namespaceResolver;
    111
    112     // The frequency of the cache cleanup
    113     private static final int CLEANUP_THRESHOLD = 500;
    114
    115     protected JXPathContextReferenceImpl(JXPathContext parentContext,
    116                                          Object contextBean)
    117     {
    118         this(parentContext, contextBean, null);
    119     }
    120
    121     public JXPathContextReferenceImpl(
    122         JXPathContext parentContext,
    123         Object contextBean,
    124         Pointer contextPointer)
    125     {
    126         super(parentContext, contextBean);
    127
    128         synchronized (nodeFactories) {
    129             createNodeFactoryArray();
    130         }
    131
    132         if (contextPointer != null) {
    133             this.contextPointer = contextPointer;
    134             this.rootPointer =
    135                 NodePointer.newNodePointer(
    136                     new QName(null, "root"),
    137                     contextPointer.getRootNode(),
    138                     getLocale());
    139         }
    140         else {
    141             this.contextPointer =
    142                 NodePointer.newNodePointer(
    143                     new QName(null, "root"),
    144                     contextBean,
    145                     getLocale());
    146             this.rootPointer = this.contextPointer;
    147         }
    148
    149         namespaceResolver = new NamespaceResolver();
    150         namespaceResolver
    151                 .setNamespaceContextPointer((NodePointer) this.contextPointer);
    152     }
    153
    154     private static void createNodeFactoryArray() {
    155         if (nodeFactoryArray == null) {
    156             nodeFactoryArray =
    157                 (NodePointerFactory[]) nodeFactories.
    158                     toArray(new NodePointerFactory[0]);
    159             Arrays.sort(nodeFactoryArray, new Comparator() {
    160                 public int compare(Object a, Object b) {
    161                     int orderA = ((NodePointerFactory) a).getOrder();
    162                     int orderB = ((NodePointerFactory) b).getOrder();
    163                     return orderA - orderB;
    164                 }
    165             });
    166         }
    167     }
    168
    169     /***
    170      * Call this with a custom NodePointerFactory to add support for
    171      * additional types of objects.  Make sure the factory returns
    172      * a name that puts it in the right position on the list of factories.
    173      */
    174     public static void addNodePointerFactory(NodePointerFactory factory) {
    175         synchronized (nodeFactories) {
    176             nodeFactories.add(factory);
    177             nodeFactoryArray = null;
    178         }
    179     }
    180
    181     public static NodePointerFactory[] getNodePointerFactories() {
    182         return nodeFactoryArray;
    183     }
    184
    185     /***
    186      * Returns a static instance of TreeCompiler.
    187      *
    188      * Override this to return an aternate compiler.
    189      */
    190     protected Compiler getCompiler() {
    191         return COMPILER;
    192     }
    193
    194     protected CompiledExpression compilePath(String xpath) {
    195         return new JXPathCompiledExpression(xpath, compileExpression(xpath));
    196     }
    197
    198     private Expression compileExpression(String xpath) {
    199         Expression expr;
    200
    201         synchronized (compiled) {
    202             if (USE_SOFT_CACHE) {
    203                 expr = null;
    204                 SoftReference ref = (SoftReference) compiled.get(xpath);
    205                 if (ref != null) {
    206                     expr = (Expression) ref.get();
    207                 }
    208             }
    209             else {
    210                 expr = (Expression) compiled.get(xpath);
    211             }
    212         }
    213
    214         if (expr != null) {
    215             return expr;
    216         }
    217
    218         expr = (Expression) Parser.parseExpression(xpath, getCompiler());
    219
    220         synchronized (compiled) {
    221             if (USE_SOFT_CACHE) {
    222                 if (cleanupCount++ >= CLEANUP_THRESHOLD) {
    223                     Iterator it = compiled.entrySet().iterator();
    224                     while (it.hasNext()) {
    225                         Entry me = (Entry) it.next();
    226                         if (((SoftReference) me.getValue()).get() == null) {
    227                             it.remove();
    228                         }
    229                     }
    230                     cleanupCount = 0;
    231                 }
    232                 compiled.put(xpath, new SoftReference(expr));
    233             }
    234             else {
    235                 compiled.put(xpath, expr);
    236             }
    237         }
    238
    239         return expr;
    240     }
    241
    242     /***
    243      * Traverses the xpath and returns the resulting object. Primitive
    244      * types are wrapped into objects.
    245      */
    246     public Object getValue(String xpath) {
    247         Expression expression = compileExpression(xpath);
    248 // TODO: (work in progress) - trying to integrate with Xalan
    249 //        Object ctxNode = getNativeContextNode(expression);
    250 //        if (ctxNode != null) {
    251 //            System.err.println("WILL USE XALAN: " + xpath);
    252 //            CachedXPathAPI api = new CachedXPathAPI();
    253 //            try {
    254 //                if (expression instanceof Path) {
    255 //                    Node node = api.selectSingleNode((Node)ctxNode, xpath);
    256 //                    System.err.println("NODE: " + node);
    257 //                    if (node == null) {
    258 //                        return null;
    259 //                    }
    260 //                    return new DOMNodePointer(node, null).getValue();
    261 //                }
    262 //                else {
    263 //                    XObject object = api.eval((Node)ctxNode, xpath);
    264 //                    switch (object.getType()) {
    265 //                    case XObject.CLASS_STRING: return object.str();
    266 //                    case XObject.CLASS_NUMBER: return new Double(object.num());
    267 //                    case XObject.CLASS_BOOLEAN: return new Boolean(object.bool());
    268 //                    default:
    269 //                        System.err.println("OTHER TYPE: " + object.getTypeString());
    270 //                    }
    271 //                }
    272 //            }
    273 //            catch (TransformerException e) {
    274 //                // TODO Auto-generated catch block
    275 //                e.printStackTrace();
    276 //            }
    277 //            return
    278 //        }
    279
    280         return getValue(xpath, expression);
    281     }
    282
    283 //    private Object getNativeContextNode(Expression expression) {
    284 //        Object node = getNativeContextNode(getContextBean());
    285 //        if (node == null) {
    286 //            return null;
    287 //        }
    288 //
    289 //        List vars = expression.getUsedVariables();
    290 //        if (vars != null) {
    291 //            return null;
    292 //        }
    293 //
    294 //        return node;
    295 //    }
    296
    297 //    private Object getNativeContextNode(Object bean) {
    298 //        if (bean instanceof Number || bean instanceof String || bean instanceof Boolean) {
    299 //            return bean;
    300 //        }
    301 //        if (bean instanceof Node) {
    302 //            return (Node)bean;
    303 //        }
    304 //
    305 //        if (bean instanceof Container) {
    306 //            bean = ((Container)bean).getValue();
    307 //            return getNativeContextNode(bean);
    308 //        }
    309 //
    310 //        return null;
    311 //    }
    312
    313     public Object getValue(String xpath, Expression expr) {
    314         Object result = expr.computeValue(getEvalContext());
    315         if (result == null) {
    316             if (expr instanceof Path) {
    317                 if (!isLenient()) {
    318                     throw new JXPathException("No value for xpath: " + xpath);
    319                 }
    320             }
    321             return null;
    322         }
    323         if (result instanceof EvalContext) {
    324             EvalContext ctx = (EvalContext) result;
    325             result = ctx.getSingleNodePointer();
    326             if (!isLenient() && result == null) {
    327                 throw new JXPathException("No value for xpath: " + xpath);
    328             }
    329         }
    330         if (result instanceof NodePointer) {
    331             result = ((NodePointer) result).getValuePointer();
    332             if (!isLenient() && !((NodePointer) result).isActual()) {
    333                 // We need to differentiate between pointers representing
    334                 // a non-existing property and ones representing a property
    335                 // whose value is null.  In the latter case, the pointer
    336                 // is going to have isActual == false, but its parent,
    337                 // which is a non-node pointer identifying the bean property,
    338                 // will return isActual() == true.
    339                 NodePointer parent =
    340                     ((NodePointer) result).getImmediateParentPointer();
    341                 if (parent == null
    342                     || !parent.isContainer()
    343                     || !parent.isActual()) {
    344                     throw new JXPathException("No value for xpath: " + xpath);
    345                 }
    346             }
    347             result = ((NodePointer) result).getValue();
    348         }
    349         return result;
    350     }
    351
    352     /***
    353      * Calls getValue(xpath), converts the result to the required type
    354      * and returns the result of the conversion.
    355      */
    356     public Object getValue(String xpath, Class requiredType) {
    357         Expression expr = compileExpression(xpath);
    358         return getValue(xpath, expr, requiredType);
    359     }
    360
    361     public Object getValue(String xpath, Expression expr, Class requiredType) {
    362         Object value = getValue(xpath, expr);
    363         if (value != null && requiredType != null) {
    364             if (!TypeUtils.canConvert(value, requiredType)) {
    365                 throw new JXPathException(
    366                     "Invalid expression type. '"
    367                         + xpath
    368                         + "' returns "
    369                         + value.getClass().getName()
    370                         + ". It cannot be converted to "
    371                         + requiredType.getName());
    372             }
    373             value = TypeUtils.convert(value, requiredType);
    374         }
    375         return value;
    376     }
    377
    378     /***
    379      * Traverses the xpath and returns a Iterator of all results found
    380      * for the path. If the xpath matches no properties
    381      * in the graph, the Iterator will not be null.
    382      */
    383     public Iterator iterate(String xpath) {
    384         return iterate(xpath, compileExpression(xpath));
    385     }
    386
    387     public Iterator iterate(String xpath, Expression expr) {
    388         return expr.iterate(getEvalContext());
    389     }
    390
    391     public Pointer getPointer(String xpath) {
    392         return getPointer(xpath, compileExpression(xpath));
    393     }
    394
    395     public Pointer getPointer(String xpath, Expression expr) {
    396         Object result = expr.computeValue(getEvalContext());
    397         if (result instanceof EvalContext) {
    398             result = ((EvalContext) result).getSingleNodePointer();
    399         }
    400         if (result instanceof Pointer) {
    401             if (!isLenient() && !((NodePointer) result).isActual()) {
    402                 throw new JXPathException("No pointer for xpath: " + xpath);
    403             }
    404             return (Pointer) result;
    405         }
    406         else {
    407             return NodePointer.newNodePointer(null, result, getLocale());
    408         }
    409     }
    410
    411     public void setValue(String xpath, Object value) {
    412         setValue(xpath, compileExpression(xpath), value);
    413     }
    414
    415
    416     public void setValue(String xpath, Expression expr, Object value) {
    417         try {
    418             setValue(xpath, expr, value, false);
    419         }
    420         catch (Throwable ex) {
    421             throw new JXPathException(
    422                 "Exception trying to set value with xpath " + xpath, ex);
    423         }
    424     }
    425
    426     public Pointer createPath(String xpath) {
    427         return createPath(xpath, compileExpression(xpath));
    428     }
    429
    430     public Pointer createPath(String xpath, Expression expr) {
    431         try {
    432             Object result = expr.computeValue(getEvalContext());
    433             Pointer pointer = null;
    434
    435             if (result instanceof Pointer) {
    436                 pointer = (Pointer) result;
    437             }
    438             else if (result instanceof EvalContext) {
    439                 EvalContext ctx = (EvalContext) result;
    440                 pointer = ctx.getSingleNodePointer();
    441             }
    442             else {
    443                 checkSimplePath(expr);
    444                 // This should never happen
    445                 throw new JXPathException("Cannot create path:" + xpath);
    446             }
    447             return ((NodePointer) pointer).createPath(this);
    448         }
    449         catch (Throwable ex) {
    450             throw new JXPathException(
    451                 "Exception trying to create xpath " + xpath,
    452                 ex);
    453         }
    454     }
    455
    456     public Pointer createPathAndSetValue(String xpath, Object value) {
    457         return createPathAndSetValue(xpath, compileExpression(xpath), value);
    458     }
    459
    460     public Pointer createPathAndSetValue(
    461         String xpath,
    462         Expression expr,
    463         Object value)
    464     {
    465         try {
    466             return setValue(xpath, expr, value, true);
    467         }
    468         catch (Throwable ex) {
    469             throw new JXPathException(
    470                 "Exception trying to create xpath " + xpath,
    471                 ex);
    472         }
    473     }
    474
    475     private Pointer setValue(
    476         String xpath,
    477         Expression expr,
    478         Object value,
    479         boolean create)
    480     {
    481         Object result = expr.computeValue(getEvalContext());
    482         Pointer pointer = null;
    483
    484         if (result instanceof Pointer) {
    485             pointer = (Pointer) result;
    486         }
    487         else if (result instanceof EvalContext) {
    488             EvalContext ctx = (EvalContext) result;
    489             pointer = ctx.getSingleNodePointer();
    490         }
    491         else {
    492             if (create) {
    493                 checkSimplePath(expr);
    494             }
    495
    496             // This should never happen
    497             throw new JXPathException("Cannot set value for xpath: " + xpath);
    498         }
    499         if (create) {
    500             pointer = ((NodePointer) pointer).createPath(this, value);
    501         }
    502         else {
    503             pointer.setValue(value);
    504         }
    505         return pointer;
    506     }
    507
    508     /***
    509      * Checks if the path follows the JXPath restrictions on the type
    510      * of path that can be passed to create... methods.
    511      */
    512     private void checkSimplePath(Expression expr) {
    513         if (!(expr instanceof LocationPath)
    514             || !((LocationPath) expr).isSimplePath()) {
    515             throw new JXPathException(
    516                 "JXPath can only create a path if it uses exclusively "
    517                     + "the child:: and attribute:: axes and has "
    518                     + "no context-dependent predicates");
    519         }
    520     }
    521
    522     /***
    523      * Traverses the xpath and returns an Iterator of Pointers.
    524      * A Pointer provides easy access to a property.
    525      * If the xpath matches no properties
    526      * in the graph, the Iterator be empty, but not null.
    527      */
    528     public Iterator iteratePointers(String xpath) {
    529         return iteratePointers(xpath, compileExpression(xpath));
    530     }
    531
    532     public Iterator iteratePointers(String xpath, Expression expr) {
    533         return expr.iteratePointers(getEvalContext());
    534     }
    535
    536     public void removePath(String xpath) {
    537         removePath(xpath, compileExpression(xpath));
    538     }
    539
    540     public void removePath(String xpath, Expression expr) {
    541         try {
    542             NodePointer pointer = (NodePointer) getPointer(xpath, expr);
    543             if (pointer != null) {
    544                 ((NodePointer) pointer).remove();
    545             }
    546         }
    547         catch (Throwable ex) {
    548             throw new JXPathException(
    549                 "Exception trying to remove xpath " + xpath,
    550                 ex);
    551         }
    552     }
    553
    554     public void removeAll(String xpath) {
    555         removeAll(xpath, compileExpression(xpath));
    556     }
    557
    558     public void removeAll(String xpath, Expression expr) {
    559         try {
    560             ArrayList list = new ArrayList();
    561             Iterator it = expr.iteratePointers(getEvalContext());
    562             while (it.hasNext()) {
    563                 list.add(it.next());
    564             }
    565             Collections.sort(list);
    566             for (int i = list.size() - 1; i >= 0; i--) {
    567                 NodePointer pointer = (NodePointer) list.get(i);
    568                 pointer.remove();
    569             }
    570         }
    571         catch (Throwable ex) {
    572             throw new JXPathException(
    573                 "Exception trying to remove all for xpath " + xpath,
    574                 ex);
    575         }
    576     }
    577
    578     public JXPathContext getRelativeContext(Pointer pointer) {
    579         Object contextBean = pointer.getNode();
    580         if (contextBean == null) {
    581             throw new JXPathException(
    582                 "Cannot create a relative context for a non-existent node: "
    583                     + pointer);
    584         }
    585         return new JXPathContextReferenceImpl(this, contextBean, pointer);
    586     }
    587
    588     public Pointer getContextPointer() {
    589         return contextPointer;
    590     }
    591
    592     private NodePointer getAbsoluteRootPointer() {
    593         return (NodePointer) rootPointer;
    594     }
    595
    596     private EvalContext getEvalContext() {
    597         return new InitialContext(new RootContext(this,
    598                 (NodePointer) getContextPointer()));
    599     }
    600
    601     public EvalContext getAbsoluteRootContext() {
    602         return new InitialContext(new RootContext(this,
    603                 getAbsoluteRootPointer()));
    604     }
    605
    606     public NodePointer getVariablePointer(QName name) {
    607         String varName = name.toString();
    608         JXPathContext varCtx = this;
    609         Variables vars = null;
    610         while (varCtx != null) {
    611             vars = varCtx.getVariables();
    612             if (vars.isDeclaredVariable(varName)) {
    613                 break;
    614             }
    615             varCtx = varCtx.getParentContext();
    616             vars = null;
    617         }
    618         if (vars != null) {
    619             return new VariablePointer(vars, name);
    620         }
    621         else {
    622             // The variable is not declared, but we will create
    623             // a pointer anyway in case the user want to set, rather
    624             // than get, the value of the variable.
    625             return new VariablePointer(name);
    626         }
    627     }
    628
    629     public Function getFunction(QName functionName, Object[] parameters) {
    630         String namespace = functionName.getPrefix();
    631         String name = functionName.getName();
    632         JXPathContext funcCtx = this;
    633         Function func = null;
    634         Functions funcs;
    635         while (funcCtx != null) {
    636             funcs = funcCtx.getFunctions();
    637             if (funcs != null) {
    638                 func = funcs.getFunction(namespace, name, parameters);
    639                 if (func != null) {
    640                     return func;
    641                 }
    642             }
    643             funcCtx = funcCtx.getParentContext();
    644         }
    645         throw new JXPathException(
    646             "Undefined function: " + functionName.toString());
    647     }
    648
    649     public void registerNamespace(String prefix, String namespaceURI) {
    650         if (namespaceResolver.isSealed()) {
    651             namespaceResolver = (NamespaceResolver) namespaceResolver.clone();
    652         }
    653         namespaceResolver.registerNamespace(prefix, namespaceURI);
    654     }
    655
    656     public String getNamespaceURI(String prefix) {
    657         return namespaceResolver.getNamespaceURI(prefix);
    658     }
    659
    660     public void setNamespaceContextPointer(Pointer pointer) {
    661         if (namespaceResolver.isSealed()) {
    662             namespaceResolver = (NamespaceResolver) namespaceResolver.clone();
    663         }
    664         namespaceResolver.setNamespaceContextPointer((NodePointer) pointer);
    665     }
    666
    667     public Pointer getNamespaceContextPointer() {
    668         return namespaceResolver.getNamespaceContextPointer();
    669     }
    670
    671     public NamespaceResolver getNamespaceResolver() {
    672         namespaceResolver.seal();
    673         return namespaceResolver;
    674     }
    675
    676     /***
    677      * Checks if existenceCheckClass exists on the class path. If so, allocates
    678      * an instance of the specified class, otherwise returns null.
    679      */
    680     public static Object allocateConditionally(
    681             String className,
    682             String existenceCheckClassName)
    683     {
    684         try {
    685             try {
    686                 Class.forName(existenceCheckClassName);
    687             }
    688             catch (ClassNotFoundException ex) {
    689                 return null;
    690             }
    691
    692             Class cls = Class.forName(className);
    693             return cls.newInstance();
    694         }
    695         catch (Exception ex) {
    696             throw new JXPathException("Cannot allocate " + className, ex);
    697         }
    698     }
    699 }