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