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*/ 028 029package ca.uhn.hl7v2.parser; 030 031import java.io.IOException; 032import java.io.InputStream; 033import java.lang.reflect.Constructor; 034import java.util.HashMap; 035import java.util.Map; 036import java.util.Properties; 037 038import org.slf4j.Logger; 039import org.slf4j.LoggerFactory; 040 041import ca.uhn.hl7v2.HL7Exception; 042import ca.uhn.hl7v2.Version; 043import ca.uhn.hl7v2.VersionLogger; 044import ca.uhn.hl7v2.model.GenericMessage; 045import ca.uhn.hl7v2.model.Group; 046import ca.uhn.hl7v2.model.Message; 047import ca.uhn.hl7v2.model.Segment; 048import ca.uhn.hl7v2.model.Type; 049import ca.uhn.hl7v2.validation.MessageValidator; 050import ca.uhn.hl7v2.validation.ValidationContext; 051import ca.uhn.hl7v2.validation.ValidationException; 052import ca.uhn.hl7v2.validation.impl.ValidationContextFactory; 053 054/** 055 * Parses HL7 message Strings into HL7 Message objects and 056 * encodes HL7 Message objects into HL7 message Strings. 057 * @author Bryan Tripp (bryan_tripp@sourceforge.net) 058 */ 059public abstract class Parser { 060 061 private static final Logger log = LoggerFactory.getLogger(Parser.class); 062 private static Map<String, Properties> messageStructures = null; 063 064 private ModelClassFactory myFactory; 065 private ValidationContext myContext; 066 private MessageValidator myValidator; 067 private ParserConfiguration myParserConfiguration = new ParserConfiguration(); 068 069 static { 070 VersionLogger.init(); 071 } 072 073 /** 074 * Uses DefaultModelClassFactory for model class lookup. 075 */ 076 public Parser() { 077 this(null); 078 } 079 080 /** 081 * @param theFactory custom factory to use for model class lookup 082 */ 083 public Parser(ModelClassFactory theFactory) { 084 if (theFactory == null) { 085 theFactory = new DefaultModelClassFactory(); 086 } 087 088 myFactory = theFactory; 089 ValidationContext validationContext; 090 try { 091 validationContext = ValidationContextFactory.getContext(); 092 } catch (ValidationException e) { 093 log.warn("Failed to get a validation context from the " + 094 "ValidationContextFactory", e); 095 validationContext = ValidationContextFactory.defaultValidation(); 096 } 097 setValidationContext(validationContext); 098 } 099 100 /** 101 * @return the factory used by this Parser for model class lookup 102 */ 103 public ModelClassFactory getFactory() { 104 return myFactory; 105 } 106 107 /** 108 * @return the set of validation rules that is applied to messages parsed or encoded by this parser. Note that this method may return <code>null</code> 109 */ 110 public ValidationContext getValidationContext() { 111 return myContext; 112 } 113 114 /** 115 * @param theContext the set of validation rules to be applied to messages parsed or 116 * encoded by this parser (defaults to ValidationContextFactory.DefaultValidation) 117 */ 118 public void setValidationContext(ValidationContext theContext) { 119 myContext = theContext; 120 myValidator = new MessageValidator(theContext, true); 121 } 122 123 124 /** 125 * @return Returns the parser configuration. This is a bean which contains configuration 126 * instructions relating to how a parser should be parsing or encoding messages it 127 * deals with. 128 */ 129 public ParserConfiguration getParserConfiguration() { 130 return myParserConfiguration; 131 } 132 133 /** 134 * Sets the parser configuration for this parser (may not be null). This is a bean which contains configuration 135 * instructions relating to how a parser should be parsing or encoding messages it 136 * deals with. 137 * 138 * @param theParserConfiguration The parser configuration 139 */ 140 public void setParserConfiguration(ParserConfiguration theParserConfiguration) { 141 if (theParserConfiguration == null) { 142 throw new NullPointerException("theParserConfiguration may not be null"); 143 } 144 myParserConfiguration = theParserConfiguration; 145 } 146 147 148 /** 149 * Returns a String representing the encoding of the given message, if 150 * the encoding is recognized. For example if the given message appears 151 * to be encoded using HL7 2.x XML rules then "XML" would be returned. 152 * If the encoding is not recognized then null is returned. That this 153 * method returns a specific encoding does not guarantee that the 154 * message is correctly encoded (e.g. well formed XML) - just that 155 * it is not encoded using any other encoding than the one returned. 156 * Returns null if the encoding is not recognized. 157 */ 158 public abstract String getEncoding(String message); 159 160 /** 161 * Returns true if and only if the given encoding is supported 162 * by this Parser. 163 */ 164 public abstract boolean supportsEncoding(String encoding); 165 166 /** 167 * @return the preferred encoding of this Parser 168 */ 169 public abstract String getDefaultEncoding(); 170 171 /** 172 * Parses a message string and returns the corresponding Message object. 173 * 174 * @param message a String that contains an HL7 message 175 * @return a HAPI Message object parsed from the given String 176 * @throws HL7Exception if the message is not correctly formatted. 177 * @throws EncodingNotSupportedException if the message encoded 178 * is not supported by this parser. 179 */ 180 public Message parse(String message) throws HL7Exception, EncodingNotSupportedException { 181 String encoding = getEncoding(message); 182 if (!supportsEncoding(encoding)) { 183 throw new EncodingNotSupportedException( 184 "Can't parse message beginning " + message.substring(0, Math.min(message.length(), 50))); 185 } 186 187 String version = getVersion(message); 188 if (!validVersion(version)) { 189 throw new HL7Exception("Can't process message of version '" + version + "' - version not recognized", 190 HL7Exception.UNSUPPORTED_VERSION_ID); 191 } 192 193 myValidator.validate(message, encoding.equals("XML"), version); 194 Message result = doParse(message, version); 195 myValidator.validate(result); 196 197 result.setParser(this); 198 199 return result; 200 } 201 202 /** 203 * Called by parse() to perform implementation-specific parsing work. 204 * 205 * @param message a String that contains an HL7 message 206 * @param version the name of the HL7 version to which the message belongs (eg "2.5") 207 * @return a HAPI Message object parsed from the given String 208 * @throws HL7Exception if the message is not correctly formatted. 209 * @throws EncodingNotSupportedException if the message encoded 210 * is not supported by this parser. 211 */ 212 protected abstract Message doParse(String message, String version) 213 throws HL7Exception, EncodingNotSupportedException; 214 215 /** 216 * Formats a Message object into an HL7 message string using the given 217 * encoding. 218 * 219 * @param source a Message object from which to construct an encoded message string 220 * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support only 221 * one encoding) 222 * @return the encoded message 223 * @throws HL7Exception if the data fields in the message do not permit encoding 224 * (e.g. required fields are null) 225 * @throws EncodingNotSupportedException if the requested encoding is not 226 * supported by this parser. 227 */ 228 public String encode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException { 229 myValidator.validate(source); 230 String result = doEncode(source, encoding); 231 myValidator.validate(result, encoding.equals("XML"), source.getVersion()); 232 233 return result; 234 } 235 236 /** 237 * Called by encode(Message, String) to perform implementation-specific encoding work. 238 * 239 * @param source a Message object from which to construct an encoded message string 240 * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support only 241 * one encoding) 242 * @return the encoded message 243 * @throws HL7Exception if the data fields in the message do not permit encoding 244 * (e.g. required fields are null) 245 * @throws EncodingNotSupportedException if the requested encoding is not 246 * supported by this parser. 247 */ 248 protected abstract String doEncode(Message source, String encoding) 249 throws HL7Exception, EncodingNotSupportedException; 250 251 /** 252 * Formats a Message object into an HL7 message string using this parser's 253 * default encoding. 254 * 255 * @param source a Message object from which to construct an encoded message string 256 * @param encoding the name of the encoding to use (eg "XML"; most implementations support only one 257 * encoding) 258 * @return the encoded message 259 * @throws HL7Exception if the data fields in the message do not permit encoding 260 * (e.g. required fields are null) 261 */ 262 public String encode(Message source) throws HL7Exception { 263 String encoding = getDefaultEncoding(); 264 265 myValidator.validate(source); 266 String result = doEncode(source); 267 myValidator.validate(result, encoding.equals("XML"), source.getVersion()); 268 269 return result; 270 } 271 272 /** 273 * Called by encode(Message) to perform implementation-specific encoding work. 274 * 275 * @param source a Message object from which to construct an encoded message string 276 * @return the encoded message 277 * @throws HL7Exception if the data fields in the message do not permit encoding 278 * (e.g. required fields are null) 279 * @throws EncodingNotSupportedException if the requested encoding is not 280 * supported by this parser. 281 */ 282 protected abstract String doEncode(Message source) throws HL7Exception; 283 284 /** 285 * <p>Returns a minimal amount of data from a message string, including only the 286 * data needed to send a response to the remote system. This includes the 287 * following fields: 288 * <ul><li>field separator</li> 289 * <li>encoding characters</li> 290 * <li>processing ID</li> 291 * <li>message control ID</li></ul> 292 * This method is intended for use when there is an error parsing a message, 293 * (so the Message object is unavailable) but an error message must be sent 294 * back to the remote system including some of the information in the inbound 295 * message. This method parses only that required information, hopefully 296 * avoiding the condition that caused the original error.</p> 297 * @return an MSH segment 298 */ 299 public abstract Segment getCriticalResponseData(String message) throws HL7Exception; 300 301 /** 302 * For response messages, returns the value of MSA-2 (the message ID of the message 303 * sent by the sending system). This value may be needed prior to main message parsing, 304 * so that (particularly in a multi-threaded scenario) the message can be routed to 305 * the thread that sent the request. We need this information first so that any 306 * parse exceptions are thrown to the correct thread. Implementers of Parsers should 307 * take care to make the implementation of this method very fast and robust. 308 * Returns null if MSA-2 can not be found (e.g. if the message is not a 309 * response message). 310 */ 311 public abstract String getAckID(String message); 312 313 /** 314 * Returns the version ID (MSH-12) from the given message, without fully parsing the message. 315 * The version is needed prior to parsing in order to determine the message class 316 * into which the text of the message should be parsed. 317 * @throws HL7Exception if the version field can not be found. 318 */ 319 public abstract String getVersion(String message) throws HL7Exception; 320 321 322 /** 323 * Encodes a particular segment and returns the encoded structure 324 * 325 * @param structure The structure to encode 326 * @param encodingCharacters The encoding characters 327 * @return The encoded segment 328 * @throws HL7Exception If there is a problem encoding 329 * @since 1.0 330 */ 331 public abstract String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception; 332 333 334 /** 335 * Encodes a particular type and returns the encoded structure 336 * 337 * @param type The type to encode 338 * @param encodingCharacters The encoding characters 339 * @return The encoded type 340 * @throws HL7Exception If there is a problem encoding 341 * @since 1.0 342 */ 343 public abstract String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception; 344 345 346 /** 347 * Parses a particular type and returns the encoded structure 348 * 349 * @param string The string to parse 350 * @param type The type to encode 351 * @param encodingCharacters The encoding characters 352 * @return The encoded type 353 * @throws HL7Exception If there is a problem encoding 354 * @since 1.0 355 */ 356 public abstract void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception; 357 358 /** 359 * Parse a message using a specific model package instead of the default, using {@link ModelClassFactory#getMessageClassInASpecificPackage(String, String, boolean, String)}. 360 * 361 * <b>WARNING: This method is only implemented in some parser implementations</b>. Currently it will only 362 * work with the PipeParser parser implementation. Use with caution. 363 */ 364 public Message parseForSpecificPackage(String message, String packageName) throws HL7Exception, EncodingNotSupportedException { 365 String encoding = getEncoding(message); 366 if (!supportsEncoding(encoding)) { 367 throw new EncodingNotSupportedException( 368 "Can't parse message beginning " + message.substring(0, Math.min(message.length(), 50))); 369 } 370 371 String version = getVersion(message); 372 if (!validVersion(version)) { 373 throw new HL7Exception("Can't process message of version '" + version + "' - version not recognized", 374 HL7Exception.UNSUPPORTED_VERSION_ID); 375 } 376 377 myValidator.validate(message, encoding.equals("XML"), version); 378 379 Message result = doParseForSpecificPackage(message, version, packageName); 380 381 myValidator.validate(result); 382 383 result.setParser(this); 384 385 return result; 386 } 387 388 /** 389 * Attempt the parse a message using a specific model package 390 */ 391 protected abstract Message doParseForSpecificPackage(String message, String version, String packageName) throws HL7Exception, EncodingNotSupportedException; 392 393 /** 394 * Instantiate a message type using a specific package name 395 * 396 * @see ModelClassFactory#getMessageClassInASpecificPackage(String, String, boolean, String) 397 */ 398 protected Message instantiateMessageInASpecificPackage(String theName, String theVersion, boolean isExplicit, String packageName) throws HL7Exception { 399 Message result = null; 400 401 try { 402 Class<? extends Message> messageClass = myFactory.getMessageClassInASpecificPackage(theName, theVersion, isExplicit, packageName); 403 if (messageClass == null) { 404 throw new ClassNotFoundException("Can't find message class in current package list: " + theName); 405 } 406 407 log.debug("Instantiating msg of class {}", messageClass.getName()); 408 Constructor<? extends Message> constructor = messageClass.getConstructor(new Class[]{ModelClassFactory.class}); 409 result = (Message) constructor.newInstance(new Object[]{myFactory}); 410 } catch (Exception e) { 411 throw new HL7Exception("Couldn't create Message object of type " + theName, 412 HL7Exception.UNSUPPORTED_MESSAGE_TYPE, e); 413 } 414 415 result.setValidationContext(myContext); 416 417 return result; 418 } 419 420 421 /** 422 * Parses a particular segment and returns the encoded structure 423 * 424 * @param string The string to parse 425 * @param segment The segment to encode 426 * @param encodingCharacters The encoding characters 427 * @return The encoded type 428 * @throws HL7Exception If there is a problem encoding 429 */ 430 public abstract void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception; 431 432 433 /** 434 * Parses a particular message and returns the encoded structure 435 * 436 * @param string The string to parse 437 * @param message The message to encode 438 * @return The encoded type 439 * @throws HL7Exception If there is a problem encoding 440 * @since 1.0 441 */ 442 public abstract void parse(Message message, String string) throws HL7Exception; 443 444 445 /** 446 * Creates a version-specific MSH object and returns it as a version-independent 447 * MSH interface. 448 * throws HL7Exception if there is a problem, e.g. invalid version, code not available 449 * for given version. 450 */ 451 public static Segment makeControlMSH(String version, ModelClassFactory factory) throws HL7Exception { 452 Segment msh = null; 453 454 try { 455 Message dummy = GenericMessage.getGenericMessageClass(version) 456 .getConstructor(new Class[]{ModelClassFactory.class}).newInstance(new Object[]{factory}); 457 458 Class<?>[] constructorParamTypes = { Group.class, ModelClassFactory.class }; 459 Object[] constructorParamArgs = { dummy, factory }; 460 Class<? extends Segment> c = factory.getSegmentClass("MSH", version); 461 Constructor<? extends Segment> constructor = c.getConstructor(constructorParamTypes); 462 msh = constructor.newInstance(constructorParamArgs); 463 } 464 catch (Exception e) { 465 throw new HL7Exception( 466 "Couldn't create MSH for version " + version + " (does your classpath include this version?) ... ", 467 HL7Exception.APPLICATION_INTERNAL_ERROR, 468 e); 469 } 470 return msh; 471 } 472 473 /** 474 * Returns true if the given string represents a valid 2.x version. Valid versions 475 * include "2.1", "2.2", "2.3", "2.3.1", "2.4", "2.5", "2.6" 476 */ 477 public static boolean validVersion(String version) { 478 return Version.supportsVersion(version); 479 } 480 481 /** 482 * Given a concatenation of message type and event (e.g. ADT_A04), and the 483 * version, finds the corresponding message structure (e.g. ADT_A01). This 484 * is needed because some events share message structures, although it is not needed 485 * when the message structure is explicitly valued in MSH-9-3. 486 * If no mapping is found, returns the original name. 487 * @throws HL7Exception if there is an error retrieving the map, or if the given 488 * version is invalid 489 */ 490 public static String getMessageStructureForEvent(String name, String version) throws HL7Exception { 491 String structure = null; 492 493 if (!validVersion(version)) 494 throw new HL7Exception("The version " + version + " is unknown"); 495 496 Properties p = null; 497 try { 498 p = (Properties) getMessageStructures().get(version); 499 500 if (p == null) 501 throw new HL7Exception("No map found for version " + version + ". Only the following are available: " + getMessageStructures().keySet()); 502 503 } catch (IOException ioe) { 504 throw new HL7Exception(ioe); 505 } 506 507 structure = p.getProperty(name); 508 509 if (structure == null) { 510 structure = name; 511 } 512 513 return structure; 514 } 515 516 517 /** 518 * Returns a copy of the message structure map for a specific version. 519 * Each key is a message type (e.g. ADT_A04) and each value is the 520 * corresponding structure (e.g. ADT_A01). 521 * 522 * @throws IOException If the event map can't be loaded 523 */ 524 public static Properties getMessageStructures(String version) throws IOException { 525 Map<String, Properties> msgStructures = getMessageStructures(); 526 if (!msgStructures.containsKey(version)) { 527 return null; 528 } 529 530 return (Properties) msgStructures.get(version).clone(); 531 } 532 533 534 /** 535 * Returns version->event->structure maps. 536 */ 537 private synchronized static Map<String, Properties> getMessageStructures() throws IOException { 538 if (messageStructures == null) { 539 messageStructures = loadMessageStructures(); 540 } 541 return messageStructures; 542 } 543 544 private static Map<String, Properties> loadMessageStructures() throws IOException { 545 Map<String, Properties> map = new HashMap<String, Properties>(); 546 for (Version v : Version.values()) { 547 String resource = "ca/uhn/hl7v2/parser/eventmap/" + v.getVersion() + ".properties"; 548 InputStream in = Parser.class.getClassLoader().getResourceAsStream(resource); 549 550 Properties structures = null; 551 if (in != null) { 552 structures = new Properties(); 553 structures.load(in); 554 map.put(v.getVersion(), structures); 555 } 556 557 } 558 return map; 559 } 560 561 /** 562 * Note that the validation context of the resulting message is set to this parser's validation 563 * context. The validation context is used within Primitive.setValue(). 564 * 565 * @param name name of the desired structure in the form XXX_YYY 566 * @param version HL7 version (e.g. "2.3") 567 * @param isExplicit true if the structure was specified explicitly in MSH-9-3, false if it 568 * was inferred from MSH-9-1 and MSH-9-2. If false, a lookup may be performed to find 569 * an alternate structure corresponding to that message type and event. 570 * @return a Message instance 571 * @throws HL7Exception if the version is not recognized or no appropriate class can be found or the Message 572 * class throws an exception on instantiation (e.g. if args are not as expected) 573 */ 574 protected Message instantiateMessage(String theName, String theVersion, boolean isExplicit) throws HL7Exception { 575 Message result = null; 576 577 try { 578 Class<? extends Message> messageClass = myFactory.getMessageClass(theName, theVersion, isExplicit); 579 if (messageClass == null) 580 throw new ClassNotFoundException("Can't find message class in current package list: " 581 + theName); 582 log.debug("Instantiating msg of class {}", messageClass.getName()); 583 Constructor<? extends Message> constructor = messageClass.getConstructor(new Class[]{ModelClassFactory.class}); 584 result = constructor.newInstance(new Object[]{myFactory}); 585 } catch (Exception e) { 586 throw new HL7Exception("Couldn't create Message object of type " + theName, 587 HL7Exception.UNSUPPORTED_MESSAGE_TYPE, e); 588 } 589 590 result.setValidationContext(myContext); 591 592 return result; 593 } 594 595}