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