001/**
002 * The contents of this file are subject to the Mozilla Public License Version 1.1
003 * (the "License"); you may not use this file except in compliance with the License.
004 * You may obtain a copy of the License at http://www.mozilla.org/MPL/
005 * Software distributed under the License is distributed on an "AS IS" basis,
006 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007 * specific language governing rights and limitations under the License.
008 *
009 * The Original Code is "MessageNaviagtor.java".  Description:
010 * "Used to navigate the nested group structure of a message."
011 *
012 * The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 * 2002.  All Rights Reserved.
014 *
015 * Contributor(s): ______________________________________.
016 *
017 * Alternatively, the contents of this file may be used under the terms of the
018 * GNU General Public License (the "GPL"), in which case the provisions of the GPL are
019 * applicable instead of those above.  If you wish to allow use of your version of this
020 * file only under the terms of the GPL and not to allow others to use your version
021 * of this file under the MPL, indicate your decision by deleting  the provisions above
022 * and replace  them with the notice and other provisions required by the GPL License.
023 * If you do not delete the provisions above, a recipient may use your version of
024 * this file under either the MPL or the GPL.
025 *
026 */
027
028package ca.uhn.hl7v2.util;
029
030import java.util.*;
031import ca.uhn.hl7v2.model.*;
032import ca.uhn.hl7v2.HL7Exception;
033
034/**
035 * <p>Used to navigate the nested group structure of a message.  This is an alternate
036 * way of accessing parts of a message, ie rather than getting a segment through
037 * a chain of getXXX() calls on the message, you can create a MessageNavigator
038 * for the message, "navigate" to the desired segment, and then call
039 * getCurrentStructure() to get the segment you have navigated to.  A message
040 * navigator always has a "current location" pointing to some structure location (segment
041 * or group location) within the message.  Note that a location exists whether or
042 * not there are any instances of the structure at that location. </p>
043 * <p>This class is used by Terser, which presents an even more convenient way
044 * of navigating a message.  </p>
045 * <p>This class also has an iterate() method, which iterates over
046 * segments (and optionally groups).  </p>
047 * @author Bryan Tripp
048 */
049public class MessageNavigator {
050    
051    private Group root;
052    private Stack<GroupContext> ancestors;
053    private int currentChild; // -1 means current structure is current group (special case used for root)
054    private Group currentGroup;
055    private String[] childNames;
056    
057    /**
058     * Creates a new instance of MessageNavigator
059     * @param root the root of navigation -- may be a message or a group
060     *      within a message.  Navigation will only occur within the subtree
061     *      of which the given group is the root.
062     */
063    public MessageNavigator(Group root) {
064        this.root = root;
065        reset();
066    }
067    
068    public Group getRoot() {
069        return this.root;
070    }
071       
072    /**
073     * Drills down into the group at the given index within the current
074     * group -- ie sets the location pointer to the first structure within the child
075     * @param childNumber the index of the group child into which to drill
076     * @param rep the group repetition into which to drill
077     */
078    public void drillDown(int childNumber, int rep) throws HL7Exception {
079        if (childNumber != -1) {
080            Structure s = currentGroup.get(childNames[childNumber], rep);
081            if (!(s instanceof Group)) {
082                throw new HL7Exception("Can't drill into segment", HL7Exception.APPLICATION_INTERNAL_ERROR);
083            }
084            Group group = (Group) s;
085            
086            //stack the current group and location
087            GroupContext gc = new GroupContext(this.currentGroup, this.currentChild);
088            this.ancestors.push(gc);
089            
090            this.currentGroup = group;
091        }
092        
093        this.currentChild = 0;
094        this.childNames = this.currentGroup.getNames();
095    }
096    
097    /**
098     * Drills down into the group at the CURRENT location.
099     */
100    public void drillDown(int rep) throws HL7Exception {
101        drillDown(this.currentChild, rep);
102    }
103    
104    /**
105     * Switches the group context to the parent of the current group,
106     * and sets the child pointer to the next sibling.
107     * @return false if already at root
108     */
109    public boolean drillUp() {
110        //pop the top group and resume search there
111        if (!this.ancestors.empty()) {
112            GroupContext gc = (GroupContext) this.ancestors.pop();
113            this.currentGroup = gc.group;
114            this.currentChild = gc.child;
115            this.childNames = this.currentGroup.getNames();
116            return true;
117        } else {
118            if (this.currentChild == -1) {
119                return false;
120            } else {
121                this.currentChild = -1;
122                return true;
123            }
124        }
125    }
126    
127    /**
128     * Returns true if there is a sibling following the current location.
129     */
130    public boolean hasNextChild() {
131        if (this.childNames.length > this.currentChild + 1) {
132            return true;
133        } else {
134            return false;
135        }
136    }
137    
138    /**
139     * Moves to the next sibling of the current location.
140     */
141    public void nextChild() throws HL7Exception {
142        int child = this.currentChild + 1;
143        toChild(child);
144    }
145    
146    /**
147     * Moves to the sibling of the current location at the specified index.
148     */
149    public void toChild(int child) throws HL7Exception {
150        if (child >= 0 && child < this.childNames.length) {
151            this.currentChild = child;
152        } else {
153            throw new HL7Exception("Can't advance to child " + child + " -- only " + this.childNames.length + " children",
154            HL7Exception.APPLICATION_INTERNAL_ERROR);
155        }
156    }
157    
158    /** Resets the location to the beginning of the tree (the root) */
159    public void reset() {
160        this.ancestors = new Stack<GroupContext>();
161        this.currentGroup = root;
162        this.currentChild = -1;
163        this.childNames = currentGroup.getNames();
164    }
165    
166    /**
167     * Returns the given rep of the structure at the current location.  
168     * If at root, always returns the root (the rep is ignored).  
169     */
170    public Structure getCurrentStructure(int rep) throws HL7Exception {
171        Structure ret = null;
172        if (this.currentChild != -1) {
173            String childName = this.childNames[this.currentChild];
174            ret = this.currentGroup.get(childName, rep);
175        } else { 
176            ret = this.currentGroup;
177        }
178        return ret;
179    }
180    
181    /** 
182     * Returns the group within which the pointer is currently located. 
183     * If at the root, the root is returned.  
184     */
185    public Group getCurrentGroup() {
186        return this.currentGroup;
187    }
188    
189    /**
190     * Returns the array of structures at the current location.  
191     * Throws an exception if pointer is at root.  
192     */
193    public Structure[] getCurrentChildReps() throws HL7Exception {
194        if (this.currentGroup == this.root && this.currentChild == -1) 
195            throw new HL7Exception("Pointer is at root of navigator: there is no current child");
196        
197        String childName = this.childNames[this.currentChild];
198        return this.currentGroup.getAll(childName);
199    }
200    
201    /**
202     * Iterates through the message tree to the next segment/group location (regardless
203     * of whether an instance of the segment exists).  If the end of the tree is
204     * reached, starts over at the root.  Only enters the first repetition of a
205     * repeating group -- explicit navigation (using the drill...() methods) is
206     * necessary to get to subsequent reps.
207     * @param segmentsOnly if true, only stops at segments (not groups)
208     * @param loop if true, loops back to beginning when end of msg reached; if false,
209     *      throws HL7Exception if end of msg reached
210     */
211    public void iterate(boolean segmentsOnly, boolean loop) throws HL7Exception { 
212        Structure start = null;
213        
214        if (this.currentChild == -1) {
215            start = this.currentGroup; 
216        } else {
217            start = (this.currentGroup.get(this.childNames[this.currentChild]));
218        }
219        
220        //using a non-existent direction and not allowing segment creation means that only
221        //the first rep of anything is traversed.
222        Iterator<Structure> it = new MessageIterator(start, "doesn't exist", false);
223        if (segmentsOnly) {
224            FilterIterator.Predicate<Structure> predicate = new FilterIterator.Predicate<Structure>() {
225                public boolean evaluate(Structure obj) {
226                    if (Segment.class.isAssignableFrom(obj.getClass())) {
227                        return true;
228                    } else {
229                        return false;
230                    }
231                }
232            };
233            it = new FilterIterator<Structure>(it, predicate);
234        }
235        
236        if (it.hasNext()) {
237            Structure next = it.next();
238            drillHere(next);
239        } else if (loop) {
240            this.reset();
241        } else {
242            throw new HL7Exception("End of message reached while iterating without loop", 
243                HL7Exception.APPLICATION_INTERNAL_ERROR);
244        }
245            
246    }
247    
248    /**
249     * Navigates to a specific location in the message
250     */
251    private void drillHere(Structure destination) throws HL7Exception {
252        Structure pathElem = destination;
253        Stack<Structure> pathStack = new Stack<Structure>();
254        Stack<MessageIterator.Index> indexStack = new Stack<MessageIterator.Index>();
255        do {
256            MessageIterator.Index index = MessageIterator.getIndex(pathElem.getParent(), pathElem);
257            indexStack.push(index);
258            pathElem = pathElem.getParent();
259            pathStack.push(pathElem);
260        } while (!root.equals(pathElem) && !Message.class.isAssignableFrom(pathElem.getClass()));
261        
262        if (!root.equals(pathElem)) {
263            throw new HL7Exception("The destination provided is not under the root of this navigator");
264        }
265        
266        this.reset();
267        while (!pathStack.isEmpty()) {
268            Group parent = (Group) pathStack.pop();
269            MessageIterator.Index index = indexStack.pop();
270            int child = search(parent.getNames(), index.name);
271            if (!pathStack.isEmpty()) {
272                this.drillDown(child, 0);
273            } else {
274                this.toChild(child);
275            }
276        }
277    }
278    
279    /** Like Arrays.binarySearch, only probably slower and doesn't require
280     * a sorted list.  Also just returns -1 if item isn't found. */
281    private int search(Object[] list, Object item) {
282        int found = -1;
283        for (int i = 0; i < list.length && found == -1; i++) {
284            if (list[i].equals(item)) found = i;
285        }
286        return found;
287    }
288    
289    /**
290     * Drills down recursively until a segment is reached.
291     */
292    private void findLeaf() throws HL7Exception {
293        if (this.currentChild == -1)
294            this.currentChild = 0;
295        
296        Class c = this.currentGroup.getClass(this.childNames[this.currentChild]);
297        if (Group.class.isAssignableFrom(c)) {
298            drillDown(this.currentChild, 0);
299            findLeaf();
300        }
301    }
302    
303    /**
304     * A structure to hold current location information at
305     * one level of the message tree.  A stack of these
306     * identifies the current location completely.
307     */
308    private class GroupContext {
309        public Group group;
310        public int child;
311        
312        public GroupContext(Group g, int c) {
313            group = g;
314            child = c;
315        }
316    }
317    
318}