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 028package ca.uhn.hl7v2.parser; 029 030import java.util.ArrayList; 031import java.util.HashMap; 032import java.util.StringTokenizer; 033 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036 037import ca.uhn.hl7v2.HL7Exception; 038import ca.uhn.hl7v2.model.DoNotCacheStructure; 039import ca.uhn.hl7v2.model.Group; 040import ca.uhn.hl7v2.model.Message; 041import ca.uhn.hl7v2.model.Primitive; 042import ca.uhn.hl7v2.model.Segment; 043import ca.uhn.hl7v2.model.Structure; 044import ca.uhn.hl7v2.model.Type; 045import ca.uhn.hl7v2.model.Varies; 046import ca.uhn.hl7v2.util.ReflectionUtil; 047import ca.uhn.hl7v2.util.Terser; 048import ca.uhn.hl7v2.validation.impl.NoValidation; 049import ca.uhn.hl7v2.validation.impl.ValidationContextFactory; 050 051/** 052 * An implementation of Parser that supports traditionally encoded (ie delimited 053 * with characters like |, ^, and ~) HL7 messages. Unexpected segments and 054 * fields are parsed into generic elements that are added to the message. 055 * 056 * @author Bryan Tripp (bryan_tripp@sourceforge.net) 057 */ 058public class PipeParser extends Parser { 059 060 private static final Logger log = LoggerFactory.getLogger(PipeParser.class); 061 062 /** 063 * The HL7 ER7 segment delimiter (see section 2.8 of spec) 064 */ 065 final static String SEGMENT_DELIMITER = "\r"; 066 067 private final HashMap<Class<? extends Message>, StructureDefinition> myStructureDefinitions = new HashMap<Class<? extends Message>, StructureDefinition>(); 068 069 /** 070 * System property key. If value is "true", legacy mode will default to true 071 * 072 * @see #isLegacyMode() 073 * @deprecated This will be removed in HAPI 3.0 074 */ 075 public static final String DEFAULT_LEGACY_MODE_PROPERTY = "ca.uhn.hl7v2.parser.PipeParser.default_legacy_mode"; 076 077 private Boolean myLegacyMode = null; 078 079 080 /** Creates a new PipeParser */ 081 public PipeParser() { 082 this(null); 083 } 084 085 086 /** 087 * Creates a new PipeParser 088 * 089 * @param theFactory 090 * custom factory to use for model class lookup 091 */ 092 public PipeParser(ModelClassFactory theFactory) { 093 super(theFactory); 094 } 095 096 097 /** 098 * Returns a String representing the encoding of the given message, if the 099 * encoding is recognized. For example if the given message appears to be 100 * encoded using HL7 2.x XML rules then "XML" would be returned. If the 101 * encoding is not recognized then null is returned. That this method 102 * returns a specific encoding does not guarantee that the message is 103 * correctly encoded (e.g. well formed XML) - just that it is not encoded 104 * using any other encoding than the one returned. 105 */ 106 public String getEncoding(String message) { 107 if (EncodingDetector.isEr7Encoded(message)) { 108 return "VB"; 109 } 110 return null; 111 } 112 113 114 /** 115 * @return the preferred encoding of this Parser 116 */ 117 public String getDefaultEncoding() { 118 return "VB"; 119 } 120 121 122 /** 123 * Returns true if and only if the given encoding is supported by this 124 * Parser. 125 */ 126 public boolean supportsEncoding(String encoding) { 127 boolean supports = false; 128 if (encoding != null && encoding.equals("VB")) 129 supports = true; 130 return supports; 131 } 132 133 134 /** 135 * @deprecated this method should not be public 136 * @param message 137 * @return 138 * @throws HL7Exception 139 * @throws EncodingNotSupportedException 140 */ 141 public String getMessageStructure(String message) throws HL7Exception, EncodingNotSupportedException { 142 return getStructure(message).messageStructure; 143 } 144 145 146 /** 147 * @returns the message structure from MSH-9-3 148 */ 149 private MessageStructure getStructure(String message) throws HL7Exception, EncodingNotSupportedException { 150 EncodingCharacters ec = getEncodingChars(message); 151 String messageStructure = null; 152 boolean explicityDefined = true; 153 String wholeFieldNine; 154 try { 155 String[] fields = split(message.substring(0, Math.max(message.indexOf(SEGMENT_DELIMITER), message.length())), String.valueOf(ec.getFieldSeparator())); 156 wholeFieldNine = fields[8]; 157 158 // message structure is component 3 but we'll accept a composite of 159 // 1 and 2 if there is no component 3 ... 160 // if component 1 is ACK, then the structure is ACK regardless of 161 // component 2 162 String[] comps = split(wholeFieldNine, String.valueOf(ec.getComponentSeparator())); 163 if (comps.length >= 3) { 164 messageStructure = comps[2]; 165 } else if (comps.length > 0 && comps[0] != null && comps[0].equals("ACK")) { 166 messageStructure = "ACK"; 167 } else if (comps.length == 2) { 168 explicityDefined = false; 169 messageStructure = comps[0] + "_" + comps[1]; 170 } 171 /* 172 * else if (comps.length == 1 && comps[0] != null && 173 * comps[0].equals("ACK")) { messageStructure = "ACK"; //it's common 174 * for people to only populate component 1 in an ACK msg } 175 */ 176 else { 177 StringBuilder buf = new StringBuilder("Can't determine message structure from MSH-9: "); 178 buf.append(wholeFieldNine); 179 if (comps.length < 3) { 180 buf.append(" HINT: there are only "); 181 buf.append(comps.length); 182 buf.append(" of 3 components present"); 183 } 184 throw new HL7Exception(buf.toString(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE); 185 } 186 } catch (IndexOutOfBoundsException e) { 187 throw new HL7Exception("Can't find message structure (MSH-9-3): " + e.getMessage(), HL7Exception.UNSUPPORTED_MESSAGE_TYPE); 188 } 189 190 return new MessageStructure(messageStructure, explicityDefined); 191 } 192 193 194 /** 195 * Returns object that contains the field separator and encoding characters 196 * for this message. 197 * @throws HL7Exception 198 */ 199 private static EncodingCharacters getEncodingChars(String message) throws HL7Exception { 200 if (message.length() < 8) { 201 throw new HL7Exception("Invalid message content: \"" + message + "\""); 202 } 203 return new EncodingCharacters(message.charAt(3), message.substring(4, 8)); 204 } 205 206 207 /** 208 * Parses a message string and returns the corresponding Message object. 209 * Unexpected segments added at the end of their group. 210 * 211 * @throws HL7Exception 212 * if the message is not correctly formatted. 213 * @throws EncodingNotSupportedException 214 * if the message encoded is not supported by this parser. 215 */ 216 protected Message doParse(String message, String version) throws HL7Exception, EncodingNotSupportedException { 217 218 // try to instantiate a message object of the right class 219 MessageStructure structure = getStructure(message); 220 Message m = instantiateMessage(structure.messageStructure, version, structure.explicitlyDefined); 221 222 parse(m, message); 223 224 return m; 225 } 226 227 228 /** 229 * {@inheritDoc} 230 */ 231 protected Message doParseForSpecificPackage(String message, String version, String packageName) throws HL7Exception, EncodingNotSupportedException { 232 233 // try to instantiate a message object of the right class 234 MessageStructure structure = getStructure(message); 235 Message m = instantiateMessageInASpecificPackage(structure.messageStructure, version, structure.explicitlyDefined, packageName); 236 237 parse(m, message); 238 239 return m; 240 } 241 242 243 /** 244 * Generates (or returns the cached value of) the message 245 */ 246 private IStructureDefinition getStructureDefinition(Message theMessage) throws HL7Exception { 247 248 Class<? extends Message> clazz = theMessage.getClass(); 249 StructureDefinition retVal = myStructureDefinitions.get(clazz); 250 if (retVal != null) { 251 return retVal; 252 } 253 254 if (clazz.isAnnotationPresent(DoNotCacheStructure.class)) { 255 Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>(); 256 retVal = createStructureDefinition(theMessage, previousLeaf); 257 } else { 258 Message message = ReflectionUtil.instantiateMessage(clazz, getFactory()); 259 Holder<StructureDefinition> previousLeaf = new Holder<StructureDefinition>(); 260 retVal = createStructureDefinition(message, previousLeaf); 261 myStructureDefinitions.put(clazz, retVal); 262 } 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 337 // MSH-2 will get split incorrectly so we have to fudge it ... 338 boolean isMSH2 = isDelimDefSegment(destination.getName()) && i + fieldOffset == 2; 339 if (isMSH2) { 340 reps = new String[1]; 341 reps[0] = fields[i]; 342 } 343 344 for (int j = 0; j < reps.length; j++) { 345 try { 346 log.debug("Parsing field {} repetition {}", i + fieldOffset, j); 347 Type field = destination.getField(i + fieldOffset, j); 348 if (isMSH2) { 349 Terser.getPrimitive(field, 1, 1).setValue(reps[j]); 350 } else { 351 parse(field, reps[j], encodingChars); 352 } 353 } catch (HL7Exception e) { 354 // set the field location and throw again ... 355 e.setFieldPosition(i); 356 if (theRepetition != null && theRepetition > 1) { 357 e.setSegmentRepetition(theRepetition); 358 } 359 e.setSegmentName(destination.getName()); 360 throw e; 361 } 362 } 363 } 364 365 // set data type of OBX-5 366 if (destination.getClass().getName().indexOf("OBX") >= 0) { 367 Varies.fixOBX5(destination, getFactory()); 368 } 369 370 } 371 372 373 /** 374 * @return true if the segment is MSH, FHS, or BHS. These need special 375 * treatment because they define delimiters. 376 * @param theSegmentName 377 */ 378 private static boolean isDelimDefSegment(String theSegmentName) { 379 boolean is = false; 380 if (theSegmentName.equals("MSH") || theSegmentName.equals("FHS") || theSegmentName.equals("BHS")) { 381 is = true; 382 } 383 return is; 384 } 385 386 387 /** 388 * Fills a field with values from an unparsed string representing the field. 389 * 390 * @param destinationField 391 * the field Type 392 * @param data 393 * the field string (including all components and subcomponents; 394 * not including field delimiters) 395 * @param encodingCharacters 396 * the encoding characters used in the message 397 */ 398 @Override 399 public void parse(Type destinationField, String data, EncodingCharacters encodingCharacters) throws HL7Exception { 400 String[] components = split(data, String.valueOf(encodingCharacters.getComponentSeparator())); 401 for (int i = 0; i < components.length; i++) { 402 String[] subcomponents = split(components[i], String.valueOf(encodingCharacters.getSubcomponentSeparator())); 403 for (int j = 0; j < subcomponents.length; j++) { 404 String val = subcomponents[j]; 405 if (val != null) { 406 val = Escape.unescape(val, encodingCharacters); 407 } 408 Terser.getPrimitive(destinationField, i + 1, j + 1).setValue(val); 409 } 410 } 411 } 412 413 414 /** 415 * Splits the given composite string into an array of components using the 416 * given delimiter. 417 */ 418 public static String[] split(String composite, String delim) { 419 ArrayList<String> components = new ArrayList<String>(); 420 421 // defend against evil nulls 422 if (composite == null) 423 composite = ""; 424 if (delim == null) 425 delim = ""; 426 427 StringTokenizer tok = new StringTokenizer(composite, delim, true); 428 boolean previousTokenWasDelim = true; 429 while (tok.hasMoreTokens()) { 430 String thisTok = tok.nextToken(); 431 if (thisTok.equals(delim)) { 432 if (previousTokenWasDelim) 433 components.add(null); 434 previousTokenWasDelim = true; 435 } else { 436 components.add(thisTok); 437 previousTokenWasDelim = false; 438 } 439 } 440 441 String[] ret = new String[components.size()]; 442 for (int i = 0; i < components.size(); i++) { 443 ret[i] = (String) components.get(i); 444 } 445 446 return ret; 447 } 448 449 450 /** 451 * {@inheritDoc } 452 */ 453 @Override 454 public String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception { 455 return encode(structure, encodingCharacters); 456 } 457 458 459 /** 460 * {@inheritDoc } 461 */ 462 @Override 463 public String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception { 464 return encode(type, encodingCharacters); 465 } 466 467 468 /** 469 * Encodes the given Type, using the given encoding characters. It is 470 * assumed that the Type represents a complete field rather than a 471 * component. 472 */ 473 public static String encode(Type source, EncodingCharacters encodingChars) { 474 return encode(source, encodingChars, null, null); 475 } 476 477 478 private static String encode(Type source, EncodingCharacters encodingChars, ParserConfiguration parserConfig, String currentTerserPath) { 479 if (source instanceof Varies) { 480 Varies varies = (Varies) source; 481 if (varies.getData() != null) { 482 source = varies.getData(); 483 } 484 } 485 486 StringBuilder field = new StringBuilder(); 487 for (int i = 1; i <= Terser.numComponents(source); i++) { 488 StringBuilder comp = new StringBuilder(); 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 498 int forceUpToFieldNum = 0; 499 if (parserConfig != null && currentTerserPath != null) { 500 for (String nextPath : parserConfig.getForcedEncode()) { 501 if (nextPath.startsWith(currentTerserPath + "-") && nextPath.length() > currentTerserPath.length()) { 502 int endOfFieldDef = nextPath.indexOf('-', currentTerserPath.length()); 503 if (endOfFieldDef == -1) { 504 forceUpToFieldNum = 0; 505 break; 506 } 507 String fieldNumString = nextPath.substring(endOfFieldDef + 1, nextPath.length()); 508 if (fieldNumString.length() > 0) { 509 forceUpToFieldNum = Math.max(forceUpToFieldNum, Integer.parseInt(fieldNumString)); 510 } 511 } 512 } 513 } 514 515 char componentSeparator = encodingChars.getComponentSeparator(); 516 String retVal = stripExtraDelimiters(field.toString(), componentSeparator); 517 518 while (forceUpToFieldNum > 0 && (countInstancesOf(retVal, componentSeparator) + 1) < forceUpToFieldNum) { 519 retVal = retVal + componentSeparator; 520 } 521 522 return retVal; 523 } 524 525 526 private static String encodePrimitive(Primitive p, EncodingCharacters encodingChars) { 527 String val = (p).getValue(); 528 if (val == null) { 529 val = ""; 530 } else { 531 val = Escape.escape(val, encodingChars); 532 } 533 return val; 534 } 535 536 537 /** 538 * Removes unecessary delimiters from the end of a field or segment. This 539 * seems to be more convenient than checking to see if they are needed while 540 * we are building the encoded string. 541 */ 542 private static String stripExtraDelimiters(String in, char delim) { 543 char[] chars = in.toCharArray(); 544 545 // search from back end for first occurance of non-delimiter ... 546 int c = chars.length - 1; 547 boolean found = false; 548 while (c >= 0 && !found) { 549 if (chars[c--] != delim) 550 found = true; 551 } 552 553 String ret = ""; 554 if (found) 555 ret = String.valueOf(chars, 0, c + 2); 556 return ret; 557 } 558 559 560 /** 561 * Formats a Message object into an HL7 message string using the given 562 * encoding. 563 * 564 * @throws HL7Exception 565 * if the data fields in the message do not permit encoding 566 * (e.g. required fields are null) 567 * @throws EncodingNotSupportedException 568 * if the requested encoding is not supported by this parser. 569 */ 570 protected String doEncode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException { 571 if (!this.supportsEncoding(encoding)) 572 throw new EncodingNotSupportedException("This parser does not support the " + encoding + " encoding"); 573 574 return encode(source); 575 } 576 577 578 /** 579 * Formats a Message object into an HL7 message string using this parser's 580 * default encoding ("VB"). 581 * 582 * @throws HL7Exception 583 * if the data fields in the message do not permit encoding 584 * (e.g. required fields are null) 585 */ 586 protected String doEncode(Message source) throws HL7Exception { 587 // get encoding characters ... 588 Segment msh = (Segment) source.get("MSH"); 589 String fieldSepString = Terser.get(msh, 1, 0, 1, 1); 590 591 if (fieldSepString == null) 592 throw new HL7Exception("Can't encode message: MSH-1 (field separator) is missing"); 593 594 char fieldSep = '|'; 595 if (fieldSepString != null && fieldSepString.length() > 0) 596 fieldSep = fieldSepString.charAt(0); 597 598 String encCharString = Terser.get(msh, 2, 0, 1, 1); 599 600 if (encCharString == null) 601 throw new HL7Exception("Can't encode message: MSH-2 (encoding characters) is missing"); 602 603 if (encCharString.length() != 4) 604 throw new HL7Exception("Encoding characters (MSH-2) value '" + encCharString + "' invalid -- must be 4 characters", HL7Exception.DATA_TYPE_ERROR); 605 EncodingCharacters en = new EncodingCharacters(fieldSep, encCharString); 606 607 // pass down to group encoding method which will operate recursively on 608 // children ... 609 return encode((Group) source, en, getParserConfiguration(), ""); 610 } 611 612 613 /** 614 * Returns given group serialized as a pipe-encoded string - this method is 615 * called by encode(Message source, String encoding). 616 */ 617 public static String encode(Group source, EncodingCharacters encodingChars) throws HL7Exception { 618 return encode(source, encodingChars, source.getMessage().getParser().getParserConfiguration(), ""); 619 } 620 621 622 /** 623 * Returns given group serialized as a pipe-encoded string - this method is 624 * called by encode(Message source, String encoding). 625 */ 626 private static String encode(Group source, EncodingCharacters encodingChars, ParserConfiguration parserConfiguration, String currentTerserPath) throws HL7Exception { 627 StringBuilder result = new StringBuilder(); 628 629 String[] names = source.getNames(); 630 631 String firstMandatorySegmentName = null; 632 boolean haveEncounteredMandatorySegment = false; 633 boolean haveEncounteredContent = false; 634 boolean haveHadMandatorySegment = false; 635 boolean haveHadSegmentBeforeMandatorySegment = false; 636 637 for (int i = 0; i < names.length; i++) { 638 639 String nextName = names[i]; 640 source.get(nextName, 0); 641 Structure[] reps = source.getAll(nextName); 642 boolean nextNameIsRequired = source.isRequired(nextName); 643 644 boolean havePreviouslyEncounteredMandatorySegment = haveEncounteredMandatorySegment; 645 haveEncounteredMandatorySegment |= nextNameIsRequired; 646 if (nextNameIsRequired && !haveHadMandatorySegment) { 647 if (!source.isGroup(nextName)) { 648 firstMandatorySegmentName = nextName; 649 } 650 } 651 652 String nextTerserPath = currentTerserPath.length() > 0 ? currentTerserPath + "/" + nextName : nextName; 653 654 // Add all reps of the next segment/group 655 for (int rep = 0; rep < reps.length; rep++) { 656 657 if (reps[rep] instanceof Group) { 658 659 String encodedGroup = encode((Group) reps[rep], encodingChars, parserConfiguration, nextTerserPath); 660 result.append(encodedGroup); 661 662 if (encodedGroup.length() > 0) { 663 if (!haveHadMandatorySegment && !haveEncounteredMandatorySegment) { 664 haveHadSegmentBeforeMandatorySegment = true; 665 } 666 if (nextNameIsRequired && !haveHadMandatorySegment && !havePreviouslyEncounteredMandatorySegment) { 667 haveHadMandatorySegment = true; 668 } 669 haveEncounteredContent = true; 670 } 671 672 } else { 673 674 // Check if we are configured to force the encoding of this segment 675 boolean encodeEmptySegments = parserConfiguration.determineForcedEncodeIncludesTerserPath(nextTerserPath); 676 String segString = encode((Segment) reps[rep], encodingChars, parserConfiguration, nextTerserPath); 677 if (segString.length() >= 4 || encodeEmptySegments) { 678 result.append(segString); 679 680 if (segString.length() == 3) { 681 result.append(encodingChars.getFieldSeparator()); 682 } 683 684 result.append(SEGMENT_DELIMITER); 685 686 haveEncounteredContent = true; 687 688 if (nextNameIsRequired) { 689 haveHadMandatorySegment = true; 690 } 691 692 if (!haveHadMandatorySegment && !haveEncounteredMandatorySegment) { 693 haveHadSegmentBeforeMandatorySegment = true; 694 } 695 696 } 697 698 } 699 700 } 701 702 } 703 704 if (firstMandatorySegmentName != null && !haveHadMandatorySegment && 705 !haveHadSegmentBeforeMandatorySegment && haveEncounteredContent && 706 parserConfiguration.isEncodeEmptyMandatorySegments()) { 707 return firstMandatorySegmentName.substring(0, 3) + encodingChars.getFieldSeparator() + SEGMENT_DELIMITER + result; 708 } else { 709 return result.toString(); 710 } 711 } 712 713 /** 714 * Convenience factory method which returns an instance that has a 715 * {@link NoValidation NoValidation validation context}. 716 */ 717 public static PipeParser getInstanceWithNoValidation() { 718 PipeParser retVal = new PipeParser(); 719 retVal.setValidationContext(ValidationContextFactory.noValidation()); 720 return retVal; 721 } 722 723 public static String encode(Segment source, EncodingCharacters encodingChars) { 724 return encode(source, encodingChars, null, null); 725 } 726 727 728 private static String encode(Segment source, EncodingCharacters encodingChars, ParserConfiguration parserConfig, String currentTerserPath) { 729 StringBuilder result = new StringBuilder(); 730 result.append(source.getName()); 731 result.append(encodingChars.getFieldSeparator()); 732 733 // start at field 2 for MSH segment because field 1 is the field 734 // delimiter 735 int startAt = 1; 736 if (isDelimDefSegment(source.getName())) 737 startAt = 2; 738 739 // loop through fields; for every field delimit any repetitions and add 740 // field delimiter after ... 741 int numFields = source.numFields(); 742 743 int forceUpToFieldNum = 0; 744 if (parserConfig != null && currentTerserPath != null) { 745 forceUpToFieldNum = parserConfig.determineForcedFieldNumForTerserPath(currentTerserPath); 746 } 747 numFields = Math.max(numFields, forceUpToFieldNum); 748 749 for (int i = startAt; i <= numFields; i++) { 750 751 String nextFieldTerserPath = currentTerserPath + "-" + i; 752 if (parserConfig != null && currentTerserPath != null) { 753 for (String nextPath : parserConfig.getForcedEncode()) { 754 if (nextPath.startsWith(nextFieldTerserPath + "-")) { 755 try { 756 source.getField(i, 0); 757 } catch (HL7Exception e) { 758 log.error("Error while encoding segment: ", e); 759 } 760 } 761 } 762 } 763 764 try { 765 Type[] reps = source.getField(i); 766 for (int j = 0; j < reps.length; j++) { 767 String fieldText = encode(reps[j], encodingChars, parserConfig, nextFieldTerserPath); 768 // if this is MSH-2, then it shouldn't be escaped, so 769 // unescape it again 770 if (isDelimDefSegment(source.getName()) && i == 2) 771 fieldText = Escape.unescape(fieldText, encodingChars); 772 result.append(fieldText); 773 if (j < reps.length - 1) 774 result.append(encodingChars.getRepetitionSeparator()); 775 } 776 } catch (HL7Exception e) { 777 log.error("Error while encoding segment: ", e); 778 } 779 result.append(encodingChars.getFieldSeparator()); 780 } 781 782 // strip trailing delimiters ... 783 char fieldSeparator = encodingChars.getFieldSeparator(); 784 String retVal = stripExtraDelimiters(result.toString(), fieldSeparator); 785 786 int offset = isDelimDefSegment(source.getName()) ? 1 : 0; 787 while (forceUpToFieldNum > 0 && (countInstancesOf(retVal, fieldSeparator) + offset) < forceUpToFieldNum) { 788 retVal = retVal + fieldSeparator; 789 } 790 791 return retVal; 792 } 793 794 795 private static int countInstancesOf(String theString, char theCharToSearchFor) { 796 int retVal = 0; 797 for (int i = 0; i < theString.length(); i++) { 798 if (theString.charAt(i) == theCharToSearchFor) { 799 retVal++; 800 } 801 } 802 return retVal; 803 } 804 805 806 /** 807 * Removes leading whitespace from the given string. This method was created 808 * to deal with frequent problems parsing messages that have been 809 * hand-written in windows. The intuitive way to delimit segments is to hit 810 * <ENTER> at the end of each segment, but this creates both a carriage 811 * return and a line feed, so to the parser, the first character of the next 812 * segment is the line feed. 813 */ 814 public static String stripLeadingWhitespace(String in) { 815 StringBuilder out = new StringBuilder(); 816 char[] chars = in.toCharArray(); 817 int c = 0; 818 while (c < chars.length) { 819 if (!Character.isWhitespace(chars[c])) 820 break; 821 c++; 822 } 823 for (int i = c; i < chars.length; i++) { 824 out.append(chars[i]); 825 } 826 return out.toString(); 827 } 828 829 830 /** 831 * <p> 832 * Returns a minimal amount of data from a message string, including only 833 * the data needed to send a response to the remote system. This includes 834 * the following fields: 835 * <ul> 836 * <li>field separator</li> 837 * <li>encoding characters</li> 838 * <li>processing ID</li> 839 * <li>message control ID</li> 840 * </ul> 841 * This method is intended for use when there is an error parsing a message, 842 * (so the Message object is unavailable) but an error message must be sent 843 * back to the remote system including some of the information in the 844 * inbound message. This method parses only that required information, 845 * hopefully avoiding the condition that caused the original error. The 846 * other fields in the returned MSH segment are empty. 847 * </p> 848 */ 849 public Segment getCriticalResponseData(String message) throws HL7Exception { 850 // try to get MSH segment 851 int locStartMSH = message.indexOf("MSH"); 852 if (locStartMSH < 0) 853 throw new HL7Exception("Couldn't find MSH segment in message: " + message, HL7Exception.SEGMENT_SEQUENCE_ERROR); 854 int locEndMSH = message.indexOf('\r', locStartMSH + 1); 855 if (locEndMSH < 0) 856 locEndMSH = message.length(); 857 String mshString = message.substring(locStartMSH, locEndMSH); 858 859 // find out what the field separator is 860 char fieldSep = mshString.charAt(3); 861 862 // get field array 863 String[] fields = split(mshString, String.valueOf(fieldSep)); 864 865 Segment msh = null; 866 try { 867 // parse required fields 868 String encChars = fields[1]; 869 char compSep = encChars.charAt(0); 870 String messControlID = fields[9]; 871 String[] procIDComps = split(fields[10], String.valueOf(compSep)); 872 873 // fill MSH segment 874 String version = "2.4"; // default 875 try { 876 version = this.getVersion(message); 877 } catch (Exception e) { /* use the default */ 878 } 879 880 msh = Parser.makeControlMSH(version, getFactory()); 881 Terser.set(msh, 1, 0, 1, 1, String.valueOf(fieldSep)); 882 Terser.set(msh, 2, 0, 1, 1, encChars); 883 Terser.set(msh, 10, 0, 1, 1, messControlID); 884 Terser.set(msh, 11, 0, 1, 1, procIDComps[0]); 885 Terser.set(msh, 12, 0, 1, 1, version); 886 887 } catch (Exception e) { 888 throw new HL7Exception("Can't parse critical fields from MSH segment (" + e.getClass().getName() + ": " + e.getMessage() + "): " + mshString, HL7Exception.REQUIRED_FIELD_MISSING, e); 889 } 890 891 return msh; 892 } 893 894 895 /** 896 * For response messages, returns the value of MSA-2 (the message ID of the 897 * message sent by the sending system). This value may be needed prior to 898 * main message parsing, so that (particularly in a multi-threaded scenario) 899 * the message can be routed to the thread that sent the request. We need 900 * this information first so that any parse exceptions are thrown to the 901 * correct thread. Returns null if MSA-2 can not be found (e.g. if the 902 * message is not a response message). 903 */ 904 public String getAckID(String message) { 905 String ackID = null; 906 int startMSA = message.indexOf("\rMSA"); 907 if (startMSA >= 0) { 908 int startFieldOne = startMSA + 5; 909 char fieldDelim = message.charAt(startFieldOne - 1); 910 int start = message.indexOf(fieldDelim, startFieldOne) + 1; 911 int end = message.indexOf(fieldDelim, start); 912 int segEnd = message.indexOf(String.valueOf(SEGMENT_DELIMITER), start); 913 if (segEnd > start && segEnd < end) 914 end = segEnd; 915 916 // if there is no field delim after MSH-2, need to go to end of 917 // message, but not including end seg delim if it exists 918 if (end < 0) { 919 if (message.charAt(message.length() - 1) == '\r') { 920 end = message.length() - 1; 921 } else { 922 end = message.length(); 923 } 924 } 925 if (start > 0 && end > start) { 926 ackID = message.substring(start, end); 927 } 928 } 929 log.debug("ACK ID: {}", ackID); 930 return ackID; 931 } 932 933 934 /** 935 * Defaults to <code>false</code> 936 * 937 * @see #isLegacyMode() 938 * @deprecated This will be removed in HAPI 3.0 939 */ 940 public void setLegacyMode(boolean legacyMode) { 941 this.myLegacyMode = legacyMode; 942 } 943 944 945 /** 946 * {@inheritDoc } 947 */ 948 @Override 949 public String encode(Message source) throws HL7Exception { 950 if (myLegacyMode != null && myLegacyMode) { 951 952 @SuppressWarnings("deprecation") 953 OldPipeParser oldPipeParser = new OldPipeParser(getFactory()); 954 955 return oldPipeParser.encode(source); 956 } 957 return super.encode(source); 958 } 959 960 961 /** 962 * {@inheritDoc } 963 */ 964 @Override 965 public Message parse(String message) throws HL7Exception, EncodingNotSupportedException { 966 if (myLegacyMode != null && myLegacyMode) { 967 968 @SuppressWarnings("deprecation") 969 OldPipeParser oldPipeParser = new OldPipeParser(getFactory()); 970 971 return oldPipeParser.parse(message); 972 } 973 return super.parse(message); 974 } 975 976 977 /** 978 * <p> 979 * Returns <code>true</code> if legacy mode is on. 980 * </p> 981 * <p> 982 * Prior to release 1.0, when an unexpected segment was encountered in a 983 * message, HAPI would recurse to the deepest nesting in the last group it 984 * encountered after the current position in the message, and deposit the 985 * segment there. This could lead to unusual behaviour where all segments 986 * afterward would not be in an expected spot within the message. 987 * </p> 988 * <p> 989 * This should normally be set to false, but any code written before the 990 * release of HAPI 1.0 which depended on this behaviour might need legacy 991 * mode to be set to true. 992 * </p> 993 * <p> 994 * Defaults to <code>false</code>. Note that this method only overrides 995 * behaviour of the {@link #parse(java.lang.String)} and 996 * {@link #encode(ca.uhn.hl7v2.model.Message) } methods 997 * </p> 998 * 999 * @deprecated This will be removed in HAPI 3.0 1000 */ 1001 public boolean isLegacyMode() { 1002 if (myLegacyMode == null) { 1003 if (Boolean.parseBoolean(System.getProperty(DEFAULT_LEGACY_MODE_PROPERTY))) { 1004 return true; 1005 } else { 1006 return false; 1007 } 1008 } 1009 return this.myLegacyMode; 1010 } 1011 1012 1013 /** 1014 * Returns the version ID (MSH-12) from the given message, without fully 1015 * parsing the message. The version is needed prior to parsing in order to 1016 * determine the message class into which the text of the message should be 1017 * parsed. 1018 * 1019 * @throws HL7Exception 1020 * if the version field can not be found. 1021 */ 1022 public String getVersion(String message) throws HL7Exception { 1023 int startMSH = message.indexOf("MSH"); 1024 int endMSH = message.indexOf(PipeParser.SEGMENT_DELIMITER, startMSH); 1025 if (endMSH < 0) 1026 endMSH = message.length(); 1027 String msh = message.substring(startMSH, endMSH); 1028 String fieldSep = null; 1029 if (msh.length() > 3) { 1030 fieldSep = String.valueOf(msh.charAt(3)); 1031 } else { 1032 throw new HL7Exception("Can't find field separator in MSH: " + msh, HL7Exception.UNSUPPORTED_VERSION_ID); 1033 } 1034 1035 String[] fields = split(msh, fieldSep); 1036 1037 String compSep = null; 1038 if (fields.length >= 2 && fields[1] != null && fields[1].length() == 4) { 1039 compSep = String.valueOf(fields[1].charAt(0)); // get component 1040 // separator as 1st 1041 // encoding char 1042 } else { 1043 throw new HL7Exception("Invalid or incomplete encoding characters - MSH-2 is " + fields[1], HL7Exception.REQUIRED_FIELD_MISSING); 1044 } 1045 1046 String version = null; 1047 if (fields.length >= 12) { 1048 String[] comp = split(fields[11], compSep); 1049 if (comp.length >= 1) { 1050 version = comp[0]; 1051 } else { 1052 throw new HL7Exception("Can't find version ID - MSH.12 is " + fields[11], HL7Exception.REQUIRED_FIELD_MISSING); 1053 } 1054 } else { 1055 throw new HL7Exception("Can't find version ID - MSH has only " + fields.length + " fields.", HL7Exception.REQUIRED_FIELD_MISSING); 1056 } 1057 return version; 1058 } 1059 1060 1061 @Override 1062 public void parse(Message message, String string) throws HL7Exception { 1063 IStructureDefinition structureDef = getStructureDefinition(message); 1064 1065 // MessagePointer ptr = new MessagePointer(this, m, 1066 // getEncodingChars(message)); 1067 MessageIterator messageIter = new MessageIterator(message, structureDef, "MSH", true); 1068 1069 String[] segments = split(string, SEGMENT_DELIMITER); 1070 1071 if (segments.length == 0) { 1072 throw new HL7Exception("Invalid message content: \"" + string + "\""); 1073 } 1074 1075 if (segments[0] == null || segments[0].length() < 4) { 1076 throw new HL7Exception("Invalid message content: \"" + string + "\""); 1077 } 1078 1079 char delim = '|'; 1080 String prevName = null; 1081 int repNum = 1; 1082 for (int i = 0; i < segments.length; i++) { 1083 1084 // get rid of any leading whitespace characters ... 1085 if (segments[i] != null && segments[i].length() > 0 && Character.isWhitespace(segments[i].charAt(0))) 1086 segments[i] = stripLeadingWhitespace(segments[i]); 1087 1088 // sometimes people put extra segment delimiters at end of msg ... 1089 if (segments[i] != null && segments[i].length() >= 3) { 1090 1091 final String name; 1092 if (i == 0) { 1093 if (segments[i].length() < 4) { 1094 throw new HL7Exception("Invalid message content: \"" + string + "\""); 1095 } 1096 name = segments[i].substring(0, 3); 1097 delim = segments[i].charAt(3); 1098 } else { 1099 if (segments[i].indexOf(delim) >= 0) { 1100 name = segments[i].substring(0, segments[i].indexOf(delim)); 1101 } else { 1102 name = segments[i]; 1103 } 1104 } 1105 1106 log.debug("Parsing segment {}", name); 1107 1108 if (name.equals(prevName)) { 1109 repNum++; 1110 } else { 1111 repNum = 1; 1112 prevName = name; 1113 } 1114 1115 messageIter.setDirection(name); 1116 1117 if (messageIter.hasNext()) { 1118 Segment next = (Segment) messageIter.next(); 1119 parse(next, segments[i], getEncodingChars(string), repNum); 1120 } 1121 } 1122 } 1123 } 1124 1125 1126 /** 1127 * A struct for holding a message class string and a boolean indicating 1128 * whether it was defined explicitly. 1129 */ 1130 private static class MessageStructure { 1131 public String messageStructure; 1132 public boolean explicitlyDefined; 1133 1134 1135 public MessageStructure(String theMessageStructure, boolean isExplicitlyDefined) { 1136 messageStructure = theMessageStructure; 1137 explicitlyDefined = isExplicitlyDefined; 1138 } 1139 } 1140 1141 private static class Holder<T> { 1142 private T myObject; 1143 1144 1145 public T getObject() { 1146 return myObject; 1147 } 1148 1149 1150 public void setObject(T theObject) { 1151 myObject = theObject; 1152 } 1153 } 1154 1155}