001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.jxpath.ri.model;
018
019import java.util.HashSet;
020import java.util.Locale;
021
022import org.apache.commons.jxpath.AbstractFactory;
023import org.apache.commons.jxpath.JXPathContext;
024import org.apache.commons.jxpath.JXPathException;
025import org.apache.commons.jxpath.NodeSet;
026import org.apache.commons.jxpath.Pointer;
027import org.apache.commons.jxpath.ri.Compiler;
028import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
029import org.apache.commons.jxpath.ri.NamespaceResolver;
030import org.apache.commons.jxpath.ri.QName;
031import org.apache.commons.jxpath.ri.compiler.NodeNameTest;
032import org.apache.commons.jxpath.ri.compiler.NodeTest;
033import org.apache.commons.jxpath.ri.compiler.NodeTypeTest;
034import org.apache.commons.jxpath.ri.model.beans.NullPointer;
035
036/**
037 * Common superclass for Pointers of all kinds.  A NodePointer maps to
038 * a deterministic XPath that represents the location of a node in an
039 * object graph. This XPath uses only simple axes: child, namespace and
040 * attribute and only simple, context-independent predicates.
041 *
042 * @author Dmitri Plotnikov
043 * @version $Revision: 668329 $ $Date: 2008-06-16 16:59:48 -0500 (Mon, 16 Jun 2008) $
044 */
045public abstract class NodePointer implements Pointer {
046
047    /** Whole collection index. */
048    public static final int WHOLE_COLLECTION = Integer.MIN_VALUE;
049
050    /** Constant to indicate unknown namespace */
051    public static final String UNKNOWN_NAMESPACE = "<<unknown namespace>>";
052
053    /** Index for this NodePointer */
054    protected int index = WHOLE_COLLECTION;
055
056    private boolean attribute = false;
057    private NamespaceResolver namespaceResolver;
058    private transient Object rootNode;
059
060    /**
061     * Allocates an entirely new NodePointer by iterating through all installed
062     * NodePointerFactories until it finds one that can create a pointer.
063     * @param name QName
064     * @param bean Object
065     * @param locale Locale
066     * @return NodePointer
067     */
068    public static NodePointer newNodePointer(
069        QName name,
070        Object bean,
071        Locale locale) {
072        NodePointer pointer = null;
073        if (bean == null) {
074            pointer = new NullPointer(name, locale);
075            return pointer;
076        }
077
078        NodePointerFactory[] factories =
079            JXPathContextReferenceImpl.getNodePointerFactories();
080        for (int i = 0; i < factories.length; i++) {
081            pointer = factories[i].createNodePointer(name, bean, locale);
082            if (pointer != null) {
083                return pointer;
084            }
085        }
086        throw new JXPathException(
087            "Could not allocate a NodePointer for object of "
088                + bean.getClass());
089    }
090
091    /**
092     * Allocates an new child NodePointer by iterating through all installed
093     * NodePointerFactories until it finds one that can create a pointer.
094     * @param parent pointer
095     * @param name QName
096     * @param bean Object
097     * @return NodePointer
098     */
099    public static NodePointer newChildNodePointer(
100        NodePointer parent,
101        QName name,
102        Object bean) {
103        NodePointerFactory[] factories =
104            JXPathContextReferenceImpl.getNodePointerFactories();
105        for (int i = 0; i < factories.length; i++) {
106            NodePointer pointer =
107                factories[i].createNodePointer(parent, name, bean);
108            if (pointer != null) {
109                return pointer;
110            }
111        }
112        throw new JXPathException(
113            "Could not allocate a NodePointer for object of "
114                + bean.getClass());
115    }
116
117    /** Parent pointer */
118    protected NodePointer parent;
119
120    /** Locale */
121    protected Locale locale;
122
123    /**
124     * Create a new NodePointer.
125     * @param parent Pointer
126     */
127    protected NodePointer(NodePointer parent) {
128        this.parent = parent;
129    }
130
131    /**
132     * Create a new NodePointer.
133     * @param parent Pointer
134     * @param locale Locale
135     */
136    protected NodePointer(NodePointer parent, Locale locale) {
137        this.parent = parent;
138        this.locale = locale;
139    }
140
141    /**
142     * Get the NamespaceResolver associated with this NodePointer.
143     * @return NamespaceResolver
144     */
145    public NamespaceResolver getNamespaceResolver() {
146        if (namespaceResolver == null && parent != null) {
147            namespaceResolver = parent.getNamespaceResolver();
148        }
149        return namespaceResolver;
150    }
151
152    /**
153     * Set the NamespaceResolver for this NodePointer.
154     * @param namespaceResolver NamespaceResolver
155     */
156    public void setNamespaceResolver(NamespaceResolver namespaceResolver) {
157        this.namespaceResolver = namespaceResolver;
158    }
159
160    /**
161     * Get the parent pointer.
162     * @return NodePointer
163     */
164    public NodePointer getParent() {
165        NodePointer pointer = parent;
166        while (pointer != null && pointer.isContainer()) {
167            pointer = pointer.getImmediateParentPointer();
168        }
169        return pointer;
170    }
171
172    /**
173     * Get the immediate parent pointer.
174     * @return NodePointer
175     */
176    public NodePointer getImmediateParentPointer() {
177        return parent;
178    }
179
180    /**
181     * Set to true if the pointer represents the "attribute::" axis.
182     * @param attribute boolean
183     */
184    public void setAttribute(boolean attribute) {
185        this.attribute = attribute;
186    }
187
188    /**
189     * Returns true if the pointer represents the "attribute::" axis.
190     * @return boolean
191     */
192    public boolean isAttribute() {
193        return attribute;
194    }
195
196    /**
197     * Returns true if this Pointer has no parent.
198     * @return boolean
199     */
200    public boolean isRoot() {
201        return parent == null;
202    }
203
204    /**
205     * If true, this node does not have children
206     * @return boolean
207     */
208    public abstract boolean isLeaf();
209
210    /**
211     * Learn whether this pointer is considered to be a node.
212     * @return boolean
213     * @deprecated Please use !isContainer()
214     */
215    public boolean isNode() {
216        return !isContainer();
217    }
218
219    /**
220     * If true, this node is auxiliary and can only be used as an intermediate in
221     * the chain of pointers.
222     * @return boolean
223     */
224    public boolean isContainer() {
225        return false;
226    }
227
228    /**
229     * If the pointer represents a collection, the index identifies
230     * an element of that collection.  The default value of <code>index</code>
231     * is <code>WHOLE_COLLECTION</code>, which just means that the pointer
232     * is not indexed at all.
233     * Note: the index on NodePointer starts with 0, not 1.
234     * @return int
235     */
236    public int getIndex() {
237        return index;
238    }
239
240    /**
241     * Set the index of this NodePointer.
242     * @param index int
243     */
244    public void setIndex(int index) {
245        this.index = index;
246    }
247
248    /**
249     * Returns <code>true</code> if the value of the pointer is an array or
250     * a Collection.
251     * @return boolean
252     */
253    public abstract boolean isCollection();
254
255    /**
256     * If the pointer represents a collection (or collection element),
257     * returns the length of the collection.
258     * Otherwise returns 1 (even if the value is null).
259     * @return int
260     */
261    public abstract int getLength();
262
263    /**
264     * By default, returns <code>getNode()</code>, can be overridden to
265     * return a "canonical" value, like for instance a DOM element should
266     * return its string value.
267     * @return Object value
268     */
269    public Object getValue() {
270        NodePointer valuePointer = getValuePointer();
271        if (valuePointer != this) {
272            return valuePointer.getValue();
273        }
274        // Default behavior is to return the same as getNode()
275        return getNode();
276    }
277
278    /**
279     * If this pointer manages a transparent container, like a variable,
280     * this method returns the pointer to the contents.
281     * Only an auxiliary (non-node) pointer can (and should) return a
282     * value pointer other than itself.
283     * Note that you probably don't want to override
284     * <code>getValuePointer()</code> directly.  Override the
285     * <code>getImmediateValuePointer()</code> method instead.  The
286     * <code>getValuePointer()</code> method is calls
287     * <code>getImmediateValuePointer()</code> and, if the result is not
288     * <code>this</code>, invokes <code>getValuePointer()</code> recursively.
289     * The idea here is to open all nested containers. Let's say we have a
290     * container within a container within a container. The
291     * <code>getValuePointer()</code> method should then open all those
292     * containers and return the pointer to the ultimate contents. It does so
293     * with the above recursion.
294     * @return NodePointer
295     */
296    public NodePointer getValuePointer() {
297        NodePointer ivp = getImmediateValuePointer();
298        return ivp == this ? this : ivp.getValuePointer();
299    }
300
301    /**
302     * @see #getValuePointer()
303     *
304     * @return NodePointer is either <code>this</code> or a pointer
305     *   for the immediately contained value.
306     */
307    public NodePointer getImmediateValuePointer() {
308        return this;
309    }
310
311    /**
312     * An actual pointer points to an existing part of an object graph, even
313     * if it is null. A non-actual pointer represents a part that does not exist
314     * at all.
315     * For instance consider the pointer "/address/street".
316     * If both <em>address</em> and <em>street</em> are not null,
317     * the pointer is actual.
318     * If <em>address</em> is not null, but <em>street</em> is null,
319     * the pointer is still actual.
320     * If <em>address</em> is null, the pointer is not actual.
321     * (In JavaBeans) if <em>address</em> is not a property of the root bean,
322     * a Pointer for this path cannot be obtained at all - actual or otherwise.
323     * @return boolean
324     */
325    public boolean isActual() {
326        return index == WHOLE_COLLECTION || index >= 0 && index < getLength();
327    }
328
329    /**
330     * Returns the name of this node. Can be null.
331     * @return QName
332     */
333    public abstract QName getName();
334
335    /**
336     * Returns the value represented by the pointer before indexing.
337     * So, if the node represents an element of a collection, this
338     * method returns the collection itself.
339     * @return Object value
340     */
341    public abstract Object getBaseValue();
342
343    /**
344     * Returns the object the pointer points to; does not convert it
345     * to a "canonical" type.
346     * @return Object node value
347     * @deprecated 1.1 Please use getNode()
348     */
349    public Object getNodeValue() {
350        return getNode();
351    }
352
353    /**
354     * Returns the object the pointer points to; does not convert it
355     * to a "canonical" type. Opens containers, properties etc and returns
356     * the ultimate contents.
357     * @return Object node
358     */
359    public Object getNode() {
360        return getValuePointer().getImmediateNode();
361    }
362
363    /**
364     * Get the root node.
365     * @return Object value of this pointer's root (top parent).
366     */
367    public synchronized Object getRootNode() {
368        if (rootNode == null) {
369            rootNode = parent == null ? getImmediateNode() : parent.getRootNode();
370        }
371        return rootNode;
372    }
373
374    /**
375     * Returns the object the pointer points to; does not convert it
376     * to a "canonical" type.
377     * @return Object node
378     */
379    public abstract Object getImmediateNode();
380
381    /**
382     * Converts the value to the required type and changes the corresponding
383     * object to that value.
384     * @param value the value to set
385     */
386    public abstract void setValue(Object value);
387
388    /**
389     * Compares two child NodePointers and returns a positive number,
390     * zero or a positive number according to the order of the pointers.
391     * @param pointer1 first pointer to be compared
392     * @param pointer2 second pointer to be compared
393     * @return int per Java comparison conventions
394     */
395    public abstract int compareChildNodePointers(
396            NodePointer pointer1, NodePointer pointer2);
397
398    /**
399     * Checks if this Pointer matches the supplied NodeTest.
400     * @param test the NodeTest to execute
401     * @return true if a match
402     */
403    public boolean testNode(NodeTest test) {
404        if (test == null) {
405            return true;
406        }
407        if (test instanceof NodeNameTest) {
408            if (isContainer()) {
409                return false;
410            }
411            NodeNameTest nodeNameTest = (NodeNameTest) test;
412            QName testName = nodeNameTest.getNodeName();
413            QName nodeName = getName();
414            if (nodeName == null) {
415                return false;
416            }
417
418            String testPrefix = testName.getPrefix();
419            String nodePrefix = nodeName.getPrefix();
420            if (!equalStrings(testPrefix, nodePrefix)) {
421                String testNS = getNamespaceURI(testPrefix);
422                String nodeNS = getNamespaceURI(nodePrefix);
423                if (!equalStrings(testNS, nodeNS)) {
424                    return false;
425                }
426            }
427            if (nodeNameTest.isWildcard()) {
428                return true;
429            }
430            return testName.getName().equals(nodeName.getName());
431        }
432        return test instanceof NodeTypeTest
433                && ((NodeTypeTest) test).getNodeType() == Compiler.NODE_TYPE_NODE && isNode();
434    }
435
436    /**
437     * Compare two strings, either of which may be null, for equality.
438     * @param s1 the first String to compare
439     * @param s2 the second String to compare
440     * @return true if both Strings are null, same or equal
441     */
442    private static boolean equalStrings(String s1, String s2) {
443        return s1 == s2 || s1 != null && s1.equals(s2);
444    }
445
446    /**
447     *  Called directly by JXPathContext. Must create path and
448     *  set value.
449     *  @param context the owning JXPathContext
450     *  @param value the new value to set
451     *  @return created NodePointer
452     */
453    public NodePointer createPath(JXPathContext context, Object value) {
454        setValue(value);
455        return this;
456    }
457
458    /**
459     * Remove the node of the object graph this pointer points to.
460     */
461    public void remove() {
462        // It is a no-op
463
464//        System.err.println("REMOVING: " + asPath() + " " + getClass());
465//        printPointerChain();
466    }
467
468    /**
469     * Called by a child pointer when it needs to create a parent object.
470     * Must create an object described by this pointer and return
471     * a new pointer that properly describes the new object.
472     * @param context the owning JXPathContext
473     * @return created NodePointer
474     */
475    public NodePointer createPath(JXPathContext context) {
476        return this;
477    }
478
479    /**
480     * Called by a child pointer if that child needs to assign the value
481     * supplied in the createPath(context, value) call to a non-existent
482     * node. This method may have to expand the collection in order to assign
483     * the element.
484     * @param context the owning JXPathCOntext
485     * @param name the QName at which a child should be created
486     * @param index child index.
487     * @param value node value to set
488     * @return created NodePointer
489     */
490    public NodePointer createChild(
491        JXPathContext context,
492        QName name,
493        int index,
494        Object value) {
495        throw new JXPathException("Cannot create an object for path "
496                + asPath() + "/" + name + "[" + (index + 1) + "]"
497                + ", operation is not allowed for this type of node");
498    }
499
500    /**
501     * Called by a child pointer when it needs to create a parent object for a
502     * non-existent collection element. It may have to expand the collection,
503     * then create an element object and return a new pointer describing the
504     * newly created element.
505     * @param context the owning JXPathCOntext
506     * @param name the QName at which a child should be created
507     * @param index child index.
508     * @return created NodePointer
509     */
510    public NodePointer createChild(JXPathContext context, QName name, int index) {
511        throw new JXPathException("Cannot create an object for path "
512                + asPath() + "/" + name + "[" + (index + 1) + "]"
513                + ", operation is not allowed for this type of node");
514    }
515
516    /**
517     * Called to create a non-existing attribute
518     * @param context the owning JXPathCOntext
519     * @param name the QName at which an attribute should be created
520     * @return created NodePointer
521     */
522    public NodePointer createAttribute(JXPathContext context, QName name) {
523        throw new JXPathException("Cannot create an attribute for path "
524                + asPath() + "/@" + name
525                + ", operation is not allowed for this type of node");
526    }
527
528    /**
529     * If the Pointer has a parent, returns the parent's locale; otherwise
530     * returns the locale specified when this Pointer was created.
531     * @return Locale for this NodePointer
532     */
533    public Locale getLocale() {
534        if (locale == null && parent != null) {
535            locale = parent.getLocale();
536        }
537        return locale;
538    }
539
540    /**
541     * Check whether our locale matches the specified language.
542     * @param lang String language to check
543     * @return true if the selected locale name starts
544     *              with the specified prefix <i>lang</i>, case-insensitive.
545     */
546    public boolean isLanguage(String lang) {
547        Locale loc = getLocale();
548        String name = loc.toString().replace('_', '-');
549        return name.toUpperCase(Locale.ENGLISH).startsWith(lang.toUpperCase(Locale.ENGLISH));
550    }
551
552    /**
553     * Returns a NodeIterator that iterates over all children or all children
554     * that match the given NodeTest, starting with the specified one.
555     * @param test NodeTest to filter children
556     * @param reverse specified iteration direction
557     * @param startWith the NodePointer to start with
558     * @return NodeIterator
559     */
560    public NodeIterator childIterator(
561        NodeTest test,
562        boolean reverse,
563        NodePointer startWith) {
564        NodePointer valuePointer = getValuePointer();
565        return valuePointer == null || valuePointer == this ? null
566                : valuePointer.childIterator(test, reverse, startWith);
567    }
568
569    /**
570     * Returns a NodeIterator that iterates over all attributes of the current
571     * node matching the supplied node name (could have a wildcard).
572     * May return null if the object does not support the attributes.
573     * @param qname the attribute name to test
574     * @return NodeIterator
575     */
576    public NodeIterator attributeIterator(QName qname) {
577        NodePointer valuePointer = getValuePointer();
578        return valuePointer == null || valuePointer == this ? null
579                : valuePointer.attributeIterator(qname);
580    }
581
582    /**
583     * Returns a NodeIterator that iterates over all namespaces of the value
584     * currently pointed at.
585     * May return null if the object does not support the namespaces.
586     * @return NodeIterator
587     */
588    public NodeIterator namespaceIterator() {
589        return null;
590    }
591
592    /**
593     * Returns a NodePointer for the specified namespace. Will return null
594     * if namespaces are not supported.
595     * Will return UNKNOWN_NAMESPACE if there is no such namespace.
596     * @param namespace incoming namespace
597     * @return NodePointer for <code>namespace</code>
598     */
599    public NodePointer namespacePointer(String namespace) {
600        return null;
601    }
602
603    /**
604     * Decodes a namespace prefix to the corresponding URI.
605     * @param prefix prefix to decode
606     * @return String uri
607     */
608    public String getNamespaceURI(String prefix) {
609        return null;
610    }
611
612    /**
613     * Returns the namespace URI associated with this Pointer.
614     * @return String uri
615     */
616    public String getNamespaceURI() {
617        return null;
618    }
619
620    /**
621     * Returns true if the supplied prefix represents the
622     * default namespace in the context of the current node.
623     * @param prefix the prefix to check
624     * @return <code>true</code> if prefix is default
625     */
626    protected boolean isDefaultNamespace(String prefix) {
627        if (prefix == null) {
628            return true;
629        }
630
631        String namespace = getNamespaceURI(prefix);
632        return namespace != null && namespace.equals(getDefaultNamespaceURI());
633    }
634
635    /**
636     * Get the default ns uri
637     * @return String uri
638     */
639    protected String getDefaultNamespaceURI() {
640        return null;
641    }
642
643    /**
644     * Locates a node by ID.
645     * @param context JXPathContext owning context
646     * @param id String id
647     * @return Pointer found
648     */
649    public Pointer getPointerByID(JXPathContext context, String id) {
650        return context.getPointerByID(id);
651    }
652
653    /**
654     * Locates a node by key and value.
655     * @param context owning JXPathContext
656     * @param key key to search for
657     * @param value value to match
658     * @return Pointer found
659     */
660    public Pointer getPointerByKey(
661            JXPathContext context,
662            String key,
663            String value) {
664        return context.getPointerByKey(key, value);
665    }
666
667    /**
668     * Find a NodeSet by key/value.
669     * @param context owning JXPathContext
670     * @param key key to search for
671     * @param value value to match
672     * @return NodeSet found
673     */
674    public NodeSet getNodeSetByKey(JXPathContext context, String key, Object value) {
675        return context.getNodeSetByKey(key, value);
676    }
677
678    /**
679     * Returns an XPath that maps to this Pointer.
680     * @return String xpath expression
681     */
682    public String asPath() {
683        // If the parent of this node is a container, it is responsible
684        // for appended this node's part of the path.
685        if (parent != null && parent.isContainer()) {
686            return parent.asPath();
687        }
688
689        StringBuffer buffer = new StringBuffer();
690        if (parent != null) {
691            buffer.append(parent.asPath());
692        }
693
694        if (buffer.length() == 0
695            || buffer.charAt(buffer.length() - 1) != '/') {
696            buffer.append('/');
697        }
698        if (attribute) {
699            buffer.append('@');
700        }
701        buffer.append(getName());
702
703        if (index != WHOLE_COLLECTION && isCollection()) {
704            buffer.append('[').append(index + 1).append(']');
705        }
706        return buffer.toString();
707    }
708
709    /**
710     * Clone this NodePointer.
711     * @return cloned NodePointer
712     */
713    public Object clone() {
714        try {
715            NodePointer ptr = (NodePointer) super.clone();
716            if (parent != null) {
717                ptr.parent = (NodePointer) parent.clone();
718            }
719            return ptr;
720        }
721        catch (CloneNotSupportedException ex) {
722            // Of course it is supported
723            ex.printStackTrace();
724        }
725        return null;
726    }
727
728    public String toString() {
729        return asPath();
730    }
731
732    public int compareTo(Object object) {
733        if (object == this) {
734            return 0;
735        }
736        // Let it throw a ClassCastException
737        NodePointer pointer = (NodePointer) object;
738        if (parent == pointer.parent) {
739            return parent == null ? 0 : parent.compareChildNodePointers(this, pointer);
740        }
741
742        // Task 1: find the common parent
743        int depth1 = 0;
744        NodePointer p1 = this;
745        HashSet parents1 = new HashSet();
746        while (p1 != null) {
747            depth1++;
748            p1 = p1.parent;
749            if (p1 != null) {
750                parents1.add(p1);
751            }
752        }
753        boolean commonParentFound = false;
754        int depth2 = 0;
755        NodePointer p2 = pointer;
756        while (p2 != null) {
757            depth2++;
758            p2 = p2.parent;
759            if (parents1.contains(p2)) {
760                commonParentFound = true;
761            }
762        }
763        //nodes from different graphs are equal, else continue comparison:
764        return commonParentFound ? compareNodePointers(this, depth1, pointer, depth2) : 0;
765    }
766
767    /**
768     * Compare node pointers.
769     * @param p1 pointer 1
770     * @param depth1 depth 1
771     * @param p2 pointer 2
772     * @param depth2 depth 2
773     * @return comparison result: (< 0) -> (p1 lt p2); (0) -> (p1 eq p2); (> 0) -> (p1 gt p2)
774     */
775    private int compareNodePointers(
776        NodePointer p1,
777        int depth1,
778        NodePointer p2,
779        int depth2) {
780        if (depth1 < depth2) {
781            int r = compareNodePointers(p1, depth1, p2.parent, depth2 - 1);
782            return r == 0 ? -1 : r;
783        }
784        if (depth1 > depth2) {
785            int r = compareNodePointers(p1.parent, depth1 - 1, p2, depth2);
786            return r == 0 ? 1 : r;
787        }
788        //henceforth depth1 == depth2:
789        if (p1 == p2 || p1 != null && p1.equals(p2)) {
790            return 0;
791        }
792        if (depth1 == 1) {
793            throw new JXPathException(
794                    "Cannot compare pointers that do not belong to the same tree: '"
795                    + p1 + "' and '" + p2 + "'");
796        }
797        int r = compareNodePointers(p1.parent, depth1 - 1, p2.parent, depth2 - 1);
798        return r == 0 ? p1.parent.compareChildNodePointers(p1, p2) : r;
799    }
800
801    /**
802     * Print internal structure of a pointer for debugging
803     */
804    public void printPointerChain() {
805        printDeep(this, "");
806    }
807
808    /**
809     * Return a string escaping single and double quotes.
810     * @param string string to treat
811     * @return string with any necessary changes made.
812     */
813    protected String escape(String string) {
814        final char[] c = new char[] { '\'', '"' };
815        final String[] esc = new String[] { "&apos;", "&quot;" };
816        StringBuffer sb = null;
817        for (int i = 0; sb == null && i < c.length; i++) {
818            if (string.indexOf(c[i]) >= 0) {
819                sb = new StringBuffer(string);
820            }
821        }
822        if (sb == null) {
823            return string;
824        }
825        for (int i = 0; i < c.length; i++) {
826            if (string.indexOf(c[i]) < 0) {
827                continue;
828            }
829            int pos = 0;
830            while (pos < sb.length()) {
831                if (sb.charAt(pos) == c[i]) {
832                    sb.replace(pos, pos + 1, esc[i]);
833                    pos += esc[i].length();
834                }
835                else {
836                    pos++;
837                }
838            }
839        }
840        return sb.toString();
841    }
842
843    /**
844     * Get the AbstractFactory associated with the specified JXPathContext.
845     * @param context JXPathContext
846     * @return AbstractFactory
847     */
848    protected AbstractFactory getAbstractFactory(JXPathContext context) {
849        AbstractFactory factory = context.getFactory();
850        if (factory == null) {
851            throw new JXPathException(
852                "Factory is not set on the JXPathContext - cannot create path: "
853                    + asPath());
854        }
855        return factory;
856    }
857
858    /**
859     * Print deep
860     * @param pointer to print
861     * @param indent indentation level
862     */
863    private static void printDeep(NodePointer pointer, String indent) {
864        if (indent.length() == 0) {
865            System.err.println(
866                "POINTER: "
867                    + pointer
868                    + "("
869                    + pointer.getClass().getName()
870                    + ")");
871        }
872        else {
873            System.err.println(
874                indent
875                    + " of "
876                    + pointer
877                    + "("
878                    + pointer.getClass().getName()
879                    + ")");
880        }
881        if (pointer.getImmediateParentPointer() != null) {
882            printDeep(pointer.getImmediateParentPointer(), indent + "  ");
883        }
884    }
885}