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}