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