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