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 }