001package ca.uhn.hl7v2.util;
002
003import java.util.NoSuchElementException;
004
005import org.slf4j.Logger;
006import org.slf4j.LoggerFactory;
007
008import ca.uhn.hl7v2.HL7Exception;
009import ca.uhn.hl7v2.model.Group;
010import ca.uhn.hl7v2.model.Message;
011import ca.uhn.hl7v2.model.Segment;
012import 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 */
029public 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, 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.equals(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}