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.Arrays;
035    import java.util.Collections;
036    import java.util.HashMap;
037    import java.util.List;
038    import java.util.Map;
039    import java.util.Properties;
040    
041    import ca.uhn.hl7v2.HL7Exception;
042    import ca.uhn.hl7v2.model.GenericMessage;
043    import ca.uhn.hl7v2.model.Group;
044    import ca.uhn.hl7v2.model.Message;
045    import ca.uhn.hl7v2.model.Segment;
046    import ca.uhn.hl7v2.model.Type;
047    import ca.uhn.hl7v2.validation.MessageValidator;
048    import ca.uhn.hl7v2.validation.ValidationContext;
049    import ca.uhn.hl7v2.validation.ValidationException;
050    import ca.uhn.hl7v2.validation.impl.DefaultValidation;
051    import ca.uhn.hl7v2.validation.impl.ValidationContextFactory;
052    import ca.uhn.log.HapiLog;
053    import ca.uhn.log.HapiLogFactory;
054    
055    /**
056     * Parses HL7 message Strings into HL7 Message objects and 
057     * encodes HL7 Message objects into HL7 message Strings.  
058     * @author Bryan Tripp (bryan_tripp@sourceforge.net)
059     */
060    public abstract class Parser {
061    
062        private static final HapiLog log = HapiLogFactory.getHapiLog(Parser.class);
063        private static Map<String, Properties> messageStructures = null;
064        
065        private ModelClassFactory myFactory;
066        private ValidationContext myContext;
067        private MessageValidator myValidator;
068        private static final List<String> versions = Collections.unmodifiableList(Arrays.asList(new String[] { "2.1", "2.2", "2.3", "2.3.1", "2.4", "2.5", "2.5.1", "2.6" }));
069    
070    
071        /**
072         * Uses DefaultModelClassFactory for model class lookup. 
073         */
074        public Parser() {
075            this(null);
076        }
077        
078        /**
079         * @param theFactory custom factory to use for model class lookup 
080         */
081        public Parser(ModelClassFactory theFactory) {
082            if (theFactory == null) {
083                    theFactory = new DefaultModelClassFactory();
084            }
085            
086            myFactory = theFactory;
087            ValidationContext validationContext;
088                    try {
089                            validationContext = ValidationContextFactory.getContext();
090                    } catch (ValidationException e) {
091                            log.warn("Failed to get a validation context from the " + 
092                                            "ValidationContextFactory", e);
093                            validationContext = new DefaultValidation();
094                    }
095            setValidationContext(validationContext);
096        }
097        
098        /**
099         * @return the factory used by this Parser for model class lookup
100         */
101        public ModelClassFactory getFactory() {
102            return myFactory;
103        }
104        
105        /**
106         * @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>
107         */
108        public ValidationContext getValidationContext() {
109            return myContext;
110        }
111        
112        /**
113         * @param theContext the set of validation rules to be applied to messages parsed or 
114         *      encoded by this parser (defaults to ValidationContextFactory.DefaultValidation)
115         */
116        public void setValidationContext(ValidationContext theContext) {
117            myContext = theContext;
118            myValidator = new MessageValidator(theContext, true);
119        }
120    
121        /**
122         * Returns a String representing the encoding of the given message, if 
123         * the encoding is recognized.  For example if the given message appears 
124         * to be encoded using HL7 2.x XML rules then "XML" would be returned.  
125         * If the encoding is not recognized then null is returned.  That this 
126         * method returns a specific encoding does not guarantee that the 
127         * message is correctly encoded (e.g. well formed XML) - just that  
128         * it is not encoded using any other encoding than the one returned.  
129         * Returns null if the encoding is not recognized.  
130         */
131        public abstract String getEncoding(String message);
132    
133        /** 
134         * Returns true if and only if the given encoding is supported 
135         * by this Parser.  
136         */
137        public abstract boolean supportsEncoding(String encoding);
138        
139        /**
140         * @return the preferred encoding of this Parser
141         */
142        public abstract String getDefaultEncoding();
143    
144        /**
145         * Parses a message string and returns the corresponding Message object.
146         *   
147         * @param message a String that contains an HL7 message 
148         * @return a HAPI Message object parsed from the given String 
149         * @throws HL7Exception if the message is not correctly formatted.  
150         * @throws EncodingNotSupportedException if the message encoded 
151         *      is not supported by this parser.   
152         */
153        public Message parse(String message) throws HL7Exception, EncodingNotSupportedException {
154            String encoding = getEncoding(message);
155            if (!supportsEncoding(encoding)) {
156                throw new EncodingNotSupportedException(
157                        "Can't parse message beginning " + message.substring(0, Math.min(message.length(), 50)));
158            }
159            
160            String version = getVersion(message);
161            if (!validVersion(version)) {
162                throw new HL7Exception("Can't process message of version '" + version + "' - version not recognized",
163                        HL7Exception.UNSUPPORTED_VERSION_ID);
164            }
165            
166            myValidator.validate(message, encoding.equals("XML"), version);        
167            Message result = doParse(message, version);
168            myValidator.validate(result);
169    
170            result.setParser(this);
171            
172            return result;
173        }
174        
175        /**
176         * Called by parse() to perform implementation-specific parsing work.  
177         * 
178         * @param message a String that contains an HL7 message 
179         * @param version the name of the HL7 version to which the message belongs (eg "2.5") 
180         * @return a HAPI Message object parsed from the given String 
181         * @throws HL7Exception if the message is not correctly formatted.  
182         * @throws EncodingNotSupportedException if the message encoded 
183         *      is not supported by this parser.   
184         */
185        protected abstract Message doParse(String message, String version) 
186            throws HL7Exception, EncodingNotSupportedException;
187    
188        /** 
189         * Formats a Message object into an HL7 message string using the given 
190         * encoding. 
191         * 
192         * @param source a Message object from which to construct an encoded message string 
193         * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support only  
194         *      one encoding) 
195         * @return the encoded message 
196         * @throws HL7Exception if the data fields in the message do not permit encoding 
197         *      (e.g. required fields are null)
198         * @throws EncodingNotSupportedException if the requested encoding is not 
199         *      supported by this parser.  
200         */
201        public String encode(Message source, String encoding) throws HL7Exception, EncodingNotSupportedException {
202            myValidator.validate(source);        
203            String result = doEncode(source, encoding);
204            myValidator.validate(result, encoding.equals("XML"), source.getVersion());
205            
206            return result;
207        }
208    
209        /**
210         * Called by encode(Message, String) to perform implementation-specific encoding work. 
211         * 
212         * @param source a Message object from which to construct an encoded message string 
213         * @param encoding the name of the HL7 encoding to use (eg "XML"; most implementations support only 
214         *      one encoding) 
215         * @return the encoded message 
216         * @throws HL7Exception if the data fields in the message do not permit encoding 
217         *      (e.g. required fields are null)
218         * @throws EncodingNotSupportedException if the requested encoding is not 
219         *      supported by this parser.  
220         */
221        protected abstract String doEncode(Message source, String encoding) 
222            throws HL7Exception, EncodingNotSupportedException;
223        
224        /** 
225         * Formats a Message object into an HL7 message string using this parser's  
226         * default encoding. 
227         * 
228         * @param source a Message object from which to construct an encoded message string 
229         * @param encoding the name of the encoding to use (eg "XML"; most implementations support only one 
230         *      encoding) 
231         * @return the encoded message 
232         * @throws HL7Exception if the data fields in the message do not permit encoding 
233         *      (e.g. required fields are null)
234         */
235        public String encode(Message source) throws HL7Exception {
236            String encoding = getDefaultEncoding();
237            
238            myValidator.validate(source);        
239            String result = doEncode(source);
240            myValidator.validate(result, encoding.equals("XML"), source.getVersion());
241            
242            return result;
243        }
244    
245        /**
246         * Called by encode(Message) to perform implementation-specific encoding work. 
247         * 
248         * @param source a Message object from which to construct an encoded message string 
249         * @return the encoded message 
250         * @throws HL7Exception if the data fields in the message do not permit encoding 
251         *      (e.g. required fields are null)
252         * @throws EncodingNotSupportedException if the requested encoding is not 
253         *      supported by this parser.  
254         */
255        protected abstract String doEncode(Message source) throws HL7Exception;
256        
257        /**
258         * <p>Returns a minimal amount of data from a message string, including only the 
259         * data needed to send a response to the remote system.  This includes the 
260         * following fields: 
261         * <ul><li>field separator</li>
262         * <li>encoding characters</li>
263         * <li>processing ID</li>
264         * <li>message control ID</li></ul>
265         * This method is intended for use when there is an error parsing a message, 
266         * (so the Message object is unavailable) but an error message must be sent 
267         * back to the remote system including some of the information in the inbound
268         * message.  This method parses only that required information, hopefully 
269         * avoiding the condition that caused the original error.</p>  
270         * @return an MSH segment 
271         */
272        public abstract Segment getCriticalResponseData(String message) throws HL7Exception;
273    
274        /**
275         * For response messages, returns the value of MSA-2 (the message ID of the message 
276         * sent by the sending system).  This value may be needed prior to main message parsing, 
277         * so that (particularly in a multi-threaded scenario) the message can be routed to 
278         * the thread that sent the request.  We need this information first so that any 
279         * parse exceptions are thrown to the correct thread.  Implementers of Parsers should 
280         * take care to make the implementation of this method very fast and robust.  
281         * Returns null if MSA-2 can not be found (e.g. if the message is not a 
282         * response message). 
283         */
284        public abstract String getAckID(String message);
285    
286        /**
287         * Returns the version ID (MSH-12) from the given message, without fully parsing the message. 
288         * The version is needed prior to parsing in order to determine the message class
289         * into which the text of the message should be parsed. 
290         * @throws HL7Exception if the version field can not be found. 
291         */
292        public abstract String getVersion(String message) throws HL7Exception;
293    
294    
295        /**
296         * Encodes a particular segment and returns the encoded structure
297         *
298         * @param structure The structure to encode
299         * @param encodingCharacters The encoding characters
300         * @return The encoded segment
301         * @throws HL7Exception If there is a problem encoding
302         * @since 1.0
303         */
304        public abstract String doEncode(Segment structure, EncodingCharacters encodingCharacters) throws HL7Exception;
305    
306    
307        /**
308         * Encodes a particular type and returns the encoded structure
309         *
310         * @param type The type to encode
311         * @param encodingCharacters The encoding characters
312         * @return The encoded type
313         * @throws HL7Exception If there is a problem encoding
314         * @since 1.0
315         */
316        public abstract String doEncode(Type type, EncodingCharacters encodingCharacters) throws HL7Exception;
317    
318    
319        /**
320         * Parses a particular type and returns the encoded structure
321         *
322         * @param string The string to parse
323         * @param type The type to encode
324         * @param encodingCharacters The encoding characters
325         * @return The encoded type
326         * @throws HL7Exception If there is a problem encoding
327         * @since 1.0
328         */
329        public abstract void parse(Type type, String string, EncodingCharacters encodingCharacters) throws HL7Exception;
330    
331    
332        /**
333         * Parses a particular segment and returns the encoded structure
334         *
335         * @param string The string to parse
336         * @param segment The segment to encode
337         * @param encodingCharacters The encoding characters
338         * @return The encoded type
339         * @throws HL7Exception If there is a problem encoding
340         */
341        public abstract void parse(Segment segment, String string, EncodingCharacters encodingCharacters) throws HL7Exception;
342    
343    
344        /**
345         * Parses a particular message and returns the encoded structure
346         *
347         * @param string The string to parse
348         * @param message The message to encode
349         * @return The encoded type
350         * @throws HL7Exception If there is a problem encoding
351         * @since 1.0
352         */
353        public abstract void parse(Message message, String string) throws HL7Exception;
354    
355    
356        /**
357         * Creates a version-specific MSH object and returns it as a version-independent 
358         * MSH interface. 
359         * throws HL7Exception if there is a problem, e.g. invalid version, code not available 
360         *     for given version. 
361         */
362        public static Segment makeControlMSH(String version, ModelClassFactory factory) throws HL7Exception {
363            Segment msh = null;
364            String className = null;
365            
366            
367            try {
368                Message dummy = (Message) GenericMessage.getGenericMessageClass(version)
369                    .getConstructor(new Class[]{ModelClassFactory.class}).newInstance(new Object[]{factory});
370                
371                Class[] constructorParamTypes = { Group.class, ModelClassFactory.class };
372                Object[] constructorParamArgs = { dummy, factory };
373                Class c = factory.getSegmentClass("MSH", version);
374                Constructor constructor = c.getConstructor(constructorParamTypes);
375                msh = (Segment) constructor.newInstance(constructorParamArgs);
376            }
377            catch (Exception e) {
378                throw new HL7Exception(
379                    "Couldn't create MSH for version " + version + " (does your classpath include this version?) ... ",
380                    HL7Exception.APPLICATION_INTERNAL_ERROR,
381                    e);
382            }
383            return msh;
384        }
385    
386        /** 
387         * Returns true if the given string represents a valid 2.x version.  Valid versions 
388         * include "2.0", "2.0D", "2.1", "2.2", "2.3", "2.3.1", "2.4", "2.5", etc 
389         */
390        public static boolean validVersion(String version) {
391            return versions.contains(version);
392        }
393        
394        
395        /**
396         * @return A list of strings containing the valid versions of HL7 supported by HAPI ("2.1", "2.2", etc)
397         */
398        public static List<String> getValidVersions() {
399            return versions;
400        }
401        
402        
403        
404        /**
405         * Given a concatenation of message type and event (e.g. ADT_A04), and the 
406         * version, finds the corresponding message structure (e.g. ADT_A01).  This  
407         * is needed because some events share message structures, although it is not needed
408         * when the message structure is explicitly valued in MSH-9-3. 
409         * If no mapping is found, returns the original name.
410         * @throws HL7Exception if there is an error retrieving the map, or if the given 
411         *      version is invalid  
412         */
413        public static String getMessageStructureForEvent(String name, String version) throws HL7Exception {
414            String structure = null;
415            
416            if (!validVersion(version)) 
417                throw new HL7Exception("The version " + version + " is unknown");
418            
419            Properties p = null; 
420            try {
421                p = (Properties) getMessageStructures().get(version);            
422                
423                if (p == null) 
424                    throw new HL7Exception("No map found for version " + version + ". Only the following are available: " + getMessageStructures().keySet());
425                
426            } catch (IOException ioe) {
427                throw new HL7Exception(ioe);
428            }
429            
430            structure = p.getProperty(name);
431            
432            if (structure == null) { 
433                structure = name;
434            }
435            
436            return structure;
437        }
438    
439        
440        /**
441         * Returns a copy of the message structure map for a specific version.
442         * Each key is a message type (e.g. ADT_A04) and each value is the
443         * corresponding structure (e.g. ADT_A01).
444         *
445         * @throws IOException If the event map can't be loaded
446         */
447        public static Properties getMessageStructures(String version) throws IOException {
448            Map<String, Properties> msgStructures = getMessageStructures();
449            if (!msgStructures.containsKey(version)) {
450                return null;
451            }
452    
453            return (Properties) msgStructures.get(version).clone();
454        }
455    
456    
457        /**
458         * Returns version->event->structure maps.
459         */
460        private synchronized static Map<String, Properties> getMessageStructures() throws IOException {
461            if (messageStructures == null) {
462                messageStructures = loadMessageStructures();
463            }
464            return messageStructures;
465        }
466        
467        private static Map<String, Properties> loadMessageStructures() throws IOException {
468            Map<String, Properties> map = new HashMap<String, Properties>();
469            for (String version : versions) {
470                String resource = "ca/uhn/hl7v2/parser/eventmap/" + version + ".properties";
471                InputStream in = Parser.class.getClassLoader().getResourceAsStream(resource);
472                
473                Properties structures = null;
474                if (in != null) {            
475                    structures = new Properties();
476                    structures.load(in);
477                    map.put(version, structures);
478                }
479                
480            }
481            return map;
482        }
483        
484        /**
485         * Note that the validation context of the resulting message is set to this parser's validation 
486         * context.  The validation context is used within Primitive.setValue().
487         *   
488         * @param name name of the desired structure in the form XXX_YYY
489         * @param version HL7 version (e.g. "2.3")  
490         * @param isExplicit true if the structure was specified explicitly in MSH-9-3, false if it 
491         *      was inferred from MSH-9-1 and MSH-9-2.  If false, a lookup may be performed to find 
492         *      an alternate structure corresponding to that message type and event.   
493         * @return a Message instance 
494         * @throws HL7Exception if the version is not recognized or no appropriate class can be found or the Message 
495         *      class throws an exception on instantiation (e.g. if args are not as expected) 
496         */
497        protected Message instantiateMessage(String theName, String theVersion, boolean isExplicit) throws HL7Exception {
498            Message result = null;
499            
500            try {
501                Class messageClass = myFactory.getMessageClass(theName, theVersion, isExplicit);
502                if (messageClass == null)
503                    throw new ClassNotFoundException("Can't find message class in current package list: " 
504                        + theName);
505                log.info("Instantiating msg of class " + messageClass.getName());
506                Constructor constructor = messageClass.getConstructor(new Class[]{ModelClassFactory.class});
507                result = (Message) constructor.newInstance(new Object[]{myFactory});
508            } catch (Exception e) {            
509                throw new HL7Exception("Couldn't create Message object of type " + theName,
510                    HL7Exception.UNSUPPORTED_MESSAGE_TYPE, e);
511            }
512            
513            result.setValidationContext(myContext);
514            
515            return result;
516        }
517        
518    }