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