001    package ca.uhn.hl7v2.util;
002    
003    import ca.uhn.hl7v2.model.*;
004    import ca.uhn.log.*;
005    import ca.uhn.hl7v2.HL7Exception;
006    import java.util.*;
007    
008    /**
009     * Iterates over all defined nodes (ie segments, groups) in a message, 
010     * regardless of whether they have been instantiated previously.  This is a 
011     * tricky process, because the number of nodes is infinite, due to infinitely 
012     * repeating segments and groups.  See <code>next()</code> for details on 
013     * how this is handled. 
014     * 
015     * This implementation assumes that the first segment in each group is present (as per
016     * HL7 rules).  Specifically, when looking for a segment location, an empty group that has 
017     * a spot for the segment will be overlooked if there is anything else before that spot. 
018     * This may result in surprising (but sensible) behaviour if a message is missing the 
019     * first segment in a group. 
020     *  
021     * @author Bryan Tripp
022     */
023    public class MessageIterator implements java.util.Iterator {
024    
025        private Structure currentStructure; 
026        private String direction;
027        private Position next;
028        private boolean handleUnexpectedSegments;
029        
030        private static final HapiLog log = HapiLogFactory.getHapiLog(MessageIterator.class);
031        
032        /* may add configurability later ... 
033        private boolean findUpToFirstRequired;
034        private boolean findFirstDescendentsOnly;
035        
036        public static final String WHOLE_GROUP;
037        public static final String FIRST_DESCENDENTS_ONLY;
038        public static final String UP_TO_FIRST_REQUIRED;
039        */
040         
041        /** Creates a new instance of MessageIterator */
042        public MessageIterator(Structure start, String direction, boolean handleUnexpectedSegments) {
043            this.currentStructure = start;
044            this.direction = direction;
045            this.handleUnexpectedSegments = handleUnexpectedSegments;
046        }
047        
048        /* for configurability (maybe to add later, replacing hard-coded options
049          in nextFromEndOfGroup) ... 
050        public void setSearchLevel(String level) {
051            if (WHOLE_GROUP.equals(level)) {
052                this.findUpToFirstRequired = false;
053                this.findFirstDescendentsOnly = false;
054            } else if (FIRST_DESCENDENTS_ONLY.equals(level)) {
055                this.findUpToFirstRequired = false;
056                this.findFirstDescendentsOnly = true;
057            } else if (UP_TO_FIRST_REQUIRED.equals(level)) {
058                this.findUpToFirstRequired = true;
059                this.findFirstDescendentsOnly = false;
060            } else {
061                throw IllegalArgumentException(level + " is not a valid search level.  Should be WHOLE_GROUP, etc.");
062            }     
063        }
064        
065        public String getSearchLevel() {
066            String level = WHOLE_GROUP;
067            if (this.findFirstDescendentsOnly) {
068                level = FIRST_DESCENDENTS_ONLY;
069            } else if (this.findUpTpFirstRequired) {
070                level = UP_TO_FIRST_REQUIRED;
071            }
072            return level;
073        }*/
074         
075        
076        /**
077         * Returns true if another object exists in the iteration sequence.  
078         */
079        public boolean hasNext() {
080            boolean has = true;
081            if (next == null) {
082                if (Group.class.isAssignableFrom(currentStructure.getClass())) {
083                    groupNext((Group) currentStructure);
084                } else {
085                    Group parent = currentStructure.getParent();
086                    Index i = getIndex(parent, currentStructure);
087                    Position currentPosition = new Position(parent, i);
088                    
089                    try {                    
090                        if (parent.isRepeating(i.name) && currentStructure.getName().equals(direction)) {
091                            nextRep(currentPosition);
092                        } else {
093                            has = nextPosition(currentPosition, this.direction, this.handleUnexpectedSegments);
094                        }
095                    } catch (HL7Exception e) {
096                        throw new Error("HL7Exception arising from bad index: " + e.getMessage());
097                    }
098                }
099            }
100            log.debug("MessageIterator.hasNext() in direction " + this.direction + "? " + has);
101            return has;
102        }
103        
104        /**
105         * Sets next to the first child of the given group (iteration 
106         * always proceeds from group to first child). 
107         */
108        private void groupNext(Group current) {
109            next = new Position(current, ((Group) current).getNames()[0], 0);
110        }
111        
112        /**
113         * Sets next to the next repetition of the current structure.  
114         */ 
115        private void nextRep(Position current) {        
116            next = new Position(current.parent, current.index.name, current.index.rep + 1);
117        }
118        
119        /**
120         * Sets this.next to the next position in the message (from the given position), 
121         * which could be the next sibling, a new segment, or the next rep 
122         * of the parent.  See next() for details. 
123         */
124        private boolean nextPosition(Position currPos, String direction, boolean makeNewSegmentIfNeeded) throws HL7Exception {
125            boolean nextExists = true;
126            if (isLast(currPos)) {
127                nextExists = nextFromGroupEnd(currPos, direction, makeNewSegmentIfNeeded);
128            } else {
129                nextSibling(currPos);
130            }
131            return nextExists;
132        }
133        
134        /** Navigates from end of group */
135        private boolean nextFromGroupEnd(Position currPos, String direction, boolean makeNewSegmentIfNeeded) throws HL7Exception {
136            assert isLast(currPos);
137            boolean nextExists = true;
138            
139            //the following conditional logic is a little convoluted -- its meant as an optimization 
140            // i.e. trying to avoid calling matchExistsAfterCurrentPosition
141            
142            if (!makeNewSegmentIfNeeded && Message.class.isAssignableFrom(currPos.parent.getClass())) {
143                nextExists = false;
144            } else if (!makeNewSegmentIfNeeded || matchExistsAfterPosition(currPos, direction, false, true)) {     
145                Group grandparent = currPos.parent.getParent();
146                Index parentIndex = getIndex(grandparent, currPos.parent);
147                Position parentPos = new Position(grandparent, parentIndex);
148                
149                try {
150                    boolean parentRepeats = parentPos.parent.isRepeating(parentPos.index.name);                
151                    if (parentRepeats && contains(parentPos.parent.get(parentPos.index.name, 0), direction, false, true)) {
152                        nextRep(parentPos);
153                    } else {
154                        nextExists = nextPosition(parentPos, direction, makeNewSegmentIfNeeded);
155                    }
156                } catch (HL7Exception e) {
157                    throw new Error("HL7Exception arising from bad index: " + e.getMessage());
158                }
159            } else {
160                newSegment(currPos.parent, direction);
161            }
162            return nextExists;
163        }
164        
165        /** 
166         * A match exists for the given name somewhere after the given position (in the 
167         * normal serialization order).  
168         * @param pos the message position after which to look (note that this specifies 
169         *      the message instance)
170         * @param name the name of the structure to look for
171         * @param firstDescendentsOnly only searches the first children of a group 
172         * @param upToFirstRequired only searches the children of a group up to the first 
173         *      required child (normally the first one).  This is used when we are parsing 
174         *      a message in order and looking for a place to parse a particular segment -- 
175         *      if the message is correct then it can't go after a required position of a 
176         *      different name. 
177         */
178        public static boolean matchExistsAfterPosition(Position pos, String name, boolean firstDescendentsOnly, boolean upToFirstRequired) throws HL7Exception {
179            boolean matchExists = false;
180            
181            //check next rep of self (if any)
182            if (pos.parent.isRepeating(pos.index.name)) {            
183                Structure s = pos.parent.get(pos.index.name, pos.index.rep);
184                matchExists = contains(s, name, firstDescendentsOnly, upToFirstRequired);
185            } 
186            
187            //check later siblings (if any) 
188            if (!matchExists) {
189                String[] siblings = pos.parent.getNames();
190                boolean after = false;
191                for (int i = 0; i < siblings.length && !matchExists; i++) {
192                    if (after) {
193                        matchExists = contains(pos.parent.get(siblings[i]), name, firstDescendentsOnly, upToFirstRequired);
194                        if (upToFirstRequired && pos.parent.isRequired(siblings[i])) break; 
195                    }
196                    if (pos.index.name.equals(siblings[i])) after = true;                
197                } 
198            }
199            
200            //recurse to parent (if parent is not message root)
201            if (!matchExists && !Message.class.isAssignableFrom(pos.parent.getClass())) {
202                Group grandparent = pos.parent.getParent();
203                Position parentPos = new Position(grandparent, getIndex(grandparent, pos.parent));
204                matchExists = matchExistsAfterPosition(parentPos, name, firstDescendentsOnly, upToFirstRequired);
205            }
206            log.debug("Match exists after position " + pos + " for " + name + "? " + matchExists);
207            return matchExists;
208        }
209        
210        /** 
211         * Sets the next position to a new segment of the given name, within the 
212         * given group. 
213         */
214        private void newSegment(Group parent, String name) throws HL7Exception {
215            log.info("MessageIterator creating new segment: " + name);
216            parent.addNonstandardSegment(name);
217            next = new Position(parent, parent.getNames()[parent.getNames().length-1], 0);
218        }
219        
220        /** 
221         * Determines whether the given structure matches the given name, or contains 
222         * a child that does.  
223         * @param s the structure to check 
224         * @param name the name to look for 
225         * @param firstDescendentsOnly only checks first descendents (i.e. first 
226         *      child, first child of first child, etc.)  In theory the first child 
227         *      of a group should always be present, and we don't use this method with 
228         *      subsequent children because finding the next position within a group is 
229         *      straightforward.  
230         * @param upToFirstRequired only checks first descendents and of their siblings 
231         *      up to the first required one.  This may be needed because in practice 
232         *      some first children of groups are not required.  
233         */
234        public static boolean contains(Structure s, String name, boolean firstDescendentsOnly, boolean upToFirstRequired) {
235            boolean contains = false;
236            if (Segment.class.isAssignableFrom(s.getClass())) {
237                if (s.getName().equals(name)) contains = true;            
238            } else {
239                Group g = (Group) s;
240                String[] names = g.getNames();
241                for (int i = 0; i < names.length && !contains; i++) {
242                    try {
243                        contains = contains(g.get(names[i], 0), name, firstDescendentsOnly, upToFirstRequired);                
244                        if (firstDescendentsOnly) break;
245                        if (upToFirstRequired && g.isRequired(names[i])) break; 
246                    } catch (HL7Exception e) {
247                        throw new Error("HL7Exception due to bad index: " + e.getMessage());
248                    }
249                }
250            }
251            return contains;
252        }
253        
254        /**
255         * Tests whether the name of the given Index matches 
256         * the name of the last child of the given group. 
257         */
258        public static boolean isLast(Position p) {
259            String[] names = p.parent.getNames();
260            return names[names.length-1].equals(p.index.name);
261        }
262        
263        /**
264         * Sets the next location to the next sibling of the given 
265         * index.  
266         */
267        private void nextSibling(Position pos) {
268            String[] names = pos.parent.getNames();
269            int i = 0;
270            for (; i < names.length && !names[i].equals(pos.index.name); i++) {}
271            String nextName = names[i+1];
272            
273            this.next = new Position(pos.parent, nextName, 0);
274        }
275        
276        /**
277         * <p>Returns the next node in the message.  Sometimes the next node is 
278         * ambiguous.  For example at the end of a repeating group, the next node 
279         * may be the first segment in the next repetition of the group, or the 
280         * next sibling, or an undeclared segment locally added to the group's end.  
281         * Cases like this are disambiguated using getDirection(), which returns  
282         * the name of the structure that we are "iterating towards".  
283         * Usually we are "iterating towards" a segment of a certain name because we 
284         * have a segment string that we would like to parse into that node. 
285         * Here are the rules: </p>
286         * <ol><li>If at a group, next means first child.</li>
287         * <li>If at a non-repeating segment, next means next "position"</li>
288         * <li>If at a repeating segment: if segment name matches 
289         * direction then next means next rep, otherwise next means next "position".</li>
290         * <li>If at a segment within a group (not at the end of the group), next "position" 
291         * means next sibling</li>
292         * <li>If at the end of a group: If name of group or any of its "first 
293         * decendents" matches direction, then next position means next rep of group.  Otherwise 
294         * if direction matches name of next sibling of the group, or any of its first 
295         * descendents, next position means next sibling of the group.  Otherwise, next means a 
296         * new segment added to the group (with a name that matches "direction").  </li>
297         * <li>"First descendents" means first child, or first child of the first child, 
298         * or first child of the first child of the first child, etc. </li> </ol>
299         */
300        public Object next() {
301            if (!hasNext()) {
302                throw new NoSuchElementException("No more nodes in message");
303            }
304            try {
305                this.currentStructure = next.parent.get(next.index.name, next.index.rep);
306            } catch (HL7Exception e) {
307                throw new NoSuchElementException("HL7Exception: " + e.getMessage());
308            }
309            clearNext();
310            return this.currentStructure;
311        }
312        
313        /** Not supported */
314        public void remove() {
315            throw new UnsupportedOperationException("Can't remove a node from a message");
316        }
317        
318        public String getDirection() {
319            return this.direction;
320        }
321        
322        public void setDirection(String direction) {
323            clearNext();
324            this.direction = direction;
325        }
326        
327        private void clearNext() {
328            next = null;
329        }
330        
331        /**
332         * Returns the index of the given structure as a child of the 
333         * given parent.  Returns null if the child isn't found. 
334         */
335        public static Index getIndex(Group parent, Structure child) {
336            Index index = null;
337            String[] names = parent.getNames();
338            findChild : for (int i = 0; i < names.length; i++) {
339                if (names[i].startsWith(child.getName())) {
340                    try {
341                        Structure[] reps = parent.getAll(names[i]);
342                        for (int j = 0; j < reps.length; j++) {
343                            if (child == reps[j]) {
344                                index = new Index(names[i], j);
345                                break findChild; 
346                            }
347                        }
348                    } catch (HL7Exception e) {
349                        log.error("", e);
350                        throw new Error("Internal HL7Exception finding structure index: " + e.getMessage());
351                    }
352                }
353            }
354            return index;
355        }
356        
357        /** 
358         * An index of a child structure within a group, consisting of the name and rep of 
359         * of the child.
360         */
361        public static class Index {
362            public String name;
363            public int rep;
364            public Index(String name, int rep) {
365                this.name = name;
366                this.rep = rep;
367            }
368            
369            /** @see Object#equals */
370            public boolean equals(Object o) {
371                boolean equals = false;
372                if (o != null && o instanceof Index) {
373                    Index i = (Index) o;
374                    if (i.rep == rep && i.name.equals(name)) equals = true;
375                }
376                return equals;
377            }
378            
379            /** @see Object#hashCode */
380            public int hashCode() {
381                return name.hashCode() + 700 * rep;
382            }
383            
384            /** @see Object#toString */        
385            public String toString() {
386                return this.name + ":" + this.rep;
387            }
388        }
389        
390        /**
391         * A structure position within a message. 
392         */
393        public static class Position {
394            public Group parent;
395            public Index index;
396            public Position(Group parent, String name, int rep) {
397                this.parent = parent;
398                this.index = new Index(name, rep);
399            }
400            public Position(Group parent, Index i) {
401                this.parent = parent;
402                this.index = i;
403            }
404    
405            /** @see Object#equals */
406            public boolean equals(Object o) {
407                boolean equals = false;
408                if (o != null && o instanceof Position) {
409                    Position p = (Position) o;
410                    if (p.parent.equals(parent) && p.index.equals(index)) equals = true;
411                }
412                return equals;
413            }
414            
415            /** @see Object#hashCode */
416            public int hashCode() {
417                return parent.hashCode() + index.hashCode();
418            }
419            
420            public String toString() {
421                StringBuffer ret = new StringBuffer(parent.getName());
422                ret.append(":");
423                ret.append(index.name);
424                ret.append("(");
425                ret.append(index.rep);
426                ret.append(")");
427                return ret.toString();           
428            }
429        }
430    }