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.HashMap;
035 import java.util.Map;
036 import java.util.Properties;
037
038 import org.slf4j.Logger;
039 import org.slf4j.LoggerFactory;
040
041 import ca.uhn.hl7v2.HL7Exception;
042 import ca.uhn.hl7v2.Version;
043 import ca.uhn.hl7v2.VersionLogger;
044 import ca.uhn.hl7v2.model.GenericMessage;
045 import ca.uhn.hl7v2.model.Group;
046 import ca.uhn.hl7v2.model.Message;
047 import ca.uhn.hl7v2.model.Segment;
048 import ca.uhn.hl7v2.model.Type;
049 import ca.uhn.hl7v2.validation.MessageValidator;
050 import ca.uhn.hl7v2.validation.ValidationContext;
051 import ca.uhn.hl7v2.validation.ValidationException;
052 import 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 */
059 public 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 }