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