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;
038
039        public DatumPath()
040        {
041                m_path = new ArrayList<>(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                                m_path.set(idx, new_value);
105                        }
106                        else
107                                throw new NullPointerException();
108                }
109                else
110                        throw new IndexOutOfBoundsException();
111        }
112
113        /** get() returns an element, which will be either a String or an Integer.
114
115        ((idx == 0) => String
116        (idx >= 1) => Integer
117        ((idx < 0) || (idx >= size())) => IndexOutOfBoundsException
118
119        We will attempt to cast the gotten object to the appropriate type before
120        returning it as an Object.  That way, if there's an object of the wrong type
121        in the wrong place in here (that got past set() somehow), then a
122        ClassCastException will be thrown even if the caller of this function
123        doesn't try to cast it.  (consider System.out.println("val: " + path.get(n))
124        nothing would barf it this get() wasn't vigilant.)
125        */
126        public Object get(int idx)
127        {
128        return m_path.get(idx);
129        }
130
131        public int size() { return m_path.size(); }
132
133        /** toString() outputs the path (from segmentID onward) in the ZYX[a]-b[c]-d-e
134        style (TODO: give it a name), suitable for a key in a map of 
135        message datum paths to values. 
136        
137        Integer values are converted to strings directly (1 => "1") so when you
138        constructed this you should have started counting from 1 for everything but
139        the "repeat" fields, if you truly want the ZYX[a]-b[c]-d-e style.
140
141        If toString() is called when this has a size in [1, 6) (=> missing numeric
142        elments), then we act as though the elements in [size(), 6) are 0 or 1 as
143        appropriate for each element.  We don't provide a default for the element 0
144        (the String element): will throw an IndexOutOfBoundsException if (size() ==
145        1).
146
147        eg. a (new DatumPath()).add(new String("ZYX")).add(2).add(6).toString() 
148        would yield "ZYX[2]-6[0]-1-1"
149        */
150        public String toString()
151        {
152
153                StringBuilder strbuf = new StringBuilder();
154
155                if(m_path.size() >= 1) {
156                        DatumPath extendedCopy = (DatumPath)this.clone();
157                        extendedCopy.setSize(s_maxSize);
158
159                        for(int i=0; i<extendedCopy.size(); ++i) {
160                                if(i == 0)
161                                        strbuf.append(extendedCopy.get(0));
162                                else if((i == 1) || (i == 3))
163                                        strbuf.append("[").append(extendedCopy.get(i)).append("]");
164                                else if((i == 2) || (i == 4) || (i == 5))
165                                        strbuf.append("-").append(extendedCopy.get(i));
166                        }
167                }
168                else 
169                        throw new IndexOutOfBoundsException();  
170
171                return strbuf.toString();
172        }
173
174        /** add() grows this by 1, inserting newValue at the end.
175        newValue must be a String or an Integer depending on the index where it will
176        be inserted, as noted at DatumPath.set().  
177        returns this.
178        (newValue == null) => NullPointerException 
179        */
180        public DatumPath add(Object newValue)
181        {
182//              m_path.ensureCapacity(m_path.size() + 1);
183//              set(m_path.size() - 1, newValue);
184                m_path.add(newValue);
185                return this;
186        }
187
188        /** Like add(String).  convenient wrapper for add(Object), when the object
189        to be added must be an Integer anyway (size() > 0 on entry).  
190
191        For the user, it turns 
192        path.add(new Integer(i)).add(new Integer(j)).add(new Integer(k)) 
193        into 
194        path.add(i).add(j).add(k), that's all.  
195
196        size() == 0 on entry throws a ClassCastException (which it is, kindof), 
197        otherwise calls add(new Integer(new_value)).
198        */
199        public DatumPath add(int new_value)
200        {
201                if(size() > 0)
202                        add(Integer.valueOf(new_value));
203                else 
204                        throw new ClassCastException();
205
206                return this;
207        }
208
209        /** convenience!  Like add(int), but the other way around. */
210        public DatumPath add(String new_value)
211        {
212                if(size() == 0) 
213                        add((Object)new_value);
214                else
215                        throw new ClassCastException();
216
217                return this;
218        }
219
220        /** setSize(): resize.  If this will grow the object, then we put default
221        values into the new elements: "" into the String element, Integer(1) into the
222        elements 2, 4, and 5, and Integer(0) into elements 1 and 3.
223        returns this.
224        */
225        public DatumPath setSize(int newSize)
226        {
227                int oldSize = m_path.size();
228                
229                while (m_path.size() < newSize) {
230                        m_path.add(null);
231                }
232                
233                while (m_path.size() > newSize) {
234                        m_path.remove(m_path.size() - 1);
235                }
236
237                if(newSize > oldSize) {
238                        // give the new elements some default values: 
239                        for(int i=oldSize; i<newSize; ++i) {
240                                if(i == 0)
241                                        set(i, "");
242                                else
243                                        set(i, (i==1 || i==3) ? 0 : 1);
244                        }
245                }
246
247                return this;
248        }
249
250        /** setSize(0).  returns this. */
251        public DatumPath clear() 
252        {
253                setSize(0);
254                return this;
255        }
256
257        public Object clone()
258        {
259                return new DatumPath(this);
260        }
261
262        /* Compare the numeric parts of "this" and "other".  string-style, start from
263        the left: if this[1] < other[1], then return true, if this[1] > other[1] then
264        return false, else repeat with [2] ... if we compare all elements, then return
265        false (they're the same.)
266
267        What are actually compared are copies of this and other that have been grown
268        to s_maxSize (default values in effect), so they'll have the same size.
269        
270        This is just a little thing that gets used in the class XML.  Look there for 
271        a justification of it's existence.
272
273        ex. [1, 1, 1, 1] < [1, 1, 1, 2] 
274        [1, 2, 1, 1] < [1, 2, 1, 2]
275        [1, 1, 5, 5] < [1, 2]
276        [1, 1] < [1, 1, 5, 5] 
277        */
278        public boolean numbersLessThan(DatumPath other)
279        {
280                DatumPath extendedCopyThis = new DatumPath(this);
281                extendedCopyThis.setSize(s_maxSize);
282
283                DatumPath extendedCopyOther = new DatumPath(other);
284                extendedCopyOther.setSize(s_maxSize);
285
286                boolean lessThan = false;
287                for(int i=1; !lessThan && (i<s_maxSize); ++i) {
288                        int this_i = ((Integer)extendedCopyThis.get(i));
289                        int other_i = ((Integer)extendedCopyOther.get(i));
290                        lessThan = (this_i < other_i);
291                }
292
293                return lessThan;
294        }
295
296        public static void main(String[] args)
297        {
298                DatumPath dp = new DatumPath();
299                dp.add("ZYX");
300                dp.add(Integer.valueOf(42));
301
302                DatumPath dp2 = new DatumPath().add(-42);
303
304                System.out.println(dp);
305                System.out.println(dp2);
306        }
307}
308