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    }