package ca.uhn.hl7v2.util; import ca.uhn.hl7v2.model.*; import ca.uhn.log.*; import ca.uhn.hl7v2.HL7Exception; import java.util.*; /** * Iterates over all defined nodes (ie segments, groups) in a message, * regardless of whether they have been instantiated previously. This is a * tricky process, because the number of nodes is infinite, due to infinitely * repeating segments and groups. See next() for details on * how this is handled. * * This implementation assumes that the first segment in each group is present (as per * HL7 rules). Specifically, when looking for a segment location, an empty group that has * a spot for the segment will be overlooked if there is anything else before that spot. * This may result in surprising (but sensible) behaviour if a message is missing the * first segment in a group. * * @author Bryan Tripp */ public class MessageIterator implements java.util.Iterator { private Structure currentStructure; private String direction; private Position next; private boolean handleUnexpectedSegments; private static final HapiLog log = HapiLogFactory.getHapiLog(MessageIterator.class); /* may add configurability later ... private boolean findUpToFirstRequired; private boolean findFirstDescendentsOnly; public static final String WHOLE_GROUP; public static final String FIRST_DESCENDENTS_ONLY; public static final String UP_TO_FIRST_REQUIRED; */ /** Creates a new instance of MessageIterator */ public MessageIterator(Structure start, String direction, boolean handleUnexpectedSegments) { this.currentStructure = start; this.direction = direction; this.handleUnexpectedSegments = handleUnexpectedSegments; } /* for configurability (maybe to add later, replacing hard-coded options in nextFromEndOfGroup) ... public void setSearchLevel(String level) { if (WHOLE_GROUP.equals(level)) { this.findUpToFirstRequired = false; this.findFirstDescendentsOnly = false; } else if (FIRST_DESCENDENTS_ONLY.equals(level)) { this.findUpToFirstRequired = false; this.findFirstDescendentsOnly = true; } else if (UP_TO_FIRST_REQUIRED.equals(level)) { this.findUpToFirstRequired = true; this.findFirstDescendentsOnly = false; } else { throw IllegalArgumentException(level + " is not a valid search level. Should be WHOLE_GROUP, etc."); } } public String getSearchLevel() { String level = WHOLE_GROUP; if (this.findFirstDescendentsOnly) { level = FIRST_DESCENDENTS_ONLY; } else if (this.findUpTpFirstRequired) { level = UP_TO_FIRST_REQUIRED; } return level; }*/ /** * Returns true if another object exists in the iteration sequence. */ public boolean hasNext() { boolean has = true; if (next == null) { if (Group.class.isAssignableFrom(currentStructure.getClass())) { groupNext((Group) currentStructure); } else { Group parent = currentStructure.getParent(); Index i = getIndex(parent, currentStructure); Position currentPosition = new Position(parent, i); try { if (parent.isRepeating(i.name) && currentStructure.getName().equals(direction)) { nextRep(currentPosition); } else { has = nextPosition(currentPosition, this.direction, this.handleUnexpectedSegments); } } catch (HL7Exception e) { throw new Error("HL7Exception arising from bad index: " + e.getMessage()); } } } log.debug("MessageIterator.hasNext() in direction " + this.direction + "? " + has); return has; } /** * Sets next to the first child of the given group (iteration * always proceeds from group to first child). */ private void groupNext(Group current) { next = new Position(current, ((Group) current).getNames()[0], 0); } /** * Sets next to the next repetition of the current structure. */ private void nextRep(Position current) { next = new Position(current.parent, current.index.name, current.index.rep + 1); } /** * Sets this.next to the next position in the message (from the given position), * which could be the next sibling, a new segment, or the next rep * of the parent. See next() for details. */ private boolean nextPosition(Position currPos, String direction, boolean makeNewSegmentIfNeeded) throws HL7Exception { boolean nextExists = true; if (isLast(currPos)) { nextExists = nextFromGroupEnd(currPos, direction, makeNewSegmentIfNeeded); } else { nextSibling(currPos); } return nextExists; } /** Navigates from end of group */ private boolean nextFromGroupEnd(Position currPos, String direction, boolean makeNewSegmentIfNeeded) throws HL7Exception { assert isLast(currPos); boolean nextExists = true; //the following conditional logic is a little convoluted -- its meant as an optimization // i.e. trying to avoid calling matchExistsAfterCurrentPosition if (!makeNewSegmentIfNeeded && Message.class.isAssignableFrom(currPos.parent.getClass())) { nextExists = false; } else if (!makeNewSegmentIfNeeded || matchExistsAfterPosition(currPos, direction, false, true)) { Group grandparent = currPos.parent.getParent(); Index parentIndex = getIndex(grandparent, currPos.parent); Position parentPos = new Position(grandparent, parentIndex); try { boolean parentRepeats = parentPos.parent.isRepeating(parentPos.index.name); if (parentRepeats && contains(parentPos.parent.get(parentPos.index.name, 0), direction, false, true)) { nextRep(parentPos); } else { nextExists = nextPosition(parentPos, direction, makeNewSegmentIfNeeded); } } catch (HL7Exception e) { throw new Error("HL7Exception arising from bad index: " + e.getMessage()); } } else { newSegment(currPos.parent, direction); } return nextExists; } /** * A match exists for the given name somewhere after the given position (in the * normal serialization order). * @param pos the message position after which to look (note that this specifies * the message instance) * @param name the name of the structure to look for * @param firstDescendentsOnly only searches the first children of a group * @param upToFirstRequired only searches the children of a group up to the first * required child (normally the first one). This is used when we are parsing * a message in order and looking for a place to parse a particular segment -- * if the message is correct then it can't go after a required position of a * different name. */ public static boolean matchExistsAfterPosition(Position pos, String name, boolean firstDescendentsOnly, boolean upToFirstRequired) throws HL7Exception { boolean matchExists = false; //check next rep of self (if any) if (pos.parent.isRepeating(pos.index.name)) { Structure s = pos.parent.get(pos.index.name, pos.index.rep); matchExists = contains(s, name, firstDescendentsOnly, upToFirstRequired); } //check later siblings (if any) if (!matchExists) { String[] siblings = pos.parent.getNames(); boolean after = false; for (int i = 0; i < siblings.length && !matchExists; i++) { if (after) { matchExists = contains(pos.parent.get(siblings[i]), name, firstDescendentsOnly, upToFirstRequired); if (upToFirstRequired && pos.parent.isRequired(siblings[i])) break; } if (pos.index.name.equals(siblings[i])) after = true; } } //recurse to parent (if parent is not message root) if (!matchExists && !Message.class.isAssignableFrom(pos.parent.getClass())) { Group grandparent = pos.parent.getParent(); Position parentPos = new Position(grandparent, getIndex(grandparent, pos.parent)); matchExists = matchExistsAfterPosition(parentPos, name, firstDescendentsOnly, upToFirstRequired); } log.debug("Match exists after position " + pos + " for " + name + "? " + matchExists); return matchExists; } /** * Sets the next position to a new segment of the given name, within the * given group. */ private void newSegment(Group parent, String name) throws HL7Exception { log.info("MessageIterator creating new segment: " + name); parent.addNonstandardSegment(name); next = new Position(parent, parent.getNames()[parent.getNames().length-1], 0); } /** * Determines whether the given structure matches the given name, or contains * a child that does. * @param s the structure to check * @param name the name to look for * @param firstDescendentsOnly only checks first descendents (i.e. first * child, first child of first child, etc.) In theory the first child * of a group should always be present, and we don't use this method with * subsequent children because finding the next position within a group is * straightforward. * @param upToFirstRequired only checks first descendents and of their siblings * up to the first required one. This may be needed because in practice * some first children of groups are not required. */ public static boolean contains(Structure s, String name, boolean firstDescendentsOnly, boolean upToFirstRequired) { boolean contains = false; if (Segment.class.isAssignableFrom(s.getClass())) { if (s.getName().equals(name)) contains = true; } else { Group g = (Group) s; String[] names = g.getNames(); for (int i = 0; i < names.length && !contains; i++) { try { contains = contains(g.get(names[i], 0), name, firstDescendentsOnly, upToFirstRequired); if (firstDescendentsOnly) break; if (upToFirstRequired && g.isRequired(names[i])) break; } catch (HL7Exception e) { throw new Error("HL7Exception due to bad index: " + e.getMessage()); } } } return contains; } /** * Tests whether the name of the given Index matches * the name of the last child of the given group. */ public static boolean isLast(Position p) { String[] names = p.parent.getNames(); return names[names.length-1].equals(p.index.name); } /** * Sets the next location to the next sibling of the given * index. */ private void nextSibling(Position pos) { String[] names = pos.parent.getNames(); int i = 0; for (; i < names.length && !names[i].equals(pos.index.name); i++) {} String nextName = names[i+1]; this.next = new Position(pos.parent, nextName, 0); } /** *

Returns the next node in the message. Sometimes the next node is * ambiguous. For example at the end of a repeating group, the next node * may be the first segment in the next repetition of the group, or the * next sibling, or an undeclared segment locally added to the group's end. * Cases like this are disambiguated using getDirection(), which returns * the name of the structure that we are "iterating towards". * Usually we are "iterating towards" a segment of a certain name because we * have a segment string that we would like to parse into that node. * Here are the rules:

*
  1. If at a group, next means first child.
  2. *
  3. If at a non-repeating segment, next means next "position"
  4. *
  5. If at a repeating segment: if segment name matches * direction then next means next rep, otherwise next means next "position".
  6. *
  7. If at a segment within a group (not at the end of the group), next "position" * means next sibling
  8. *
  9. If at the end of a group: If name of group or any of its "first * decendents" matches direction, then next position means next rep of group. Otherwise * if direction matches name of next sibling of the group, or any of its first * descendents, next position means next sibling of the group. Otherwise, next means a * new segment added to the group (with a name that matches "direction").
  10. *
  11. "First descendents" means first child, or first child of the first child, * or first child of the first child of the first child, etc.
*/ public Object next() { if (!hasNext()) { throw new NoSuchElementException("No more nodes in message"); } try { this.currentStructure = next.parent.get(next.index.name, next.index.rep); } catch (HL7Exception e) { throw new NoSuchElementException("HL7Exception: " + e.getMessage()); } clearNext(); return this.currentStructure; } /** Not supported */ public void remove() { throw new UnsupportedOperationException("Can't remove a node from a message"); } public String getDirection() { return this.direction; } public void setDirection(String direction) { clearNext(); this.direction = direction; } private void clearNext() { next = null; } /** * Returns the index of the given structure as a child of the * given parent. Returns null if the child isn't found. */ public static Index getIndex(Group parent, Structure child) { Index index = null; String[] names = parent.getNames(); findChild : for (int i = 0; i < names.length; i++) { if (names[i].startsWith(child.getName())) { try { Structure[] reps = parent.getAll(names[i]); for (int j = 0; j < reps.length; j++) { if (child == reps[j]) { index = new Index(names[i], j); break findChild; } } } catch (HL7Exception e) { log.error("", e); throw new Error("Internal HL7Exception finding structure index: " + e.getMessage()); } } } return index; } /** * An index of a child structure within a group, consisting of the name and rep of * of the child. */ public static class Index { public String name; public int rep; public Index(String name, int rep) { this.name = name; this.rep = rep; } /** @see Object#equals */ public boolean equals(Object o) { boolean equals = false; if (o != null && o instanceof Index) { Index i = (Index) o; if (i.rep == rep && i.name.equals(name)) equals = true; } return equals; } /** @see Object#hashCode */ public int hashCode() { return name.hashCode() + 700 * rep; } /** @see Object#toString */ public String toString() { return this.name + ":" + this.rep; } } /** * A structure position within a message. */ public static class Position { public Group parent; public Index index; public Position(Group parent, String name, int rep) { this.parent = parent; this.index = new Index(name, rep); } public Position(Group parent, Index i) { this.parent = parent; this.index = i; } /** @see Object#equals */ public boolean equals(Object o) { boolean equals = false; if (o != null && o instanceof Position) { Position p = (Position) o; if (p.parent.equals(parent) && p.index.equals(index)) equals = true; } return equals; } /** @see Object#hashCode */ public int hashCode() { return parent.hashCode() + index.hashCode(); } public String toString() { StringBuffer ret = new StringBuffer(parent.getName()); ret.append(":"); ret.append(index.name); ret.append("("); ret.append(index.rep); ret.append(")"); return ret.toString(); } } }