001    package com.saelist.stx;
002    
003    import java.io.*;
004    import java.util.*;
005    import com.saelist.stx.xpath.PairXPath;
006    import com.saelist.stx.util.Escaper;
007    import org.jaxen.JaxenException;
008    import org.apache.log4j.*;
009    
010    /** The central data structure of the toolkit.
011      * A Pair contains text (key) and optionally a list of pairs (values).
012      *
013      * The term 'value' is used to refer to the text of the first sub-pair.
014      *
015      */
016    public class Pair {
017    
018      public static Logger logger = Logger.getLogger(Pair.class);
019      private static Iterator emptyIt = new LinkedList().iterator();
020    
021      protected Pair parent;
022      protected String text;
023      protected PairList pairList;
024      protected int indent;
025      protected int srcLine;
026      protected int srcColumn;
027    
028      public Pair(Pair parent, String text) {
029        if(text == null)
030          throw new IllegalArgumentException("Null text not allowed");
031        this.parent = parent;
032        this.text = text;
033      }
034    
035      public Pair(Pair parent, int indent, String line, int srcLine, int srcColumn) {
036        this.parent = parent;
037        this.indent = indent;
038        this.srcLine = srcLine;
039        this.srcColumn = srcColumn;
040        if(line == null)
041          throw new IllegalArgumentException("Null text not allowed");
042        text = line;
043      }
044    
045      protected Pair() {
046      }
047    
048    
049      public int getSrcColumn() {
050        return srcColumn;
051      }
052    
053      public int getSrcLine() {
054        return srcLine;
055      }
056    
057      public int getIndent() {
058        return indent;
059      }
060    
061      public Pair getRoot() {
062        if(parent == null)
063          return this;
064        return parent.getRoot();
065      }
066    
067      public Pair getParent() {
068        return parent;
069      }
070    
071      public void setParent(Pair parent) {
072        this.parent = parent;
073      }
074    
075      public void setText(String text) {
076        this.text = text;
077      }
078    
079      public String getText() {
080        return text;
081      }
082    
083      /** @return the first sub-pair with the given text or null if none exists. */
084      public Pair get(String text) {
085        for(Iterator it = getPairs(); it.hasNext(); ) {
086          Pair pair = (Pair) it.next();
087          if(pair.getText().equals(text))
088            return pair;
089        }
090        return null;
091      }
092    
093      /** @return the index'th sub-pair if any.
094        * @throws ArrayIndexOutOfBoundsException if index exceeds size.
095        */
096      public Pair get(int index) {
097        return (Pair) pairList.get(index);
098      }
099    
100      /**
101        * @return an Iterator over the sub-pairs, potentially empty.
102        */
103      public Iterator getPairs() {
104        if(pairList == null)
105          return emptyIt;
106        return pairList.getPairs();
107      }
108    
109      /** Returns the text of the first sub-pair or "" if none exists. */
110      public String getValue() {
111        Iterator it = getPairs();
112        if(it.hasNext())
113          return ((Pair) it.next()).getText();
114        return "";
115      }
116    
117      /** Returns the concatenated texts of all sub-pairs or "" if non exist.
118        * Values are separated by a space.
119        */
120      public String getValues() {
121        StringBuffer result = new StringBuffer();
122        Iterator it = getPairs();
123        while(it.hasNext()) {
124          result.append(((Pair) it.next()).getText());
125          if(it.hasNext())
126            result.append(" ");
127        }
128        return result.toString();
129      }
130    
131      /** Returns the pairs matching the xpath. */
132      public Iterator select(String xpath) {
133        try {
134          return new PairXPath(xpath).selectNodes(this).iterator();
135        } catch(JaxenException e) {
136          throw new RuntimeException("Error evaluating xpath '" + xpath + "' on pair '" + this + "'.", e);
137        }
138      }
139    
140      /** Returns the 1st pair matching the xpath or null if none matches. */
141      public Pair select1(String xpath) {
142        Iterator it = select(xpath);
143        if(it.hasNext())
144          return (Pair) it.next();
145        return null;
146      }
147    
148      /** Returns the 'value' of the first pair maching the xpath or "" if none. */
149      public String eval1(String xpath) {
150        String result = "";
151        Iterator it = select(xpath);
152        if(it.hasNext())
153          result = ((Pair) it.next()).getValue();
154        logger.info("eval1(): " + xpath + "=" + result);
155        return result;
156      }
157    
158      /** Returns the concatenated texts of all sub-pairs of the matching xpaths
159        * or "" if none match.
160        */
161      public String eval(String xpath) {
162        StringBuffer result = new StringBuffer();
163        Iterator it = select(xpath);
164        while(it.hasNext())
165          result.append(((Pair) it.next()).getValues());
166        logger.info("eval(): " + xpath + "=" + result);
167        return result.toString();
168      }
169    
170      /** Insert the pair at position pos in the list of sub-pairs.
171        * @throws IndexOutOfBoundsException if no such position exists. I.e. if
172        * pos > size().
173        */
174      public void add(int pos, Pair pair) {
175        if(pairList == null)
176          pairList = new PairList();
177        pairList.addPair(pos, pair);
178        pair.setParent(this);
179      }
180    
181      /** Appends the pair to the list of sub-pairs. */
182      public void add(Pair pair) {
183        add(size(), pair);
184      }
185    
186      /** Removes all sub-pairs. Does not change the parent property of the
187        * removed pairs.
188        */
189      public void clear() {
190        pairList = null;
191      }
192    
193      /** Removes the pair from the list of sub-pairs. Does not change the parent property of the
194        * removed pair.
195        */
196      public void remove(Pair pair) {
197        pairList.remove(pair);
198        if(size() == 0)
199          clear();
200      }
201    
202      /** @return a deep copy of this pair. */
203      public Pair copy() {
204        Pair result = new Pair(null, text);
205        if(size() > 0)
206          for(int i = 0; i < size(); i++)
207            result.add(get(i).copy());
208        return result;
209      }
210    
211      /** @return the number of sub-pairs. */
212      public int size() {
213        if(pairList == null)
214          return 0;
215        return pairList.size();
216      }
217    
218      /** A String of the form <code>text[ sub-pair1 sub-pair2 ... ] where
219        * each sub-pair may also be composite.
220        */
221      public String toString() {
222        if(pairList != null)
223          return Escaper.saveConvert(text, true) + "[ " + pairList + " ]";
224        return Escaper.saveConvert(text, true);
225      }
226    
227      public int hashCode() {
228        if(pairList == null)
229          return text.hashCode();
230        return text.hashCode() + pairList.hashCode();
231      }
232    
233      /**
234        * Two pairs are equal if their texts are equals and their sub-pairs match.
235        */
236      public boolean equals(Object object) {
237        if(object == null || ! (object instanceof Pair))
238          return false;
239        Pair pair = (Pair) object;
240        if(! text.equals(pair.text))
241          return false;
242        if(pairList != null)
243          return pairList.equals(pair.pairList);
244        return true;
245      }
246    
247    }
248