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 "PipeParser.java". Description: 010 * "An implementation of Parser that supports traditionally encoded (i.e" 011 * 012 * The Initial Developer of the Original Code is University Health Network. Copyright (C) 013 * 2001. All Rights Reserved. 014 * 015 * Contributor(s): Kenneth Beaton. 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 028 package ca.uhn.hl7v2.parser; 029 030 import java.util.ArrayList; 031 import java.util.StringTokenizer; 032 033 import ca.uhn.hl7v2.HL7Exception; 034 import ca.uhn.hl7v2.model.Group; 035 import ca.uhn.hl7v2.model.Message; 036 import ca.uhn.hl7v2.model.Primitive; 037 import ca.uhn.hl7v2.model.Segment; 038 import ca.uhn.hl7v2.model.Structure; 039 import ca.uhn.hl7v2.model.Type; 040 import ca.uhn.hl7v2.model.Varies; 041 import ca.uhn.hl7v2.util.Terser; 042 import ca.uhn.hl7v2.util.ReflectionUtil; 043 import ca.uhn.log.HapiLog; 044 import ca.uhn.log.HapiLogFactory; 045 import java.util.HashMap; 046 047 /** 048 * An implementation of Parser that supports traditionally encoded (ie delimited 049 * with characters like |, ^, and ~) HL7 messages. Unexpected segments and 050 * fields are parsed into generic elements that are added to the message. 051 * 052 * @author Bryan Tripp (bryan_tripp@sourceforge.net) 053 */ 054 public class PipeParser extends Parser { 055 056 private static final HapiLog log = HapiLogFactory.getHapiLog(PipeParser.class); 057 058 private final static String segDelim = "\r"; // see section 2.8 of spec 059 060 private final HashMap<Class<? extends Message>, StructureDefinition> myStructureDefinitions = new HashMap<Class<? extends Message>, StructureDefinition>(); 061 062 /** 063 * System property key. If value is "true", legacy mode will default to true 064 * 065 * @see #isLegacyMode() 066 */ 067 public static final String DEFAULT_LEGACY_MODE_PROPERTY = "ca.uhn.hl7v2.parser.PipeParser.default_legacy_mode"; 068 069 private Boolean myLegacyMode = null; 070 071 072 /** Creates a new PipeParser */ 073 public PipeParser() { 074 } 075 076 077 /** 078 * Creates a new PipeParser 079 * 080 * @param theFactory 081 * custom factory to use for model class lookup 082 */ 083 public PipeParser(ModelClassFactory theFactory) { 084 super(theFactory); 085 } 086 087 088 /** 089 * Returns a String representing the encoding of the given message, if the 090 * encoding is recognized. For example if the given message appears to be 091 * encoded using HL7 2.x XML rules then "XML" would be returned. If the 092 * encoding is not recognized then null is returned. That this method 093 * returns a specific encoding does not guarantee that the message is 094 * correctly encoded (e.g. well formed XML) - just that it is not encoded 095 * using any other encoding than the one returned. 096 */ 097 public String getEncoding(String message) { 098 String encoding = null; 099 100 // quit if the string is too short 101 if (message.length() < 4) 102 return null; 103 104 // see if it looks like this message is | encoded ... 105 boolean ok = true; 106 107 // string should start with "MSH" 108 if (!message.startsWith("MSH")) 109 return null; 110 111 // 4th character of each segment should be field delimiter 112 char fourthChar = message.charAt(3); 113 StringTokenizer st = new StringTokenizer(message, String.valueOf(segDelim), false); 114 while (st.hasMoreTokens()) { 115 String x = st.nextToken(); 116 if (x.length() > 0) { 117 if (Character.isWhitespace(x.charAt(0))) 118 x = stripLeadingWhitespace(x); 119 if (x.length() >= 4 && x.charAt(3) != fourthChar) 120 return null; 121 } 122 } 123 124 // should be at least 11 field delimiters (because MSH-12 is required) 125 int nextFieldDelimLoc = 0; 126 for (int i = 0; i < 11; i++) { 127 nextFieldDelimLoc = message.indexOf(fourthChar, nextFieldDelimLoc + 1); 128 if (nextFieldDelimLoc < 0) 129 return null; 130 } 131 132 if (ok) 133 encoding = "VB"; 134 135 return encoding; 136 } 137 138 139 /** 140 * @return the preferred encoding of this Parser 141 */ 142 public String getDefaultEncoding() { 143 return "VB"; 144 } 145 146 147 /** 148 * Returns true if and only if the given encoding is supported by this 149 * Parser. 150 */ 151 public boolean supportsEncoding(String encoding) { 152 boolean supports = false; 153 if (encoding != null && encoding.equals("VB")) 154 supports = true; 155 return supports; 156 } 157 158 159 /** 160 * @deprecated this method should not be public 161 * @param message 162 * @return 163 * @throws HL7Exception 164 * @throws EncodingNotSupportedException 165 */ 166 public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException { 167 return getStructure(message).messageStructure; 168 } 169 170 171 /** 172 * @returns the message structure from MSH-9-3 173 */ 174 private MessageStructure getStructure(String message) throws HL7Exception, EncodingNotSupportedException { 175 EncodingCharacters ec = getEncodingChars(message); 176 String messageStructure = null; 177 boolean explicityDefined = true; 178 String wholeFieldNine; 179 try { 180 String[] fields = split(message.substring(0, Math.max(message.indexOf(segDelim), message.length())), String.valueOf(ec.getFieldSeparator())); 181 wholeFieldNine = fields[8]; 182 183 // message structure is component 3 but we'll accept a composite of 184 // 1 and 2 if there is no component 3 ... 185 // if component 1 is ACK, then the structure is ACK regardless of 186 // component 2 187 String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator())); 188 if (comps.length >= 3) { 189 messageStructure = comps[2]; 190 } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) { 191 messageStructure = "ACK"; 192 } else if (comps.length == 2) { 193 explicityDefined = false; 194 messageStructure = comps[0] + "_" + comps[1]; 195 } 196 /* 197 * else if (comps.length == 1 && comps[0] != null && 198 * comps[0].equals("ACK")) { messageStructure = "ACK"; //it's common 199 * for people to only populate component 1 in an ACK msg } 200 */ 201 else { 202 StringBuffer buf = new StringBuffer("Can't determine message structure from MSH-9: "); 203 buf.append(wholeFieldNine); 204 if (comps.length < 3) { 205 buf.append(" HINT: there are only "); 206 buf.append(comps.length); 207 buf.append(" of 3 components present"); 208 } 209 throw new HL7Exception(buf.toString(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE); 210 } 211 } catch (IndexOutOfBoundsException e) { 212 throw new HL7Exception("Can't find message structure (MSH-9-3): " + e.getMessage(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE); 213 } 214 215 return new MessageStructure(messageStructure, explicityDefined); 216 } 217 218 219 /** 220 * Returns object that contains the field separator and encoding characters 221 * for this message. 222 */ 223 private static EncodingCharacters getEncodingChars(String message) { 224 return new EncodingCharacters(message.charAt(3), message.substring(4, 8)); 225 } 226 227 228 /** 229 * Parses a message string and returns the corresponding Message object. 230 * Unexpected segments added at the end of their group. 231 * 232 * @throws HL7Exception 233 * if the message is not correctly formatted. 234 * @throws EncodingNotSupportedException 235 * if the message encoded is not supported by this parser. 236 */ 237 protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException { 238 239 // try to instantiate a message object of the right class 240 MessageStructure structure = getStructure(message); 241 Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined); 242 243 parse(m, message); 244 245 return m; 246 } 247 248 249 /** 250 * Generates (or returns the cached value of) the message 251 */ 252 private IStructureDefinition getStructureDefinition(Class<? extends Message> theClazz) throws HL7Exception { 253 254 StructureDefinition retVal = myStructureDefinitions.get(theClazz); 255 if (retVal != null) { 256 return retVal; 257 } 258 259 Message message = ReflectionUtil.instantiateMessage(theClazz, getFactory()); 260 Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>(); 261 retVal = createStructureDefinition(message, previousLeaf); 262 myStructureDefinitions.put(theClazz, retVal); 263 264 return retVal; 265 } 266 267 268 private StructureDefinition createStructureDefinition(Structure theStructure, Holder<StructureDefinition> thePreviousLeaf) throws HL7Exception { 269 270 StructureDefinition retVal = new StructureDefinition(); 271 retVal.setName(theStructure.getName()); 272 273 if (theStructure instanceof Group) { 274 retVal.setSegment(false); 275 Group group = (Group) theStructure; 276 int index = 0; 277 for (String nextName : group.getNames()) { 278 Structure nextChild = group.get(nextName); 279 StructureDefinition structureDefinition = createStructureDefinition(nextChild, thePreviousLeaf); 280 structureDefinition.setNameAsItAppearsInParent(nextName); 281 structureDefinition.setRepeating(group.isRepeating(nextName)); 282 structureDefinition.setRequired(group.isRequired(nextName)); 283 structureDefinition.setPosition(index++); 284 structureDefinition.setParent(retVal); 285 retVal.addChild(structureDefinition); 286 } 287 } else { 288 if (thePreviousLeaf.getObject() != null) { 289 thePreviousLeaf.getObject().setNextLeaf(retVal); 290 } 291 thePreviousLeaf.setObject(retVal); 292 retVal.setSegment(true); 293 } 294 295 return retVal; 296 } 297 298 299 /** 300 * Parses a segment string and populates the given Segment object. 301 * Unexpected fields are added as Varies' at the end of the segment. 302 * 303 * @param theRepetition 304 * The repetition number of this segment within its group 305 * @throws HL7Exception 306 * if the given string does not contain the given segment or if 307 * the string is not encoded properly 308 */ 309 public void parse(Segment destination, String segment, EncodingCharacters encodingChars) throws HL7Exception { 310 parse(destination, segment, encodingChars, null); 311 } 312 313 314 /** 315 * Parses a segment string and populates the given Segment object. 316 * Unexpected fields are added as Varies' at the end of the segment. 317 * 318 * @param theRepetition 319 * The repetition number of this segment within its group 320 * @throws HL7Exception 321 * if the given string does not contain the given segment or if 322 * the string is not encoded properly 323 */ 324 public void parse(Segment destination, String segment, EncodingCharacters encodingChars, Integer theRepetition) throws HL7Exception { 325 int fieldOffset = 0; 326 if (isDelimDefSegment(destination.getName())) { 327 fieldOffset = 1; 328 // set field 1 to fourth character of string 329 Terser.set(destination, 1, 0, 1, 1, String.valueOf(encodingChars.getFieldSeparator())); 330 } 331 332 String[] fields = split(segment, String.valueOf(encodingChars.getFieldSeparator())); 333 // destination.setName(fields[0]); 334 for (int i = 1; i < fields.length; i++) { 335 String[] reps = split(fields[i], String.valueOf(encodingChars.getRepetitionSeparator())); 336 if (log.isDebugEnabled()) { 337 log.debug(reps.length + "reps delimited by: " + encodingChars.getRepetitionSeparator()); 338 } 339 340 // MSH-2 will get split incorrectly so we have to fudge it ... 341 boolean isMSH2 = isDelimDefSegment(destination.getName()) && i + fieldOffset == 2; 342 if (isMSH2) { 343 reps = new String[1]; 344 reps[0] = fields[i]; 345 } 346 347 for (int j = 0; j < reps.length; j++) { 348 try { 349 if (log.isDebugEnabled()) { 350 StringBuffer statusMessage = new StringBuffer("Parsing field "); 351 statusMessage.append(i + fieldOffset); 352 statusMessage.append(" repetition "); 353 statusMessage.append(j); 354 log.debug(statusMessage.toString()); 355 } 356 // parse(destination.getField(i + fieldOffset, j), reps[j], 357 // encodingChars, false); 358 359 Type field = destination.getField(i + fieldOffset, j); 360 if (isMSH2) { 361 Terser.getPrimitive(field, 1, 1).setValue(reps[j]); 362 } else { 363 parse(field, reps[j], encodingChars); 364 } 365 } catch (HL7Exception e) { 366 // set the field location and throw again ... 367 e.setFieldPosition(i); 368 if (theRepetition != null) { 369 e.setSegmentRepetition(theRepetition); 370 } 371 e.setSegmentName(destination.getName()); 372 throw e; 373 } 374 } 375 } 376 377 // set data type of OBX-5 378 if (destination.getClass().getName().indexOf("OBX") >= 0) { 379 Varies.fixOBX5(destination, getFactory()); 380 } 381 382 } 383 384 385 /** 386 * @return true if the segment is MSH, FHS, or BHS. These need special 387 * treatment because they define delimiters. 388 * @param theSegmentName 389 */ 390 private static boolean isDelimDefSegment(String theSegmentName) { 391 boolean is = false; 392 if (theSegmentName.equals("MSH") || theSegmentName.equals("FHS") || theSegmentName.equals("BHS")) { 393 is = true; 394 } 395 return is; 396 } 397 398 399 /** 400 * Fills a field with values from an unparsed string representing the field. 401 * 402 * @param destinationField 403 * the field Type 404 * @param data 405 * the field string (including all components and subcomponents; 406 * not including field delimiters) 407 * @param encodingCharacters 408 * the encoding characters used in the message 409 */ 410 @Override 411 public void parse(Type destinationField, String data, EncodingCharacters encodingCharacters) throws HL7Exception { 412 String[] components = split(data, String.valueOf(encodingCharacters.getComponentSeparator())); 413 for (int i = 0; i < components.length; i++) { 414 String[] subcomponents = split(components[i], String.valueOf(encodingCharacters.getSubcomponentSeparator())); 415 for (int j = 0; j < subcomponents.length; j++) { 416 String val = subcomponents[j]; 417 if (val != null) { 418 val = Escape.unescape(val, encodingCharacters); 419 } 420 Terser.getPrimitive(destinationField, i + 1, j + 1).setValue(val); 421 } 422 } 423 } 424 425 426 /** 427 * Splits the given composite string into an array of components using the 428 * given delimiter. 429 */ 430 public static String[] split(String composite, String delim) { 431 ArrayList<String> components = new ArrayList<String>(); 432 433 // defend against evil nulls 434 if (composite == null) 435 composite = ""; 436 if (delim == null) 437 delim = ""; 438 439 StringTokenizer tok = new StringTokenizer(composite, delim, true); 440 boolean previousTokenWasDelim = true; 441 while (tok.hasMoreTokens()) { 442 String thisTok = tok.nextToken(); 443 if (thisTok.equals(delim)) { 444 if (previousTokenWasDelim) 445 components.add(null); 446 previousTokenWasDelim = true; 447 } else { 448 components.add(thisTok); 449 previousTokenWasDelim = false; 450 } 451 } 452 453 String[] ret = new String[components.size()]; 454 for (int i = 0; i < components.size(); i++) { 455 ret[i] = (String) components.get(i); 456 } 457 458 return ret; 459 } 460 461 462 /** 463 * {@inheritDoc } 464 */ 465 @Override 466 public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { 467 return encode(structure, encodingCharacters); 468 } 469 470 471 /** 472 * {@inheritDoc } 473 */ 474 @Override 475 public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { 476 return encode(type, encodingCharacters); 477 } 478 479 480 /** 481 * Encodes the given Type, using the given encoding characters. It is 482 * assumed that the Type represents a complete field rather than a 483 * component. 484 */ 485 public static String encode(Type source, EncodingCharacters encodingChars) { 486 StringBuffer field = new StringBuffer(); 487 for (int i = 1; i <= Terser.numComponents(source); i++) { 488 StringBuffer comp = new StringBuffer(); 489 for (int j = 1; j <= Terser.numSubComponents(source, i); j++) { 490 Primitive p = Terser.getPrimitive(source, i, j); 491 comp.append(encodePrimitive(p, encodingChars)); 492 comp.append(encodingChars.getSubcomponentSeparator()); 493 } 494 field.append(stripExtraDelimiters(comp.toString(), encodingChars.getSubcomponentSeparator())); 495 field.append(encodingChars.getComponentSeparator()); 496 } 497 return stripExtraDelimiters(field.toString(), encodingChars.getComponentSeparator()); 498 // return encode(source, encodingChars, false); 499 } 500 501 502 private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) { 503 String val = (p).getValue(); 504 if (val == null) { 505 val = ""; 506 } else { 507 val = Escape.escape(val, encodingChars); 508 } 509 return val; 510 } 511 512 513 /** 514 * Removes unecessary delimiters from the end of a field or segment. This 515 * seems to be more convenient than checking to see if they are needed while 516 * we are building the encoded string. 517 */ 518 private static String stripExtraDelimiters(String in, char delim) { 519 char[] chars = in.toCharArray(); 520 521 // search from back end for first occurance of non-delimiter ... 522 int c = chars.length - 1; 523 boolean found = false; 524 while (c >= 0 && !found) { 525 if (chars[c--] != delim) 526 found = true; 527 } 528 529 String ret = ""; 530 if (found) 531 ret = String.valueOf(chars, 0, c + 2); 532 return ret; 533 } 534 535 536 /** 537 * Formats a Message object into an HL7 message string using the given 538 * encoding. 539 * 540 * @throws HL7Exception 541 * if the data fields in the message do not permit encoding 542 * (e.g. required fields are null) 543 * @throws EncodingNotSupportedException 544 * if the requested encoding is not supported by this parser. 545 */ 546 protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException { 547 if (!this.supportsEncoding(encoding)) 548 throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding"); 549 550 return encode(source); 551 } 552 553 554 /** 555 * Formats a Message object into an HL7 message string using this parser's 556 * default encoding ("VB"). 557 * 558 * @throws HL7Exception 559 * if the data fields in the message do not permit encoding 560 * (e.g. required fields are null) 561 */ 562 protected String doEncode(Message source) throws HL7Exception { 563 // get encoding characters ... 564 Segment msh = (Segment) source.get("MSH"); 565 String fieldSepString = Terser.get(msh, 1, 0, 1, 1); 566 567 if (fieldSepString == null) 568 throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing"); 569 570 char fieldSep = '|'; 571 if (fieldSepString != null && fieldSepString.length() > 0) 572 fieldSep = fieldSepString.charAt(0); 573 574 String encCharString = Terser.get(msh, 2, 0, 1, 1); 575 576 if (encCharString == null) 577 throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing"); 578 579 if (encCharString.length() != 4) 580 throw new HL7Exception("Encoding characters '" + encCharString + "' invalid -- must be 4 characters", HL7Exception.DATA_TYPE_ERROR); 581 EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString); 582 583 // pass down to group encoding method which will operate recursively on 584 // children ... 585 return encode((Group) source, en); 586 } 587 588 589 /** 590 * Returns given group serialized as a pipe-encoded string - this method is 591 * called by encode(Message source, String encoding). 592 */ 593 public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception { 594 StringBuffer result = new StringBuffer(); 595 596 String[] names = source.getNames(); 597 for (int i = 0; i < names.length; i++) { 598 Structure[] reps = source.getAll(names[i]); 599 for (int rep = 0; rep < reps.length; rep++) { 600 if (reps[rep] instanceof Group) { 601 result.append(encode((Group) reps[rep], encodingChars)); 602 } else { 603 String segString = encode((Segment) reps[rep], encodingChars); 604 if (segString.length() >= 4) { 605 result.append(segString); 606 result.append('\r'); 607 } 608 } 609 } 610 } 611 return result.toString(); 612 } 613 614 615 public static String encode(Segment source, EncodingCharacters encodingChars) { 616 StringBuffer result = new StringBuffer(); 617 result.append(source.getName()); 618 result.append(encodingChars.getFieldSeparator()); 619 620 // start at field 2 for MSH segment because field 1 is the field 621 // delimiter 622 int startAt = 1; 623 if (isDelimDefSegment(source.getName())) 624 startAt = 2; 625 626 // loop through fields; for every field delimit any repetitions and add 627 // field delimiter after ... 628 int numFields = source.numFields(); 629 for (int i = startAt; i <= numFields; i++) { 630 try { 631 Type[] reps = source.getField(i); 632 for (int j = 0; j < reps.length; j++) { 633 String fieldText = encode(reps[j], encodingChars); 634 // if this is MSH-2, then it shouldn't be escaped, so 635 // unescape it again 636 if (isDelimDefSegment(source.getName()) && i == 2) 637 fieldText = Escape.unescape(fieldText, encodingChars); 638 result.append(fieldText); 639 if (j < reps.length - 1) 640 result.append(encodingChars.getRepetitionSeparator()); 641 } 642 } catch (HL7Exception e) { 643 log.error("Error while encoding segment: ", e); 644 } 645 result.append(encodingChars.getFieldSeparator()); 646 } 647 648 // strip trailing delimiters ... 649 return stripExtraDelimiters(result.toString(), encodingChars.getFieldSeparator()); 650 } 651 652 653 /** 654 * Removes leading whitespace from the given string. This method was created 655 * to deal with frequent problems parsing messages that have been 656 * hand-written in windows. The intuitive way to delimit segments is to hit 657 * <ENTER> at the end of each segment, but this creates both a carriage 658 * return and a line feed, so to the parser, the first character of the next 659 * segment is the line feed. 660 */ 661 public static String stripLeadingWhitespace(String in) { 662 StringBuffer out = new StringBuffer(); 663 char[] chars = in.toCharArray(); 664 int c = 0; 665 while (c < chars.length) { 666 if (!Character.isWhitespace(chars[c])) 667 break; 668 c++; 669 } 670 for (int i = c; i < chars.length; i++) { 671 out.append(chars[i]); 672 } 673 return out.toString(); 674 } 675 676 677 /** 678 * <p> 679 * Returns a minimal amount of data from a message string, including only 680 * the data needed to send a response to the remote system. This includes 681 * the following fields: 682 * <ul> 683 * <li>field separator</li> 684 * <li>encoding characters</li> 685 * <li>processing ID</li> 686 * <li>message control ID</li> 687 * </ul> 688 * This method is intended for use when there is an error parsing a message, 689 * (so the Message object is unavailable) but an error message must be sent 690 * back to the remote system including some of the information in the 691 * inbound message. This method parses only that required information, 692 * hopefully avoiding the condition that caused the original error. The 693 * other fields in the returned MSH segment are empty. 694 * </p> 695 */ 696 public Segment getCriticalResponseData(String message) throws HL7Exception { 697 // try to get MSH segment 698 int locStartMSH = message.indexOf("MSH"); 699 if (locStartMSH < 0) 700 throw new HL7Exception("Couldn't find MSH segment in message: " + message, HL7Exception.SEGMENT_SEQUENCE_ERROR); 701 int locEndMSH = message.indexOf('\r', locStartMSH + 1); 702 if (locEndMSH < 0) 703 locEndMSH = message.length(); 704 String mshString = message.substring(locStartMSH, locEndMSH); 705 706 // find out what the field separator is 707 char fieldSep = mshString.charAt(3); 708 709 // get field array 710 String[] fields = split(mshString, String.valueOf(fieldSep)); 711 712 Segment msh = null; 713 try { 714 // parse required fields 715 String encChars = fields[1]; 716 char compSep = encChars.charAt(0); 717 String messControlID = fields[9]; 718 String[] procIDComps = split(fields[10], String.valueOf(compSep)); 719 720 // fill MSH segment 721 String version = "2.4"; // default 722 try { 723 version = this.getVersion(message); 724 } catch (Exception e) { /* use the default */ 725 } 726 727 msh = Parser.makeControlMSH(version, getFactory()); 728 Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep)); 729 Terser.set(msh, 2, 0, 1, 1, encChars); 730 Terser.set(msh, 10, 0, 1, 1, messControlID); 731 Terser.set(msh, 11, 0, 1, 1, procIDComps[0]); 732 Terser.set(msh, 12, 0, 1, 1, version); 733 734 } catch (Exception e) { 735 throw new HL7Exception("Can't parse critical fields from MSH segment (" + e.getClass().getName() + ": " + e.getMessage() + "): " + mshString, HL7Exception.REQUIRED_FIELD_MISSING, e); 736 } 737 738 return msh; 739 } 740 741 742 /** 743 * For response messages, returns the value of MSA-2 (the message ID of the 744 * message sent by the sending system). This value may be needed prior to 745 * main message parsing, so that (particularly in a multi-threaded scenario) 746 * the message can be routed to the thread that sent the request. We need 747 * this information first so that any parse exceptions are thrown to the 748 * correct thread. Returns null if MSA-2 can not be found (e.g. if the 749 * message is not a response message). 750 */ 751 public String getAckID(String message) { 752 String ackID = null; 753 int startMSA = message.indexOf("\rMSA"); 754 if (startMSA >= 0) { 755 int startFieldOne = startMSA + 5; 756 char fieldDelim = message.charAt(startFieldOne - 1); 757 int start = message.indexOf(fieldDelim, startFieldOne) + 1; 758 int end = message.indexOf(fieldDelim, start); 759 int segEnd = message.indexOf(String.valueOf(segDelim), start); 760 if (segEnd > start && segEnd < end) 761 end = segEnd; 762 763 // if there is no field delim after MSH-2, need to go to end of 764 // message, but not including end seg delim if it exists 765 if (end < 0) { 766 if (message.charAt(message.length() - 1) == '\r') { 767 end = message.length() - 1; 768 } else { 769 end = message.length(); 770 } 771 } 772 if (start > 0 && end > start) { 773 ackID = message.substring(start, end); 774 } 775 } 776 log.debug("ACK ID: " + ackID); 777 return ackID; 778 } 779 780 781 /** 782 * Defaults to <code>false</code> 783 * 784 * @see #isLegacyMode() 785 */ 786 public void setLegacyMode(boolean legacyMode) { 787 this.myLegacyMode = legacyMode; 788 } 789 790 791 /** 792 * {@inheritDoc } 793 */ 794 @Override 795 public String encode(Message source) throws HL7Exception { 796 if (myLegacyMode != null && myLegacyMode) { 797 798 @SuppressWarnings("deprecation") 799 OldPipeParser oldPipeParser = new OldPipeParser(getFactory()); 800 801 return oldPipeParser.encode(source); 802 } 803 return super.encode(source); 804 } 805 806 807 /** 808 * {@inheritDoc } 809 */ 810 @Override 811 public Message parse(String message) throws HL7Exception, EncodingNotSupportedException { 812 if (myLegacyMode != null && myLegacyMode) { 813 814 @SuppressWarnings("deprecation") 815 OldPipeParser oldPipeParser = new OldPipeParser(getFactory()); 816 817 return oldPipeParser.parse(message); 818 } 819 return super.parse(message); 820 } 821 822 823 /** 824 * <p> 825 * Returns <code>true</code> if legacy mode is on. 826 * </p> 827 * <p> 828 * Prior to release 1.0, when an unexpected segment was encountered in a 829 * message, HAPI would recurse to the deepest nesting in the last group it 830 * encountered after the current position in the message, and deposit the 831 * segment there. This could lead to unusual behaviour where all segments 832 * afterward would not be in an expected spot within the message. 833 * </p> 834 * <p> 835 * This should normally be set to false, but any code written before the 836 * release of HAPI 1.0 which depended on this behaviour might need legacy 837 * mode to be set to true. 838 * </p> 839 * <p> 840 * Defaults to <code>false</code>. Note that this method only overrides 841 * behaviour of the {@link #parse(java.lang.String)} and 842 * {@link #encode(ca.uhn.hl7v2.model.Message) } methods 843 * </p> 844 */ 845 public boolean isLegacyMode() { 846 if (myLegacyMode == null) { 847 if (Boolean.parseBoolean(System.getProperty(DEFAULT_LEGACY_MODE_PROPERTY))) { 848 return true; 849 } else { 850 return false; 851 } 852 } 853 return this.myLegacyMode; 854 } 855 856 857 /** 858 * Returns the version ID (MSH-12) from the given message, without fully 859 * parsing the message. The version is needed prior to parsing in order to 860 * determine the message class into which the text of the message should be 861 * parsed. 862 * 863 * @throws HL7Exception 864 * if the version field can not be found. 865 */ 866 public String getVersion(String message) throws HL7Exception { 867 int startMSH = message.indexOf("MSH"); 868 int endMSH = message.indexOf(PipeParser.segDelim, startMSH); 869 if (endMSH < 0) 870 endMSH = message.length(); 871 String msh = message.substring(startMSH, endMSH); 872 String fieldSep = null; 873 if (msh.length() > 3) { 874 fieldSep = String.valueOf(msh.charAt(3)); 875 } else { 876 throw new HL7Exception("Can't find field separator in MSH: " + msh, HL7Exception.UNSUPPORTED_VERSION_ID); 877 } 878 879 String[] fields = split(msh, fieldSep); 880 881 String compSep = null; 882 if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) { 883 compSep = String.valueOf(fields[1].charAt(0)); // get component 884 // separator as 1st 885 // encoding char 886 } else { 887 throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1], HL7Exception.REQUIRED_FIELD_MISSING); 888 } 889 890 String version = null; 891 if (fields.length >= 12) { 892 String[] comp = split(fields[11], compSep); 893 if (comp.length >= 1) { 894 version = comp[0]; 895 } else { 896 throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11], HL7Exception.REQUIRED_FIELD_MISSING); 897 } 898 } else { 899 throw new HL7Exception("Can't find version ID - MSH has only " + fields.length + " fields.", HL7Exception.REQUIRED_FIELD_MISSING); 900 } 901 return version; 902 } 903 904 905 @Override 906 public void parse(Message message, String string) throws HL7Exception { 907 IStructureDefinition structureDef = getStructureDefinition(message.getClass()); 908 909 // MessagePointer ptr = new MessagePointer(this, m, 910 // getEncodingChars(message)); 911 MessageIterator messageIter = new MessageIterator(message, structureDef, "MSH", true); 912 913 String[] segments = split(string, segDelim); 914 915 char delim = '|'; 916 for (int i = 0; i < segments.length; i++) { 917 918 // get rid of any leading whitespace characters ... 919 if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0))) 920 segments[i] = stripLeadingWhitespace(segments[i]); 921 922 // sometimes people put extra segment delimiters at end of msg ... 923 if (segments[i] != null && segments[i].length() >= 3) { 924 final String name; 925 if (i == 0) { 926 name = segments[i].substring(0, 3); 927 delim = segments[i].charAt(3); 928 } else { 929 if (segments[i].indexOf(delim) >= 0) { 930 name = segments[i].substring(0, segments[i].indexOf(delim)); 931 } else { 932 name = segments[i]; 933 } 934 } 935 936 log.debug("Parsing segment " + name); 937 938 messageIter.setDirection(name); 939 940 if (messageIter.hasNext()) { 941 Segment next = (Segment) messageIter.next(); 942 int nextIndexWithinParent = messageIter.getNextIndexWithinParent(); 943 parse(next, segments[i], getEncodingChars(string), nextIndexWithinParent); 944 } 945 } 946 } 947 } 948 949 /** 950 * A struct for holding a message class string and a boolean indicating 951 * whether it was defined explicitly. 952 */ 953 private static class MessageStructure { 954 public String messageStructure; 955 public boolean explicitlyDefined; 956 957 958 public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) { 959 messageStructure = theMessageStructure; 960 explicitlyDefined = isExplicitlyDefined; 961 } 962 } 963 964 private static class Holder<T> { 965 private T myObject; 966 967 968 public T getObject() { 969 return myObject; 970 } 971 972 973 public void setObject(T theObject) { 974 myObject = theObject; 975 } 976 } 977 978 }