1 /**
2 The contents of this file are subject to the Mozilla Public License Version 1.1
3 (the "License"); you may not use this file except in compliance with the License.
4 You may obtain a copy of the License at http://www.mozilla.org/MPL/
5 Software distributed under the License is distributed on an "AS IS" basis,
6 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
7 specific language governing rights and limitations under the License.
8
9 The Original Code is "Parser.java". Description:
10 "Parses HL7 message Strings into HL7 Message objects and
11 encodes HL7 Message objects into HL7 message Strings"
12
13 The Initial Developer of the Original Code is University Health Network. Copyright (C)
14 2001. All Rights Reserved.
15
16 Contributor(s): ______________________________________.
17
18 Alternatively, the contents of this file may be used under the terms of the
19 GNU General Public License (the "GPL"), in which case the provisions of the GPL are
20 applicable instead of those above. If you wish to allow use of your version of this
21 file only under the terms of the GPL and not to allow others to use your version
22 of this file under the MPL, indicate your decision by deleting the provisions above
23 and replace them with the notice and other provisions required by the GPL License.
24 If you do not delete the provisions above, a recipient may use your version of
25 this file under either the MPL or the GPL.
26 */
27
28 package ca.uhn.hl7v2.parser;
29
30 import java.lang.reflect.Constructor;
31
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import ca.uhn.hl7v2.DefaultHapiContext;
36 import ca.uhn.hl7v2.ErrorCode;
37 import ca.uhn.hl7v2.HL7Exception;
38 import ca.uhn.hl7v2.HapiContext;
39 import ca.uhn.hl7v2.HapiContextSupport;
40 import ca.uhn.hl7v2.Version;
41 import ca.uhn.hl7v2.model.AbstractSuperMessage;
42 import ca.uhn.hl7v2.model.GenericMessage;
43 import ca.uhn.hl7v2.model.GenericSegment;
44 import ca.uhn.hl7v2.model.Group;
45 import ca.uhn.hl7v2.model.Message;
46 import ca.uhn.hl7v2.model.Segment;
47 import ca.uhn.hl7v2.model.Type;
48 import ca.uhn.hl7v2.util.ReflectionUtil;
49 import ca.uhn.hl7v2.util.StringUtil;
50 import ca.uhn.hl7v2.util.Terser;
51 import ca.uhn.hl7v2.validation.ValidationContext;
52 import ca.uhn.hl7v2.validation.ValidationExceptionHandler;
53 import ca.uhn.hl7v2.validation.ValidationExceptionHandlerFactory;
54 import ca.uhn.hl7v2.validation.Validator;
55
56 /**
57 * Parses HL7 message Strings into HL7 Message objects and encodes HL7 Message objects into HL7
58 * message Strings.
59 *
60 * @author Bryan Tripp (bryan_tripp@sourceforge.net)
61 * @author Christian Ohr
62 */
63 public abstract class Parser extends HapiContextSupport {
64
65 private static final Logger log = LoggerFactory.getLogger(Parser.class);
66
67 /**
68 * Uses DefaultModelClassFactory for model class lookup.
69 */
70 public Parser() {
71 this(new DefaultHapiContext());
72 }
73
74 /**
75 * Creates a new parser, using the {@link ModelClassFactory}, the {@link ParserConfiguration}
76 * and the {@link ValidationContext} as defined in the context.
77 *
78 * @param context HapiContext
79 */
80 public Parser(HapiContext context) {
81 super(context);
82 }
83
84 /**
85 * Initialize parser with custom ModelClassFactory and default ValidationContext
86 *
87 * @param modelClassFactory custom factory to use for model class lookup
88 */
89 public Parser(ModelClassFactory modelClassFactory) {
90 this(new DefaultHapiContext(modelClassFactory));
91 }
92
93 /**
94 * @return the factory used by this Parser for model class lookup
95 */
96 public ModelClassFactory getFactory() {
97 return getHapiContext().getModelClassFactory();
98 }
99
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(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 HL7Exceptionl#HL7Exception">HL7Exception e = new HL7Exception("Validation has failed");
620 e.setDetail(result);
621 if (result instanceof Message/uhn/hl7v2/model/Message.html#Message">Message) e.setResponseMessage((Message)result);
622 throw e;
623 }
624 }
625
626 private boolean isValidating() {
627 return getHapiContext().getParserConfiguration().isValidating();
628 }
629
630
631 }