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 "GroupPointer.java". Description: 010 "A GroupPointer is used when parsing traditionally encoded HL7 messages" 011 012 The Initial Developer of the Original Code is University Health Network. Copyright (C) 013 2001. 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 package ca.uhn.hl7v2.parser; 028 029 import java.io.BufferedReader; 030 import java.io.IOException; 031 import java.io.InputStreamReader; 032 import java.net.URL; 033 import java.net.URLConnection; 034 import java.util.ArrayList; 035 import java.util.Arrays; 036 import java.util.HashMap; 037 import java.util.Iterator; 038 import java.util.List; 039 import java.util.Map; 040 import java.util.Stack; 041 import java.util.StringTokenizer; 042 043 import ca.uhn.hl7v2.HL7Exception; 044 import ca.uhn.hl7v2.model.Message; 045 import ca.uhn.hl7v2.model.Primitive; 046 import ca.uhn.hl7v2.model.Segment; 047 import ca.uhn.hl7v2.model.Type; 048 import ca.uhn.hl7v2.util.Terser; 049 import ca.uhn.log.HapiLog; 050 import ca.uhn.log.HapiLogFactory; 051 052 /** 053 * A Parser for the ER7 encoding, which is faster than PipeParser, but fussier and harder to use. 054 * It's harder to use in that you must tell it ahead of time which segments and fields to parse 055 * for each event, as well as where in a message structure to find each segment. It's fussier in 056 * that each segment you identify as to-be-parsed must always be present in the message (although 057 * it can be empty -- the minimum needed is the segment name and a carriage return). 058 * 059 * Note that an instance of configuration data (see StructRef below) takes on some state during parsing, 060 * so it can only be used to parse one message at a time. There is a synchronized block to ensure this, 061 * but if parallel parsing with the same configuration (e.g. parsing multiple messages of the same event 062 * at once) is needed, you may want to pool some FastParsers or use separate ones in separate threads. 063 * 064 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a> 065 * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $ 066 */ 067 public class FastParser extends Parser { 068 069 private static final HapiLog ourLog = HapiLogFactory.getHapiLog(FastParser.class); 070 071 private static char ourSegmentSeparator = '\r'; 072 private Map myEventGuideMap; 073 private PipeParser myPipeParser; 074 075 /** 076 * @param theEventGuideMap a map with keys in the form "type^event" (like MSH-9 077 * components 1 and 2). Values are corresponding parsing guides for those events. 078 * A parsing guide is a group of StructRef that identify which segments to parse, 079 * the relationships between them, and where to find them in a message hierarchy. 080 * The value in the map is the RootRef of the message root. It must return the 081 * StructRef for the MSH segment from getSuccessor("MSH"). References to other 082 * segments can be included as needed. 083 */ 084 public FastParser(Map theEventGuideMap) { 085 this(null, theEventGuideMap); 086 } 087 088 /** 089 * @param theFactory custom factory to use for model class lookup 090 * @param theEventGuideMap a map with keys in the form "type^event" (like MSH-9 091 * components 1 and 2). Values are corresponding parsing guides for those events. 092 * A parsing guide is a group of StructRef that identify which segments to parse, 093 * the relationships between them, and where to find them in a message hierarchy. 094 * The value in the map is the RootRef of the message root. It must return the 095 * StructRef for the MSH segment from getSuccessor("MSH"). References to other 096 * segments can be included as needed. 097 */ 098 public FastParser(ModelClassFactory theFactory, Map theEventGuideMap) { 099 super(theFactory); 100 myEventGuideMap = theEventGuideMap; 101 myPipeParser = new PipeParser(); 102 } 103 104 /** 105 * Loads a parsing guide map (as required for FastParser instantiation). The URL should 106 * point to a file with one or more guides in sections delimited by blank lines. Within 107 * a section, the first line must contain an event name of the for "type^event". Subsequent 108 * lines define the parsed parts of messages with that event. Each line begins with either 109 * a segment name or "{" (indicating group start) or "}" (indicating group end). Group 110 * start lines then have whitespace and a Terser path to the group (relative to the closest 111 * ancestor group listed in the parsin guide). Segment lines then have whitespace and a 112 * relative Terser path to the segment, followed by a colon and a comma-delimited list of field 113 * numbers, which indicates which fields for that segment are to be parsed. Within Terser 114 * paths, repetition numbers must be replaced with asterisks. An example follows: 115 * 116 * ORU^R01 117 * MSH MSH:9,12 118 * { ORU_R01_PIDNTEPV1ORCOBRNTEOBXNTE(*) 119 * { ORU_R01_PIDNTEPV1 120 * PID PID:3-5 121 * } 122 * { ORU_R01_ORCOBRNTEOBXNTE(*) 123 * { ORU_R01_OBXNTE(*) 124 * OBX OBX:2,5 125 * } 126 * } 127 * } 128 * 129 * ADT^A01 130 * MSH MSH:9,12 131 * PID PID:3 132 * PV1 PV1:7-9 133 * 134 * @param theMapURL an URL to a file of the form desribed above 135 * @return the corresponding Map 136 */ 137 public static Map loadEventGuideMap(URL theMapURL) throws HL7Exception { 138 Map result = new HashMap(); 139 140 try { 141 URLConnection conn = theMapURL.openConnection(); 142 BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); 143 144 String eventName = null; 145 StringBuffer spec = new StringBuffer(); 146 String line = null; 147 while ((line = reader.readLine()) != null) { 148 if (line.length() == 0) { 149 finish(eventName, spec, result); 150 eventName = null; 151 spec = new StringBuffer(); 152 } else { 153 if (eventName == null) { 154 eventName = line; 155 } else { 156 spec.append(line + "\r"); 157 } 158 } 159 } 160 reader.close(); 161 finish(eventName, spec, result); 162 } catch (IOException e) { 163 throw new HL7Exception(e); 164 } 165 166 return result; 167 } 168 169 private static void finish(String theEventName, StringBuffer theSpec, Map theMap) { 170 if (theEventName != null) { 171 RootRef root = parseGuide(theSpec.toString()); 172 theMap.put(theEventName, root); 173 } 174 } 175 176 private static RootRef parseGuide(String theSpec) { 177 StringTokenizer lines = new StringTokenizer(theSpec, "\r", false); 178 RootRef result = new RootRef(); 179 Stack ancestry = new Stack(); 180 ancestry.push(result); 181 Map successors = new HashMap(); 182 183 StructRef previous = result; 184 while (lines.hasMoreTokens()) { 185 String line = lines.nextToken(); 186 StringTokenizer parts = new StringTokenizer(line, "\t ", false); 187 String segName = parts.nextToken(); 188 String path = parts.hasMoreTokens() ? parts.nextToken() : ""; 189 parts = new StringTokenizer(path, ":", false); 190 path = parts.hasMoreTokens() ? parts.nextToken() : null; 191 192 int[] fields = getFieldList(parts.hasMoreTokens() ? parts.nextToken() : ""); 193 194 if (segName.equals("}")) { 195 StructRef parent = (StructRef) ancestry.pop(); 196 if (parent.getChildName() != null && parent.getRelativePath().indexOf('*') >= 0) { //repeating group 197 previous.setSuccessor(parent.getChildName(), parent); 198 } 199 } else { 200 boolean isSegment = !(segName.equals("{")); 201 StructRef ref = new StructRef((StructRef) ancestry.peek(), path, isSegment, fields); 202 if (isSegment) { 203 previous.setSuccessor(segName, ref); 204 if (path.indexOf('*') >= 0) ref.setSuccessor(segName, ref); 205 setGroupSuccessors(successors, segName); 206 } else { 207 successors.put(previous, ref); 208 } 209 if (!isSegment) ancestry.push(ref); 210 previous = ref; 211 } 212 } 213 214 return result; 215 } 216 217 private static void setGroupSuccessors(Map theSuccessors, String theSegName) { 218 for (Iterator it = theSuccessors.keySet().iterator(); it.hasNext(); ) { 219 StructRef from = (StructRef) it.next(); 220 StructRef to = (StructRef) theSuccessors.get(from); 221 from.setSuccessor(theSegName, to); 222 } 223 theSuccessors.clear(); 224 } 225 226 private static int[] getFieldList(String theSpec) { 227 StringTokenizer tok = new StringTokenizer(theSpec, ",", false); 228 List fieldList = new ArrayList(30); 229 while (tok.hasMoreTokens()) { 230 String token = tok.nextToken(); 231 int index = token.indexOf('-'); 232 if (index >= 0) { //it's a range 233 int start = Integer.parseInt(token.substring(0, index)); 234 int end = Integer.parseInt(token.substring(index+1)); 235 for (int i = start; i <= end; i++) { 236 fieldList.add(new Integer(i)); 237 } 238 } else { 239 fieldList.add(Integer.valueOf(token)); 240 } 241 } 242 243 int[] result = new int[fieldList.size()]; 244 for (int i = 0; i < result.length; i++) { 245 result[i] = ((Integer) fieldList.get(i)).intValue(); 246 } 247 248 return result; 249 } 250 251 /** 252 * @see ca.uhn.hl7v2.parser.Parser#getEncoding(java.lang.String) 253 */ 254 public String getEncoding(String message) { 255 return myPipeParser.getEncoding(message); 256 } 257 258 /** 259 * @see ca.uhn.hl7v2.parser.Parser#supportsEncoding(java.lang.String) 260 */ 261 public boolean supportsEncoding(String encoding) { 262 return myPipeParser.supportsEncoding(encoding); 263 } 264 265 /** 266 * @return the preferred encoding of this Parser 267 */ 268 public String getDefaultEncoding() { 269 return "VB"; 270 } 271 272 /** 273 * @see ca.uhn.hl7v2.parser.Parser#doParse(java.lang.String, java.lang.String) 274 */ 275 protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException { 276 Message result = null; 277 278 char fieldSep = message.charAt(3); 279 EncodingCharacters ec = new EncodingCharacters(fieldSep, message.substring(4, 8)); 280 281 StringTokenizer tok = new StringTokenizer(message.substring(4), 282 String.valueOf(new char[]{fieldSep, ourSegmentSeparator}), true); 283 284 String[] mshFields = getMSHFields(tok, fieldSep); 285 Object[] structure = getStructure(mshFields[8], ec.getComponentSeparator()); 286 287 StructRef root = (StructRef) myEventGuideMap.get(structure[0]); 288 if (root == null) { 289 ourLog.debug("FastParser delegating to PipeParser because no metadata available for event " 290 + structure[0]); 291 result = myPipeParser.parse(message); 292 } else { 293 int csIndex = mshFields[11].indexOf(ec.getComponentSeparator()); 294 result = instantiateMessage((String) structure[1], version, ((Boolean) structure[2]).booleanValue()); 295 296 StructRef mshRef = null; 297 synchronized (root) { 298 mshRef = root.getSuccessor("MSH"); 299 root.reset(); 300 } 301 Segment msh = (Segment) result.get("MSH"); 302 for (int i = 0; i < mshRef.getFields().length; i++) { 303 int fieldNum = mshRef.getFields()[i]; 304 parse(mshFields[fieldNum-1], msh, fieldNum, ec); 305 } 306 307 parse(tok, result, root, ec); 308 } 309 310 return result; 311 } 312 313 private String[] getMSHFields(StringTokenizer tok, char fieldSep) { 314 String[] result = new String[21]; 315 result[0] = String.valueOf(fieldSep); 316 String token = null; 317 int field = 1; 318 while (tok.hasMoreTokens() && (token = tok.nextToken()).charAt(0) != ourSegmentSeparator) { 319 if (token.charAt(0) == fieldSep) { 320 field++; 321 } else { 322 result[field] = token; 323 } 324 } 325 return result; 326 } 327 328 private void parse(StringTokenizer tok, Message message, StructRef root, EncodingCharacters ec) 329 throws HL7Exception { 330 331 Terser t = new Terser(message); 332 333 synchronized (root) { 334 StructRef ref = root.getSuccessor("MSH"); 335 336 int field = 0; 337 Segment segment = null; 338 int[] fields = new int[0]; 339 340 while (tok.hasMoreTokens()) { 341 String token = tok.nextToken(); 342 if (token.charAt(0) == ec.getFieldSeparator()) { 343 field++; 344 } else if (token.charAt(0) == ourSegmentSeparator) { 345 field = 0; 346 } else if (field == 0) { 347 StructRef newref = drill(ref, token); 348 if (newref == null) { 349 segment = null; 350 fields = new int[0]; 351 } else { 352 ref = newref; 353 if (ourLog.isDebugEnabled()) { 354 ourLog.debug("Parsing into segment " + ref.getFullPath()); 355 } 356 segment = t.getSegment(ref.getFullPath()); 357 fields = ref.getFields(); 358 } 359 } else if (segment != null && Arrays.binarySearch(fields, field) >= 0) { 360 parse(token, segment, field, ec); 361 } 362 } 363 root.reset(); 364 } 365 } 366 367 //drill through groups to a segment 368 private StructRef drill(StructRef ref, String name) { 369 ref = ref.getSuccessor(name); 370 while (ref != null && !ref.isSegment()) { 371 ref = ref.getSuccessor(name); 372 } 373 return ref; 374 } 375 376 private void parse(String field, Segment segment, int num, EncodingCharacters ec) throws HL7Exception { 377 if (field != null) { 378 int rep = 0; 379 int component = 1; 380 int subcomponent = 1; 381 Type type = segment.getField(num, rep); 382 383 String delim = String.valueOf(new char[]{ec.getRepetitionSeparator(), 384 ec.getComponentSeparator(), ec.getSubcomponentSeparator()}); 385 for (StringTokenizer tok = new StringTokenizer(field, delim, true); tok.hasMoreTokens(); ) { 386 String token = tok.nextToken(); 387 char c = token.charAt(0); 388 if (c == ec.getRepetitionSeparator()) { 389 rep++; 390 component = 1; 391 subcomponent = 1; 392 type = segment.getField(num, rep); 393 } else if (c == ec.getComponentSeparator()) { 394 component++; 395 subcomponent = 1; 396 } else if (c == ec.getSubcomponentSeparator()) { 397 subcomponent++; 398 } else { 399 Primitive p = Terser.getPrimitive(type, component, subcomponent); 400 p.setValue(token); 401 } 402 } 403 } 404 } 405 406 /** 407 * @returns the message structure from MSH-9-3 408 */ 409 private Object[] getStructure(String msh9, char compSep) throws HL7Exception { 410 String structure = null; 411 String event = null; 412 413 String[] components = new String[3]; 414 StringTokenizer tok = new StringTokenizer(msh9, String.valueOf(compSep), true); 415 for (int i = 0; tok.hasMoreTokens() && i < components.length; ) { 416 String token = tok.nextToken(); 417 if (token.charAt(0) == compSep) { 418 i++; 419 } else { 420 components[i] = token; 421 } 422 } 423 424 boolean explicitlyDefined = (components[2] == null) ? false : true; 425 426 if (explicitlyDefined) { 427 structure = components[2]; 428 } else if (components[0] != null && components[0].equals("ACK")) { 429 structure = "ACK"; 430 } else if (components[0] != null && components[1] != null) { 431 structure = components[0] + "_" + components[1]; 432 } else { 433 throw new HL7Exception("Can't determine message structure from MSH-9: " + msh9, 434 HL7Exception.UNSUPPORTED_MESSAGE_TYPE); 435 } 436 437 if (components[1] == null) { 438 event = components[0]; 439 } else { 440 event = components[0] + "^" + components[1]; 441 } 442 443 return new Object[] {event, structure, Boolean.valueOf(explicitlyDefined)}; 444 } 445 446 447 /** 448 * @see ca.uhn.hl7v2.parser.Parser#encode(ca.uhn.hl7v2.model.Message, java.lang.String) 449 */ 450 protected String doEncode(Message source, String encoding) throws HL7Exception, 451 EncodingNotSupportedException { 452 return myPipeParser.doEncode(source, encoding); 453 } 454 455 /** 456 * @see ca.uhn.hl7v2.parser.Parser#encode(ca.uhn.hl7v2.model.Message) 457 */ 458 protected String doEncode(Message source) throws HL7Exception { 459 return myPipeParser.doEncode(source); 460 } 461 462 /** 463 * @see ca.uhn.hl7v2.parser.Parser#getCriticalResponseData(java.lang.String) 464 */ 465 public Segment getCriticalResponseData(String message) throws HL7Exception { 466 return myPipeParser.getCriticalResponseData(message); 467 } 468 469 /** 470 * @see ca.uhn.hl7v2.parser.Parser#getAckID(java.lang.String) 471 */ 472 public String getAckID(String message) { 473 return myPipeParser.getAckID(message); 474 } 475 476 /** 477 * @see ca.uhn.hl7v2.parser.Parser#getVersion(java.lang.String) 478 */ 479 public String getVersion(String message) throws HL7Exception { 480 return myPipeParser.getVersion(message); 481 } 482 483 /** 484 * Not supported, throws UnsupportedOperationException 485 * 486 * @throws UnsupportedOperationException 487 */ 488 @Override 489 public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { 490 throw new UnsupportedOperationException("Not supported yet."); 491 } 492 493 /** 494 * Not supported, throws UnsupportedOperationException 495 * 496 * @throws UnsupportedOperationException 497 */ 498 @Override 499 public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { 500 throw new UnsupportedOperationException("Not supported yet."); 501 } 502 503 /** 504 * Not supported, throws UnsupportedOperationException 505 * 506 * @throws UnsupportedOperationException 507 */ 508 @Override 509 public void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 510 throw new UnsupportedOperationException("Not supported yet."); 511 } 512 513 /** 514 * Not supported, throws UnsupportedOperationException 515 * 516 * @throws UnsupportedOperationException 517 */ 518 @Override 519 public void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception { 520 throw new UnsupportedOperationException("Not supported yet."); 521 } 522 523 524 /** 525 * Not supported, throws UnsupportedOperationException 526 * 527 * @throws UnsupportedOperationException 528 */ 529 @Override 530 public void parse(Message message, String string) throws HL7Exception { 531 throw new UnsupportedOperationException("Not supported yet."); 532 } 533 534 /** 535 * A pointer to a distinct segment or group position in a message. 536 * 537 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a> 538 * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $ 539 */ 540 public static class StructRef { 541 542 private StructRef myParent; 543 private String myRelativePath; 544 private Map mySuccessors; 545 private int myRep; 546 private boolean mySegmentFlag; 547 //private boolean myResettableFlag; 548 private int[] myFields; 549 private List myChildren; 550 551 /** 552 * @param theParent a StructRef for the parent Group of the referenced Structure 553 * @param theRelativePath the relative (from the parent) Terser path to the referenced 554 * structure. If the structure repeats, the rep number should be replaced with "*" 555 * (it will be incremented as needed). 556 * @param isSegment true iff the referenced Structure is a Segment (rather than a Group) 557 * @param theFields a list of fields to be parsed for this segment (null or empty for groups) 558 */ 559 public StructRef(StructRef theParent, String theRelativePath, boolean isSegment, int[] theFields) { 560 myParent = theParent; 561 myChildren = new ArrayList(); 562 if (myParent != null) myParent.addChild(this); 563 564 myRelativePath = theRelativePath; 565 if (!myRelativePath.startsWith("/")) { 566 myRelativePath = "/" + myRelativePath; 567 } 568 mySegmentFlag = isSegment; 569 mySuccessors = new HashMap(); 570 myRep = -1; 571 if (mySegmentFlag) { 572 myFields = theFields; 573 Arrays.sort(myFields); 574 } else { 575 myFields = new int[0]; 576 } 577 //myResettableFlag = (myParent == null) ? true : false; 578 } 579 580 /** 581 * Indicates an immediately subsequent structure in parsing order. A Structure in a list 582 * should point to the next Structure in the list. A Structure that repeats should point to 583 * itself. A Structure at the end of a repeating Group should point to the Group. 584 * A Group should point to its first child. 585 * 586 * @param theName name of the next Segment in this direction (ie if the next structure is a group, 587 * not that one) 588 * @param theSuccessor the immediately next StructRef in that direction 589 */ 590 public void setSuccessor(String theName, StructRef theSuccessor) { 591 mySuccessors.put(theName, theSuccessor); 592 } 593 594 /** 595 * @return full Terser path, including parent and repetition information. 596 */ 597 public String getFullPath() { 598 return myParent.getFullPath() + myRelativePath.replaceAll("\\*", String.valueOf(myRep)); 599 } 600 601 /** 602 * @return relative Terser path as defined in constructor 603 */ 604 public String getRelativePath() { 605 return myRelativePath; 606 } 607 608 /** 609 * @param theName name of a successor in parse order, as set in setSuccessor() 610 * @return the StructRef under that name 611 */ 612 public StructRef getSuccessor(String theName) { 613 StructRef ref = (StructRef) mySuccessors.get(theName); 614 if (ref != null) { 615 ref.next(); 616 } 617 return ref; 618 } 619 620 /** 621 * @return name of first successor, if available and if this is not a segment reference, 622 * otherwise null 623 */ 624 public String getChildName() { 625 String result = null; 626 if (!mySegmentFlag && !mySuccessors.isEmpty()) { 627 result = (String) mySuccessors.keySet().iterator().next(); 628 } 629 return result; 630 } 631 632 /** 633 * @return true iff referenced Structure is a Segment 634 */ 635 public boolean isSegment() { 636 return mySegmentFlag; 637 } 638 639 /** 640 * Increments the repetition number of the underlying Structure, which is used in getFullPath() 641 */ 642 private void next() { 643 myRep++; 644 resetChildren(); 645 } 646 647 private void addChild(StructRef theChild) { 648 if (!isSegment()) { 649 myChildren.add(theChild); 650 } 651 } 652 653 /** 654 * Resets the StructRef to its starting state, before its first iteration, and resets 655 * its children as well. 656 */ 657 public void reset() { 658 myRep = -1; 659 resetChildren(); 660 } 661 662 private void resetChildren() { 663 for (int i = 0; i < myChildren.size(); i++) { 664 StructRef child = (StructRef) myChildren.get(i); 665 child.reset(); 666 } 667 } 668 669 /** 670 * @return an ordered list of fields to be parsed for this segment (empty if not a segment) 671 */ 672 public int[] getFields() { 673 return myFields; 674 } 675 676 } 677 678 /** 679 * A convenience StructRef that points to a message root. 680 * 681 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a> 682 * @version $Revision: 1.3 $ updated on $Date: 2009/10/03 15:25:46 $ by $Author: jamesagnew $ 683 */ 684 public static class RootRef extends StructRef { 685 public RootRef() { 686 super(null, "", false, null); 687 } 688 689 public String getFullPath() { 690 return ""; 691 } 692 } 693 694 }