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 Initial Developer of the Original Code is University Health Network. Copyright (C)
010    2001.  All Rights Reserved.
011    
012    Contributor(s): ______________________________________.
013    
014    Alternatively, the contents of this file may be used under the terms of the
015    GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
016    applicable instead of those above.  If you wish to allow use of your version of this
017    file only under the terms of the GPL and not to allow others to use your version
018    of this file under the MPL, indicate your decision by deleting  the provisions above
019    and replace  them with the notice and other provisions required by the GPL License.
020    If you do not delete the provisions above, a recipient may use your version of
021    this file under either the MPL or the GPL.
022    
023    */
024    package ca.uhn.hl7v2.parser;
025    
026    import java.io.BufferedReader;
027    import java.io.IOException;
028    import java.io.InputStream;
029    import java.io.InputStreamReader;
030    import java.text.MessageFormat;
031    import java.util.ArrayList;
032    import java.util.HashMap;
033    import java.util.List;
034    
035    import org.slf4j.Logger;
036    import org.slf4j.LoggerFactory;
037    
038    import ca.uhn.hl7v2.HL7Exception;
039    import ca.uhn.hl7v2.Version;
040    import ca.uhn.hl7v2.model.GenericMessage;
041    import ca.uhn.hl7v2.model.Type;
042    import ca.uhn.hl7v2.model.Segment;
043    import ca.uhn.hl7v2.model.Message;
044    import ca.uhn.hl7v2.model.Group;
045    
046    /**
047     * Default implementation of ModelClassFactory.  See packageList() for configuration instructions. 
048     * 
049     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
050     * @version $Revision: 1.9 $ updated on $Date: 2010-08-05 17:51:16 $ by $Author: jamesagnew $
051     */
052    public class DefaultModelClassFactory implements ModelClassFactory {
053    
054        private static final long serialVersionUID = 1;
055    
056        private static final Logger log = LoggerFactory.getLogger(DefaultModelClassFactory.class);
057        
058        static final String CUSTOM_PACKAGES_RESOURCE_NAME_TEMPLATE = "custom_packages/{0}";
059        private static final HashMap<String, String[]> packages = new HashMap<String, String[]>();
060        private static List<String> ourVersions = null;
061    
062        static {
063            reloadPackages();
064        }
065        
066        
067        /** 
068         * <p>Attempts to return the message class corresponding to the given name, by 
069         * searching through default and user-defined (as per packageList()) packages. 
070         * Returns GenericMessage if the class is not found.</p>
071         * <p>It is important to note that there can only be one implementation of a particular message 
072         * structure (i.e. one class with the message structure name, regardless of its package) among 
073         * the packages defined as per the <code>packageList()</code> method.  If there are duplicates 
074         * (e.g. two ADT_A01 classes) the first one in the search order will always be used.  However, 
075         * this restriction only applies to message classes, not (normally) segment classes, etc.  This is because 
076         * classes representing parts of a message are referenced explicitly in the code for the message 
077         * class, rather than being looked up (using findMessageClass() ) based on the String value of MSH-9. 
078         * The exception is that Segments may have to be looked up by name when they appear 
079         * in unexpected locations (e.g. by local extension) -- see findSegmentClass().</p>  
080         * <p>Note: the current implementation will be slow if there are multiple user-
081         * defined packages, because the JVM will try to load a number of non-existent 
082         * classes every parse.  This should be changed so that specific classes, rather 
083         * than packages, are registered by name.</p>
084         * 
085         * @param name name of the desired structure in the form XXX_YYY
086         * @param version HL7 version (e.g. "2.3")  
087         * @param isExplicit true if the structure was specified explicitly in MSH-9-3, false if it 
088         *      was inferred from MSH-9-1 and MSH-9-2.  If false, a lookup may be performed to find 
089         *      an alternate structure corresponding to that message type and event.   
090         * @return corresponding message subclass if found; GenericMessage otherwise
091         */
092        @SuppressWarnings("unchecked")
093            public Class<? extends Message> getMessageClass(String theName, String theVersion, boolean isExplicit) throws HL7Exception {
094            Class<? extends Message> mc = null;
095            if (!isExplicit) {
096                theName = Parser.getMessageStructureForEvent(theName, theVersion);
097            } 
098            mc = (Class<? extends Message>) findClass(theName, theVersion, "message");
099            if (mc == null) 
100                mc = GenericMessage.getGenericMessageClass(theVersion);
101            return mc;
102        }
103    
104        /**
105         * @see ca.uhn.hl7v2.parser.ModelClassFactory#getGroupClass(java.lang.String, java.lang.String)
106         */
107        @SuppressWarnings("unchecked")
108            public Class<? extends Group> getGroupClass(String theName, String theVersion) throws HL7Exception {
109            return (Class<? extends Group>) findClass(theName, theVersion, "group");
110        }
111    
112        /** 
113         * @see ca.uhn.hl7v2.parser.ModelClassFactory#getSegmentClass(java.lang.String, java.lang.String)
114         */
115        @SuppressWarnings("unchecked")
116            public Class<? extends Segment> getSegmentClass(String theName, String theVersion) throws HL7Exception {
117            return (Class<? extends Segment>) findClass(theName, theVersion, "segment");
118        }
119    
120        /** 
121         * @see ca.uhn.hl7v2.parser.ModelClassFactory#getTypeClass(java.lang.String, java.lang.String)
122         */
123        @SuppressWarnings("unchecked")
124            public Class<? extends Type> getTypeClass(String theName, String theVersion) throws HL7Exception {
125            return (Class<? extends Type>) findClass(theName, theVersion, "datatype");
126        }
127        
128        /**
129         * Retrieves and instantiates a message class by looking in a specific java package for the 
130         * message type.
131         *  
132         * @param theName The message structure type (e.g. "ADT_A01")
133         * @param theVersion The HL7 version (e.g. "2.3.1")
134         * @param isExplicit If false, the message structure is looked up using {@link Parser#getMessageStructureForEvent(String, String)} and converted to the appropriate structure type. For example, "ADT_A04" would be converted to "ADT_A01" because the A04 trigger uses the A01 message structure according to HL7.
135         * @param packageName The package name to use. Note that if the message type can't be found in this package, HAPI will return the standard type returned by {@link #getMessageClass(String, String, boolean)}
136         * @since 1.3 
137         */
138            @SuppressWarnings("unchecked")
139            public Class<? extends Message> getMessageClassInASpecificPackage(String theName, String theVersion, boolean isExplicit, String packageName) throws HL7Exception { 
140            Class<? extends Message> mc = null;
141                
142            if (!isExplicit) { 
143                theName = Parser.getMessageStructureForEvent(theName, theVersion); 
144            } 
145            
146            mc = (Class<? extends Message>) findClassInASpecificPackage(theName, theVersion, "message", packageName); 
147            if (mc == null) {
148                mc = GenericMessage.getGenericMessageClass(theVersion);
149            }
150            
151            return mc; 
152        } 
153    
154    
155        private static Class<?> findClassInASpecificPackage(String name, String version, String type, String packageName) throws HL7Exception { 
156             
157                    if (packageName == null || packageName.length() == 0) { 
158                            return findClass(name, version, type); 
159                    }
160                    
161                    Class<?> compClass = null; 
162                    String classNameToTry = packageName + "." + name; 
163                     
164                    try { 
165                            compClass = Class.forName(classNameToTry); 
166                    } catch (ClassNotFoundException e) { 
167                            if (log.isDebugEnabled()) {
168                                    log.debug("Unable to find class " + classNameToTry + ", using default", e);
169                            }
170                            return findClass(name, version, type); 
171                    } 
172                     
173                    return compClass; 
174        } 
175        
176    
177        /**
178             * Returns the path to the base package for model elements of the given version
179             * - e.g. "ca/uhn/hl7v2/model/v24/".
180             * This package should have the packages datatype, segment, group, and message
181             * under it. The path ends in with a slash.
182             */
183            public static String getVersionPackagePath(String ver) throws HL7Exception {
184                if (Parser.validVersion(ver) == false) { 
185                    throw new HL7Exception("The HL7 version " + ver + " is not recognized", HL7Exception.UNSUPPORTED_VERSION_ID);
186                }
187                StringBuffer path = new StringBuffer("ca/uhn/hl7v2/model/v");
188                char[] versionChars = new char[ver.length()];
189                ver.getChars(0, ver.length(), versionChars, 0);
190                for (int i = 0; i < versionChars.length; i++) {
191                    if (versionChars[i] != '.') path.append(versionChars[i]);
192                }
193                path.append('/');
194                return path.toString();
195            }
196    
197            /**
198             * Returns the package name for model elements of the given version - e.g.
199             * "ca.uhn.hl7v2.model.v24.".  This method
200             * is identical to <code>getVersionPackagePath(...)</code> except that path
201             * separators are replaced with dots.
202             */
203            public static String getVersionPackageName(String ver) throws HL7Exception {
204                String path = DefaultModelClassFactory.getVersionPackagePath(ver);
205                String packg = path.replace('/', '.');
206                packg = packg.replace('\\', '.');
207                return packg;
208            }
209    
210            /** 
211         * <p>Lists all the packages (user-definable) where classes for standard and custom 
212         * messages may be found.  Each package has subpackages called "message", 
213         * "group", "segment", and "datatype" in which classes for these message elements 
214         * can be found. </p> 
215         * <p>At a minimum, this method returns the standard package for the 
216         * given version.  For example, for version 2.4, the package list contains <code>
217         * ca.uhn.hl7v2.model.v24</code>.  In addition, user-defined packages may be specified
218         * for custom messages.</p>
219         * <p>If you define custom message classes, and want Parsers to be able to 
220         * find them, you must register them as follows (otherwise you will get an exception when 
221         * the corresponding messages are parsed).  For each HL7 version you want to support, you must 
222         * put a text file on your classpath, under the folder /custom_packages, named after the version.  For example, 
223         * for version 2.4, you might put the file "custom_packages/2.4" in your application JAR.  Each line in the  
224         * file should name a package to search for message classes of that version.  For example, if you 
225         * work at foo.org, you might create a v2.4 message structure called "ZFO" and define it in the class
226         * <code>org.foo.hl7.custom.message.ZFO<code>.  In order for parsers to find this message
227         * class, you would need to enter the following line in custom_packages/2.4:</p>
228         * <p>org.foo.hl7.custom</p>
229         * <p>Packages are searched in the order specified.  The standard package for a given version
230         * is searched last, allowing you to override the default implementation.  Please note that 
231         * if you create custom classes for messages, segments, etc., their names must correspond exactly 
232         * to their names in the message text.  For example, if you subclass the QBP segment in order to 
233         * add your own fields, your subclass must also be called QBP. although it will obviously be in 
234         * a different package.  To make sure your class is used instead of the default implementation, 
235         * put your package in the package list.  User-defined packages are searched first, so yours 
236         * will be found first and used.  </p>
237         * <p>It is important to note that there can only be one implementation of a particular message 
238         * structure (i.e. one class with the message structure name, regardless of its package) among 
239         * the packages defined as per the <code>packageList()</code> method.  If there are duplicates 
240         * (e.g. two ADT_A01 classes) the first one in the search order will always be used.  However, 
241         * this restriction only applies to message classes, not segment classes, etc.  This is because 
242         * classes representing parts of a message are referenced explicitly in the code for the message 
243         * class, rather than being looked up (using findMessageClass() ) based on the String value of MSH-9.<p>  
244         */
245        public static String[] packageList(String version) throws HL7Exception {
246            //get package list for this version 
247            return packages.get(version);
248        }
249    
250        /**
251         * Returns a package list for the given version, including the standard package
252         * for that version and also user-defined packages (see packageList()). 
253         */
254        private static String[] loadPackages(String version) throws HL7Exception {
255            String[] retVal = null;
256            
257            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
258            
259            String customPackagesResourceName = 
260                MessageFormat.format( CUSTOM_PACKAGES_RESOURCE_NAME_TEMPLATE, new Object[] { version } );
261            
262            InputStream resourceInputStream = classLoader.getResourceAsStream( customPackagesResourceName );
263            
264            List<String> packageList = new ArrayList<String>();
265            
266            if ( resourceInputStream != null) {
267                BufferedReader in = new BufferedReader(new InputStreamReader(resourceInputStream));
268                
269                try {
270                    String line = in.readLine();
271                    while (line != null) {
272                        log.info( "Adding package to user-defined package list: {}", line );
273                        packageList.add( line );
274                        line = in.readLine();
275                    }
276                    
277                } catch (IOException e) {
278                    log.error( "Can't load all the custom package list - user-defined classes may not be recognized", e );
279                }
280                
281            }
282            else {
283                log.debug("No user-defined packages for version {}", version);
284            }
285    
286            //add standard package
287            packageList.add( getVersionPackageName(version) );
288            retVal = packageList.toArray(new String[]{});
289    
290            return retVal;
291        }
292    
293        /**
294         * Finds a message or segment class by name and version.
295         * @param name the segment or message structure name 
296         * @param version the HL7 version
297         * @param type 'message', 'group', 'segment', or 'datatype'  
298         */
299        private static Class<?> findClass(String name, String version, String type) throws HL7Exception {
300            if (Parser.validVersion(version) == false) {
301                throw new HL7Exception(
302                    "The HL7 version " + version + " is not recognized",
303                    HL7Exception.UNSUPPORTED_VERSION_ID);
304            }
305    
306            //get list of packages to search for the corresponding message class 
307            String[] packageList = packageList(version);
308    
309            if (packageList == null) {
310                    return null;
311            }
312            
313            //get subpackage for component type
314            String types = "message|group|segment|datatype";
315            if (types.indexOf(type) < 0) 
316                throw new HL7Exception("Can't find " + name + " for version " + version 
317                            + " -- type must be " + types + " but is " + type);
318            String subpackage = type;
319            
320            //try to load class from each package
321            Class<?> compClass = null;
322            int c = 0;
323            while (compClass == null && c < packageList.length) {
324                String classNameToTry = null;
325                try {
326                    String p = packageList[c];
327                    if (!p.endsWith("."))
328                        p = p + ".";
329                    classNameToTry = p + subpackage + "." + name;
330    
331                    if (log.isDebugEnabled()) {
332                        log.debug("Trying to load: {}", classNameToTry);                    
333                    }
334                    compClass = Class.forName(classNameToTry);
335                    if (log.isDebugEnabled()) {
336                        log.debug("Loaded: {} class: {}", classNameToTry, compClass);                    
337                    }
338                }
339                catch (ClassNotFoundException cne) {
340                    log.debug("Failed to load: {}", classNameToTry);                    
341                    /* just try next one */
342                }
343                c++;
344            }
345            return compClass;
346        }
347    
348    
349        /**
350             * Reloads the packages. Note that this should not be performed
351             * after and messages have been parsed or otherwise generated,
352             * as undetermined behaviour may result. 
353             */
354            public static void reloadPackages() {
355            packages.clear();
356            ourVersions = new ArrayList<String>();
357            for (Version v : Version.values()) {
358                try {
359                    String[] versionPackages = loadPackages(v.getVersion());
360                    if (versionPackages.length > 0) {
361                        ourVersions.add(v.getVersion());
362                    }
363                    packages.put(v.getVersion(), versionPackages);
364                } catch (HL7Exception e) {
365                    throw new Error("Version \"" + v.getVersion() + "\" is invalid. This is a programming error: ", e);
366                }
367            }               
368            }
369    
370            
371            /**
372             * Returns a string containing the highest known version of HL7 known to HAPI (i.e. "2.6"). Note that this
373             * is determined by checking which structure JARs are available on the classpath, so if this release of
374             * HAPI supports version 2.6, but only the hapi-structures-v23.jar is available on the classpath,
375             * "2.3" will be returned
376             */
377            public static String getHighestKnownVersion() {
378                if (ourVersions == null || ourVersions.size() == 0) {
379                    return null;
380                }
381                return ourVersions.get(ourVersions.size() - 1);
382            }
383    
384    
385    
386    }