001package ca.uhn.hl7v2.preparser;
002
003import java.util.*;
004
005/** An object of this class represents a variable-size path for identifying
006the location of a datum within an HL7 message, which we can use for
007maintaining parser state and for generating a suitable string key (in the
008ZYX[a]-b[c]-d-e style) for a piece of data in the message (see toString()).
009
010The elements are: 
011segmentID / segmentRepIdx / fieldIdx / fieldRepIdx / compIdx / subcompIdx 
012
013("rep" means "repetition")
014
015segmentID is a String, the rest are Integers.
016
017It is variable-size path-style in that if it has a size of 1, the one element
018will be the segmentID; if it has a size of two, element 0 will be the segmentID
019and element 1 will be the segmentRepIdx, etc.  This class can't represent a
020fieldIdx without having segmentID / segmentRepIdx, etc. etc. 
021
022possible sizes: 0 to 6 inclusive
023
024As toString() simply converts this's integer values to strings (1 => "1"), and
025since for some reason the ZYX[a]-b[c]-d-e style counts b, d, e starting from 1
026and a, c from 0 -- it is intended that one store the numeric values in this
027class starting from 1 for fieldIdx (element 2), compIdx (4) and subcompIdx
028(5), and from 0 for segmentRepIdx (1) and fieldRepIdx (3).  default values
029provided by setSize() and by toString() do this.
030*/
031public class DatumPath implements Cloneable {
032
033        public static final int s_maxSize = 6;
034
035        protected ArrayList<Object> m_path = null;
036
037        public DatumPath()
038        {
039                m_path = new ArrayList<Object>(s_maxSize);
040        }
041
042        /** copy constructor */
043        public DatumPath(DatumPath other)
044        {
045                this();
046
047                copy(other);
048        }
049
050        public boolean equals(Object otherObject)
051        {
052                boolean ret = false;
053                DatumPath other = (DatumPath)otherObject;
054                if(this.size() == other.size()) {
055                        ret = true;
056                        for(int i=0; i<this.size(); ++i)
057                                ret &= this.get(i).equals(other.get(i));
058                }
059
060                return ret;
061        }
062
063        /** Works like String.startsWith: 
064        returns true iff prefix.size() <= this.size()
065         AND if, for 0 <= i < prefix.size(), this.get(i).equals(prefix.get(i))
066        */
067        public boolean startsWith(DatumPath prefix)
068        {
069                boolean ret = false;
070                if(prefix.size() <= this.size()) {
071                        ret = true;
072                        for(int i=0; i<prefix.size(); ++i)
073                                ret &= this.get(i).equals(prefix.get(i));
074                }
075                return ret;
076        }
077
078        /** like a copy constructor without the constructor */
079        public void copy(DatumPath other)
080        {
081                setSize(0);
082                for(int i=0; i<other.size(); ++i)
083                        add(other.get(i));
084        }
085
086        /** set() sets an element of the path.  
087        
088        idx must be in [0, size()). else => IndexOutOfBoundsException. 
089        
090        (new_value == null) => NullPointerException  
091
092        new_value must be either a String or an Integer depending on what part 
093        of the path you're setting:  
094
095        (idx == 0) => String
096        (idx >= 1) => Integer
097
098        If new_value can't be cast to the appropriate type, a ClassCastException 
099        is thrown before new_value is stored.
100
101        Of course, on success, this will discard it's reference that used to be at
102        position idx.
103        */
104        public void set(int idx, Object new_value)
105        {
106                if((0 <= idx) && (idx < m_path.size())) {
107                        if(new_value != null) {
108                                if(idx == 0)
109                                        m_path.set(idx, (String)new_value);
110                                else if(idx >= 1)
111                                        m_path.set(idx, (Integer)new_value);
112                        }
113                        else
114                                throw new NullPointerException();
115                }
116                else
117                        throw new IndexOutOfBoundsException();
118        }
119
120        /** get() returns an element, which will be either a String or an Integer.
121
122        ((idx == 0) => String
123        (idx >= 1) => Integer
124        ((idx < 0) || (idx >= size())) => IndexOutOfBoundsException
125
126        We will attempt to cast the gotten object to the appropriate type before
127        returning it as an Object.  That way, if there's an object of the wrong type
128        in the wrong place in here (that got past set() somehow), then a
129        ClassCastException will be thrown even if the caller of this function
130        doesn't try to cast it.  (consider System.out.println("val: " + path.get(n))
131        nothing would barf it this get() wasn't vigilant.)
132        */
133        public Object get(int idx)
134        {
135                Object gottenObj = m_path.get(idx);
136                if(idx == 0)
137                        return (String)gottenObj;
138                else
139                        return (Object)gottenObj;
140        }
141
142        public int size() { return m_path.size(); }
143
144        /** toString() outputs the path (from segmentID onward) in the ZYX[a]-b[c]-d-e
145        style (TODO: give it a name), suitable for a key in a map of 
146        message datum paths to values. 
147        
148        Integer values are converted to strings directly (1 => "1") so when you
149        constructed this you should have started counting from 1 for everything but
150        the "repeat" fields, if you truly want the ZYX[a]-b[c]-d-e style.
151
152        If toString() is called when this has a size in [1, 6) (=> missing numeric
153        elments), then we act as though the elements in [size(), 6) are 0 or 1 as
154        appropriate for each element.  We don't provide a default for the element 0
155        (the String element): will throw an IndexOutOfBoundsException if (size() ==
156        1).
157
158        eg. a (new DatumPath()).add(new String("ZYX")).add(2).add(6).toString() 
159        would yield "ZYX[2]-6[0]-1-1"
160        */
161        public String toString()
162        {
163
164                StringBuffer strbuf = new StringBuffer(15);
165
166                if(m_path.size() >= 1) {
167                        DatumPath extendedCopy = (DatumPath)this.clone();
168                        extendedCopy.setSize(s_maxSize);
169
170                        for(int i=0; i<extendedCopy.size(); ++i) {
171                                if(i == 0)
172                                        strbuf.append("" + ((String)extendedCopy.get(0)));
173                                else if((i == 1) || (i == 3))
174                                        strbuf.append("[" + ((Integer)extendedCopy.get(i)).intValue() + "]");
175                                else if((i == 2) || (i == 4) || (i == 5))
176                                        strbuf.append("-" + (((Integer)extendedCopy.get(i)).intValue()));
177                        }
178                }
179                else 
180                        throw new IndexOutOfBoundsException();  
181
182                return "" + strbuf;
183        }
184
185        /** add() grows this by 1, inserting newValue at the end.
186        newValue must be a String or an Integer depending on the index where it will
187        be inserted, as noted at DatumPath.set().  
188        returns this.
189        (newValue == null) => NullPointerException 
190        */
191        public DatumPath add(Object newValue)
192        {
193//              m_path.ensureCapacity(m_path.size() + 1);
194//              set(m_path.size() - 1, newValue);
195                m_path.add(newValue);
196                return this;
197        }
198
199        /** Like add(String).  convenient wrapper for add(Object), when the object
200        to be added must be an Integer anyway (size() > 0 on entry).  
201
202        For the user, it turns 
203        path.add(new Integer(i)).add(new Integer(j)).add(new Integer(k)) 
204        into 
205        path.add(i).add(j).add(k), that's all.  
206
207        size() == 0 on entry throws a ClassCastException (which it is, kindof), 
208        otherwise calls add(new Integer(new_value)).
209        */
210        public DatumPath add(int new_value)
211        {
212                if(size() > 0)
213                        add(new Integer(new_value));
214                else 
215                        throw new ClassCastException();
216
217                return this;
218        }
219
220        /** convenience!  Like add(int), but the other way around. */
221        public DatumPath add(String new_value)
222        {
223                if(size() == 0) 
224                        add((Object)new_value);
225                else
226                        throw new ClassCastException();
227
228                return this;
229        }
230
231        /** setSize(): resize.  If this will grow the object, then we put default
232        values into the new elements: "" into the String element, Integer(1) into the
233        elements 2, 4, and 5, and Integer(0) into elements 1 and 3.
234        returns this.
235        */
236        public DatumPath setSize(int newSize)
237        {
238                int oldSize = m_path.size();
239                
240                while (m_path.size() < newSize) {
241                        m_path.add(null);
242                }
243                
244                while (m_path.size() > newSize) {
245                        m_path.remove(m_path.size() - 1);
246                }
247
248                if(newSize > oldSize) {
249                        // give the new elements some default values: 
250                        for(int i=oldSize; i<newSize; ++i) {
251                                if(i == 0)
252                                        set(i, "");
253                                else
254                                        set(i, new Integer((i==1 || i==3) ? 0 : 1));
255                        }
256                }
257
258                return this;
259        }
260
261        /** setSize(0).  returns this. */
262        public DatumPath clear() 
263        {
264                setSize(0);
265                return this;
266        }
267
268        public Object clone()
269        {
270                return new DatumPath(this);
271        }
272
273        /* Compare the numeric parts of "this" and "other".  string-style, start from
274        the left: if this[1] < other[1], then return true, if this[1] > other[1] then
275        return false, else repeat with [2] ... if we compare all elements, then return
276        false (they're the same.)
277
278        What are actually compared are copies of this and other that have been grown
279        to s_maxSize (default values in effect), so they'll have the same size.
280        
281        This is just a little thing that gets used in the class XML.  Look there for 
282        a justification of it's existence.
283
284        ex. [1, 1, 1, 1] < [1, 1, 1, 2] 
285        [1, 2, 1, 1] < [1, 2, 1, 2]
286        [1, 1, 5, 5] < [1, 2]
287        [1, 1] < [1, 1, 5, 5] 
288        */
289        public boolean numbersLessThan(DatumPath other)
290        {
291                DatumPath extendedCopyThis = new DatumPath(this);
292                extendedCopyThis.setSize(s_maxSize);
293
294                DatumPath extendedCopyOther = new DatumPath(other);
295                extendedCopyOther.setSize(s_maxSize);
296
297                boolean lessThan = false;
298                for(int i=1; !lessThan && (i<s_maxSize); ++i) {
299                        int this_i = ((Integer)extendedCopyThis.get(i)).intValue();
300                        int other_i = ((Integer)extendedCopyOther.get(i)).intValue();
301                        lessThan |= (this_i < other_i);
302                }
303
304                return lessThan;
305        }
306
307        public static void main(String args[])
308        {
309                DatumPath dp = new DatumPath();
310                dp.add(new String("ZYX"));
311                dp.add(new Integer(42));
312
313                DatumPath dp2 = (new DatumPath()).add(new String()).add(-42);
314
315                System.out.println(dp);
316                System.out.println(dp2);
317        }
318}
319