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