001/** 002The 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. 004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005Software distributed under the License is distributed on an "AS IS" basis, 006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007specific language governing rights and limitations under the License. 008 009The Original Code is "Parser.java". Description: 010"Parses HL7 message Strings into HL7 Message objects and 011 encodes HL7 Message objects into HL7 message Strings" 012 013The Initial Developer of the Original Code is University Health Network. Copyright (C) 0142001. All Rights Reserved. 015 016Contributor(s): ______________________________________. 017 018Alternatively, the contents of this file may be used under the terms of the 019GNU General Public License (the "GPL"), in which case the provisions of the GPL are 020applicable instead of those above. If you wish to allow use of your version of this 021file only under the terms of the GPL and not to allow others to use your version 022of this file under the MPL, indicate your decision by deleting the provisions above 023and replace them with the notice and other provisions required by the GPL License. 024If you do not delete the provisions above, a recipient may use your version of 025this file under either the MPL or the GPL. 026*/ 027 028package ca.uhn.hl7v2.parser; 029 030import java.lang.reflect.Constructor; 031 032import org.slf4j.Logger; 033import org.slf4j.LoggerFactory; 034 035import ca.uhn.hl7v2.DefaultHapiContext; 036import ca.uhn.hl7v2.ErrorCode; 037import ca.uhn.hl7v2.HL7Exception; 038import ca.uhn.hl7v2.HapiContext; 039import ca.uhn.hl7v2.HapiContextSupport; 040import ca.uhn.hl7v2.Version; 041import ca.uhn.hl7v2.model.AbstractSuperMessage; 042import ca.uhn.hl7v2.model.GenericMessage; 043import ca.uhn.hl7v2.model.GenericSegment; 044import ca.uhn.hl7v2.model.Group; 045import ca.uhn.hl7v2.model.Message; 046import ca.uhn.hl7v2.model.Segment; 047import ca.uhn.hl7v2.model.Type; 048import ca.uhn.hl7v2.util.ReflectionUtil; 049import ca.uhn.hl7v2.util.StringUtil; 050import ca.uhn.hl7v2.util.Terser; 051import ca.uhn.hl7v2.validation.ValidationContext; 052import ca.uhn.hl7v2.validation.ValidationExceptionHandler; 053import ca.uhn.hl7v2.validation.ValidationExceptionHandlerFactory; 054import ca.uhn.hl7v2.validation.Validator; 055 056/** 057 * Parses HL7 message Strings into HL7 Message objects and encodes HL7 Message objects into HL7 058 * message Strings. 059 * 060 * @author Bryan Tripp (bryan_tripp@sourceforge.net) 061 * @author Christian Ohr 062 */ 063public abstract class Parser extends HapiContextSupport { 064 065 private static final Logger log = LoggerFactory.getLogger(Parser.class); 066 067 /** 068 * Uses DefaultModelClassFactory for model class lookup. 069 */ 070 public Parser() { 071 this(new DefaultHapiContext()); 072 } 073 074 /** 075 * Creates a new parser, using the {@link ModelClassFactory}, the {@link ParserConfiguration} 076 * and the {@link ValidationContext} as defined in the context. 077 * 078 * @param context HapiContext 079 */ 080 public Parser(HapiContext context) { 081 super(context); 082 } 083 084 /** 085 * Initialize parser with custom ModelClassFactory and default ValidationContext 086 * 087 * @param modelClassFactory custom factory to use for model class lookup 088 */ 089 public Parser(ModelClassFactory modelClassFactory) { 090 this(new DefaultHapiContext(modelClassFactory)); 091 } 092 093 /** 094 * @return the factory used by this Parser for model class lookup 095 */ 096 public ModelClassFactory getFactory() { 097 return getHapiContext().getModelClassFactory(); 098 } 099 100 /** 101 * @return the set of validation rules that is applied to messages parsed or encoded by this 102 * parser. Note that this method may return <code>null</code> 103 */ 104 public ValidationContext getValidationContext() { 105 return isValidating() ? getHapiContext().getValidationContext() : null; 106 } 107 108 /** 109 * @param context the set of validation rules to be applied to messages parsed or encoded by 110 * this parser (defaults to ValidationContextFactory.DefaultValidation) 111 * 112 * @deprecated use a dedicated {@link HapiContext} and set its ValidationContext property 113 */ 114 public void setValidationContext(ValidationContext context) { 115 HapiContext newContext = new DefaultHapiContext(getHapiContext()); 116 newContext.setValidationContext(context); 117 setHapiContext(newContext); 118 } 119 120 /** 121 * <p> 122 * Returns the parser configuration. This is a bean which contains configuration 123 * instructions relating to how a parser should be parsing or encoding messages it deals 124 * with. 125 * </p> 126 * <p> 127 * <b>Note that the parser configuration comes from the {@link #getHapiContext() HAPI Context}.</b> 128 * Changes to the configuration for one parser will affect all parsers which share the same 129 * context. 130 * </p> 131 * 132 * @return the current parser configuration 133 */ 134 public ParserConfiguration getParserConfiguration() { 135 return getHapiContext().getParserConfiguration(); 136 } 137 138 /** 139 * Sets the parser configuration for this parser (may not be null). This is a bean which 140 * contains configuration instructions relating to how a parser should be parsing or encoding 141 * messages it deals with. 142 * 143 * @param configuration The parser configuration 144 * 145 * @deprecated use a dedicated {@link HapiContext} and set its ParserConfiguration property 146 */ 147 public void setParserConfiguration(ParserConfiguration configuration) { 148 HapiContext newContext = new DefaultHapiContext(getHapiContext()); 149 newContext.setParserConfiguration(configuration); 150 setHapiContext(newContext); 151 } 152 153 /** 154 * Returns a String representing the encoding of the given message, if the encoding is 155 * recognized. For example if the given message appears to be encoded using HL7 2.x XML rules 156 * then "XML" would be returned. If the encoding is not recognized then null is returned. That 157 * this method returns a specific encoding does not guarantee that the message is correctly 158 * encoded (e.g. well formed XML) - just that it is not encoded using any other encoding than 159 * the one returned. Returns null if the encoding is not recognized. 160 * 161 * @param message message string 162 * @return string representing the encoding of the given message, i.e. "XML" or "ER7" 163 */ 164 public abstract String getEncoding(String message); 165 166 /** 167 * Returns true if and only if the given encoding is supported by this Parser. 168 * @param encoding the encoding, "XML" or "ER7" 169 * @return true if this parser supports parsing message encoded this way 170 */ 171 public boolean supportsEncoding(String encoding) { 172 return getDefaultEncoding().equalsIgnoreCase(encoding); 173 } 174 175 /** 176 * @return the preferred encoding of this Parser ("XML" or "ER7") 177 */ 178 public abstract String getDefaultEncoding(); 179 180 /** 181 * Parses a message string and returns the corresponding Message object. 182 * 183 * @param message a String that contains an HL7 message 184 * @return a HAPI Message object parsed from the given String 185 * @throws HL7Exception if the message is not correctly formatted. 186 * @throws EncodingNotSupportedException if the message encoded is not supported by this parser. 187 */ 188 public Message parse(String message) throws HL7Exception { 189 String encoding = getEncoding(message); 190 if (!supportsEncoding(encoding)) { 191 String startOfMessage = null; 192 if (message.startsWith("MSH")) { 193 int indexOfCR = message.indexOf('\r'); 194 if (indexOfCR > 0) { 195 startOfMessage = message.substring(0, indexOfCR); 196 } 197 } 198 if (startOfMessage == null) { 199 startOfMessage = message.substring(0, Math.min(message.length(), 50)); 200 } 201 throw new EncodingNotSupportedException("Determine encoding for message. The following is the first 50 chars of the message for reference, although this may not be where the issue is: " 202 + startOfMessage); 203 } 204 205 String version = getVersion(message); 206 207 if (!getParserConfiguration().isAllowUnknownVersions()) { 208 assertVersionExists(version); 209 } 210 211 assertMessageValidates(message, encoding, version); 212 Message result = doParse(message, version); 213 assertMessageValidates(result); 214 215 result.setParser(this); 216 217 applySuperStructureName(result); 218 219 return result; 220 } 221 222 /** 223 * Called by parse() to perform implementation-specific parsing work. 224 * 225 * @param message a String that contains an HL7 message 226 * @param version the name of the HL7 version to which the message belongs (eg "2.5") 227 * @return a HAPI Message object parsed from the given String 228 * @throws HL7Exception if the message is not correctly formatted. 229 * @throws EncodingNotSupportedException if the message encoded is not supported by this parser. 230 */ 231 protected abstract Message doParse(String message, String version) throws HL7Exception; 232 233 /** 234 * Formats a Message object into an HL7 message string using the given encoding. 235 * 236 * @param source a Message object from which to construct an encoded message string 237 * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support 238 * only one encoding) 239 * @return the encoded message 240 * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required 241 * fields are null) 242 * @throws EncodingNotSupportedException if the requested encoding is not supported by this 243 * parser. 244 */ 245 public String encode(Message source, String encoding) throws HL7Exception { 246 assertMessageValidates(source); 247 String result = doEncode(source, encoding); 248 assertMessageValidates(result, encoding, source.getVersion()); 249 return result; 250 } 251 252 /** 253 * Called by encode(Message, String) to perform implementation-specific encoding work. 254 * 255 * @param source a Message object from which to construct an encoded message string 256 * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support 257 * only one encoding) 258 * @return the encoded message 259 * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required 260 * fields are null) 261 * @throws EncodingNotSupportedException if the requested encoding is not supported by this 262 * parser. 263 */ 264 protected abstract String doEncode(Message source, String encoding) throws HL7Exception; 265 266 /** 267 * Formats a Message object into an HL7 message string using this parser's default encoding. 268 * 269 * @param source a Message object from which to construct an encoded message string 270 * @return the encoded message 271 * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required 272 * fields are null) 273 */ 274 public String encode(Message source) throws HL7Exception { 275 assertMessageValidates(source); 276 String result = doEncode(source); 277 assertMessageValidates(result, getDefaultEncoding(), source.getVersion()); 278 return result; 279 } 280 281 /** 282 * Called by encode(Message) to perform implementation-specific encoding work. 283 * 284 * @param source a Message object from which to construct an encoded message string 285 * @return the encoded message 286 * @throws HL7Exception if the data fields in the message do not permit encoding (e.g. required 287 * fields are null) 288 * @throws EncodingNotSupportedException if the requested encoding is not supported by this 289 * parser. 290 */ 291 protected abstract String doEncode(Message source) throws HL7Exception; 292 293 /** 294 * <p> 295 * Returns a minimal amount of data from a message string, including only the data needed to 296 * send a response to the remote system. This includes the following fields: 297 * <ul> 298 * <li>field separator</li> 299 * <li>encoding characters</li> 300 * <li>processing ID</li> 301 * <li>message control ID</li> 302 * </ul> 303 * This method is intended for use when there is an error parsing a message, (so the Message 304 * object is unavailable) but an error message must be sent back to the remote system including 305 * some of the information in the inbound message. This method parses only that required 306 * information, hopefully avoiding the condition that caused the original error. 307 * </p> 308 * 309 * @param message the message 310 * @return an MSH segment 311 * @throws HL7Exception if no MSH segment could be created 312 */ 313 public abstract Segment getCriticalResponseData(String message) throws HL7Exception; 314 315 /** 316 * For response messages, returns the value of MSA-2 (the message ID of the message sent by the 317 * sending system). This value may be needed prior to main message parsing, so that 318 * (particularly in a multi-threaded scenario) the message can be routed to the thread that sent 319 * the request. We need this information first so that any parse exceptions are thrown to the 320 * correct thread. Implementers of Parsers should take care to make the implementation of this 321 * method very fast and robust. Returns null if MSA-2 can not be found (e.g. if the message is 322 * not a response message). 323 * 324 * @param message the message 325 * @return the value of MSA-2 326 */ 327 public abstract String getAckID(String message); 328 329 /** 330 * Returns the version ID (MSH-12) from the given message, without fully parsing the message. 331 * The version is needed prior to parsing in order to determine the message class into which the 332 * text of the message should be parsed. 333 * 334 * @param message the message 335 * @return the value of MSH-12 336 * @throws HL7Exception if the version field can not be found. 337 */ 338 public abstract String getVersion(String message) throws HL7Exception; 339 340 /** 341 * Encodes a particular segment and returns the encoded structure 342 * 343 * @param structure The structure to encode 344 * @param encodingCharacters The encoding characters 345 * @return The encoded segment 346 * @throws HL7Exception If there is a problem encoding 347 * @since 1.0 348 */ 349 public abstract String doEncode(Segment structure, EncodingCharacters encodingCharacters) 350 throws HL7Exception; 351 352 /** 353 * Encodes a particular type and returns the encoded structure 354 * 355 * @param type The type to encode 356 * @param encodingCharacters The encoding characters 357 * @return The encoded type 358 * @throws HL7Exception If there is a problem encoding 359 * @since 1.0 360 */ 361 public abstract String doEncode(Type type, EncodingCharacters encodingCharacters) 362 throws HL7Exception; 363 364 /** 365 * Parses a particular type and returns the encoded structure 366 * 367 * @param string The string to parse 368 * @param type The type to encode 369 * @param encodingCharacters The encoding characters 370 * @throws HL7Exception If there is a problem encoding 371 * @since 1.0 372 */ 373 public abstract void parse(Type type, String string, EncodingCharacters encodingCharacters) 374 throws HL7Exception; 375 376 /** 377 * Parse a message using a specific model package instead of the default, using 378 * {@link ModelClassFactory#getMessageClassInASpecificPackage(String, String, boolean, String)} 379 * . 380 * 381 * <b>WARNING: This method is only implemented in some parser implementations</b>. Currently it 382 * will only work with the PipeParser parser implementation. Use with caution. 383 * 384 * @param message message string 385 * @param packageName name of the package of the models 386 * @return parsed message 387 * @throws HL7Exception if an error occurred while parsing 388 */ 389 public Message parseForSpecificPackage(String message, String packageName) throws HL7Exception { 390 String encoding = getEncoding(message); 391 if (!supportsEncoding(encoding)) { 392 throw new EncodingNotSupportedException("Can't parse message beginning " 393 + message.substring(0, Math.min(message.length(), 50))); 394 } 395 396 String version = getVersion(message); 397 assertVersionExists(version); 398 399 assertMessageValidates(message, encoding, version); 400 Message result = doParseForSpecificPackage(message, version, packageName); 401 assertMessageValidates(result); 402 403 result.setParser(this); 404 return result; 405 } 406 407 /** 408 * Attempt the parse a message using a specific model package 409 */ 410 protected abstract Message doParseForSpecificPackage(String message, String version, 411 String packageName) throws HL7Exception; 412 413 /** 414 * Instantiate a message type using a specific package name 415 * 416 * @see ModelClassFactory#getMessageClassInASpecificPackage(String, String, boolean, String) 417 */ 418 protected Message instantiateMessageInASpecificPackage(String theName, String theVersion, 419 boolean isExplicit, String packageName) throws HL7Exception { 420 Class<? extends Message> messageClass = getFactory().getMessageClassInASpecificPackage( 421 theName, theVersion, isExplicit, packageName); 422 if (messageClass == null) { 423 throw new HL7Exception("Can't find message class in current package list: " + theName); 424 } 425 return ReflectionUtil.instantiateMessage(messageClass, getFactory()); 426 } 427 428 /** 429 * Parses a particular segment and returns the encoded structure 430 * 431 * @param segment The segment to encode 432 * @param string The string to parse 433 * @param encodingCharacters The encoding characters 434 * @throws HL7Exception If there is a problem encoding 435 */ 436 public abstract void parse(Segment segment, String string, EncodingCharacters encodingCharacters) 437 throws HL7Exception; 438 439 /** 440 * Parses a particular message and returns the encoded structure 441 * 442 * @param message The message to encode 443 * @param string The string to parse 444 * @throws HL7Exception If there is a problem encoding 445 * @since 1.0 446 */ 447 public abstract void parse(Message message, String string) throws HL7Exception; 448 449 /** 450 * <p> 451 * Creates a version-specific MSH object and returns it as a 452 * version-independent MSH interface. 453 * </p> 454 * <p> 455 * Since HAPI 2.1, if a version specific MSH 456 * segment can't be found (for example because the specific 457 * structure JAR is not found on the classpath), an instance of 458 * {@link GenericSegment} is returned. 459 * </p> 460 * 461 * @param version HL7 version 462 * @param factory model class factory to be used 463 * @return MSH segment for this version returned by the model class factory 464 * @throws HL7Exception if no matching segment could be found 465 */ 466 public static Segment makeControlMSH(String version, ModelClassFactory factory) 467 throws HL7Exception { 468 Segment msh; 469 470 try { 471 Class<? extends Message> genericMessageClass; 472 genericMessageClass = GenericMessage.getGenericMessageClass(version); 473 474 Constructor<? extends Message> constr = genericMessageClass 475 .getConstructor(new Class[] { ModelClassFactory.class }); 476 Message dummy = constr.newInstance(factory); 477 478 Class<? extends Segment> c = null; 479 480 if (Version.supportsVersion(version)) { 481 c = factory.getSegmentClass("MSH", version); 482 } 483 484 if (c != null) { 485 if (GenericSegment.class.isAssignableFrom(c)) { 486 Class<?>[] constructorParamTypes = { Group.class, String.class }; 487 Object[] constructorParamArgs = { dummy, "MSH" }; 488 Constructor<? extends Segment> constructor = c.getConstructor(constructorParamTypes); 489 msh = constructor.newInstance(constructorParamArgs); 490 } else { 491 Class<?>[] constructorParamTypes = { Group.class, ModelClassFactory.class }; 492 Object[] constructorParamArgs = { dummy, factory }; 493 Constructor<? extends Segment> constructor = c.getConstructor(constructorParamTypes); 494 msh = constructor.newInstance(constructorParamArgs); 495 } 496 } else { 497 msh = new GenericSegment(dummy, "MSH"); 498 } 499 } catch (Exception e) { 500 throw new HL7Exception("Couldn't create MSH for version " + version 501 + " (does your classpath include this version?) ... ", e); 502 } 503 return msh; 504 } 505 506 /** 507 * Returns true if the given string represents a valid 2.x version. Valid versions include 508 * "2.1", "2.2", "2.3", "2.3.1", "2.4", "2.5", "2.5.1", "2.6" 509 * 510 * @param version HL7 version string 511 * @return <code>true</code> if version is known 512 * @deprecated Use {@link Version#supportsVersion(String)} 513 */ 514 @Deprecated 515 public static boolean validVersion(String version) { 516 return Version.supportsVersion(version); 517 } 518 519 /** 520 * Like {@link #validVersion(String)} but throws an HL7Exception instead 521 * 522 * @param version HL7 version 523 * @throws HL7Exception if version is unknown 524 */ 525 public static void assertVersionExists(String version) throws HL7Exception { 526 if (!Version.supportsVersion(version)) 527 throw new HL7Exception( 528 "The HL7 version " + version + " is not recognized", 529 ErrorCode.UNSUPPORTED_VERSION_ID); 530 } 531 532 /** 533 * Given a concatenation of message type and event (e.g. ADT_A04), and the version, finds the 534 * corresponding message structure (e.g. ADT_A01). This is needed because some events share 535 * message structures, although it is not needed when the message structure is explicitly valued 536 * in MSH-9-3. If no mapping is found, returns the original name. 537 * 538 * @throws HL7Exception if there is an error retrieving the map, or if the given version is 539 * invalid 540 * 541 * @deprecated use {@link ModelClassFactory#getMessageStructureForEvent(String, Version)} 542 */ 543 public String getMessageStructureForEvent(String name, String version) 544 throws HL7Exception { 545 assertVersionExists(version); 546 return getHapiContext().getModelClassFactory(). 547 getMessageStructureForEvent(name, Version.versionOf(version)); 548 } 549 550 /** 551 * Note that the validation context of the resulting message is set to this parser's validation 552 * context. The validation context is used within Primitive.setValue(). 553 * 554 * @param theName name of the desired structure in the form XXX_YYY 555 * @param theVersion HL7 version (e.g. "2.3") 556 * @param isExplicit true if the structure was specified explicitly in MSH-9-3, false if it was 557 * inferred from MSH-9-1 and MSH-9-2. If false, a lookup may be performed to find an 558 * alternate structure corresponding to that message type and event. 559 * @return a Message instance 560 * @throws HL7Exception if the version is not recognized or no appropriate class can be found or 561 * the Message class throws an exception on instantiation (e.g. if args are not as 562 * expected) 563 */ 564 protected Message instantiateMessage(String theName, String theVersion, boolean isExplicit) 565 throws HL7Exception { 566 Class<? extends Message> messageClass = getFactory().getMessageClass(theName, theVersion, isExplicit); 567 if (messageClass == null) 568 throw new HL7Exception("Can't find message class in current package list: " + theName); 569 return ReflectionUtil.instantiateMessage(messageClass, getFactory()); 570 } 571 572 protected void applySuperStructureName(Message theMessage) throws HL7Exception { 573 if (theMessage instanceof AbstractSuperMessage) { 574 if (theMessage.getName() == null) { 575 Terser t = new Terser(theMessage); 576 String name = null; 577 try { 578 name = t.get("/MSH-9-3"); 579 } catch (HL7Exception e) { 580 // ignore 581 } 582 583 if (StringUtil.isBlank(name)) { 584 name = t.get("/MSH-9-1") + "_" + t.get("/MSH-9-2"); 585 } 586 587 ((AbstractSuperMessage)theMessage).setName(name); 588 } 589 } 590 591 } 592 593 private <R> void assertMessageValidates(String message, String encoding, String version) throws HL7Exception { 594 if (isValidating()) { 595 Validator<R> validator = getHapiContext().getMessageValidator(); 596 ValidationExceptionHandlerFactory<R> factory = getHapiContext().getValidationExceptionHandlerFactory(); 597 ValidationExceptionHandler<R> handler = factory.getNewInstance(getHapiContext()); 598 R result = validator.validate(message, encoding.equals("XML"), version, handler); 599 handleException(handler, result); 600 } 601 } 602 603 private <R> void assertMessageValidates(Message message) throws HL7Exception { 604 if (isValidating()) { 605 Validator<R> validator = getHapiContext().getMessageValidator(); 606 ValidationExceptionHandlerFactory<R> factory = getHapiContext().getValidationExceptionHandlerFactory(); 607 if (factory == null) { 608 throw new NullPointerException("Validation is enabled for this parser, but ValidationExceptionHandlerFactory is null"); 609 } 610 ValidationExceptionHandler<R> handler = factory.getNewInstance(getHapiContext()); 611 R result = validator.validate(message, handler); 612 handleException(handler, result); 613 } 614 } 615 616 private <R> void handleException(ValidationExceptionHandler<R> handler, R result) 617 throws HL7Exception { 618 if (handler.hasFailed()) { 619 HL7Exception e = new HL7Exception("Validation has failed"); 620 e.setDetail(result); 621 if (result instanceof Message) e.setResponseMessage((Message)result); 622 throw e; 623 } 624 } 625 626 private boolean isValidating() { 627 return getHapiContext().getParserConfiguration().isValidating(); 628 } 629 630 631}