View Javadoc

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 }