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