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*/
028
029package ca.uhn.hl7v2.parser;
030
031import java.io.IOException;
032import java.io.InputStream;
033import java.lang.reflect.Constructor;
034import java.util.HashMap;
035import java.util.Map;
036import java.util.Properties;
037
038import org.slf4j.Logger;
039import org.slf4j.LoggerFactory;
040
041import ca.uhn.hl7v2.HL7Exception;
042import ca.uhn.hl7v2.Version;
043import ca.uhn.hl7v2.VersionLogger;
044import ca.uhn.hl7v2.model.GenericMessage;
045import ca.uhn.hl7v2.model.Group;
046import ca.uhn.hl7v2.model.Message;
047import ca.uhn.hl7v2.model.Segment;
048import ca.uhn.hl7v2.model.Type;
049import ca.uhn.hl7v2.validation.MessageValidator;
050import ca.uhn.hl7v2.validation.ValidationContext;
051import ca.uhn.hl7v2.validation.ValidationException;
052import 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 */
059public 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}