View Javadoc
1   /**
2   The contents of this file are subject to the Mozilla Public License Version 1.1
3   (the "License"); you may not use this file except in compliance with the License.
4   You may obtain a copy of the License at http://www.mozilla.org/MPL/
5   Software distributed under the License is distributed on an "AS IS" basis,
6   WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
7   specific language governing rights and limitations under the License.
8   
9   The Initial Developer of the Original Code is University Health Network. Copyright (C)
10  2001.  All Rights Reserved.
11  
12  Contributor(s): ______________________________________.
13  
14  Alternatively, the contents of this file may be used under the terms of the
15  GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
16  applicable instead of those above.  If you wish to allow use of your version of this
17  file only under the terms of the GPL and not to allow others to use your version
18  of this file under the MPL, indicate your decision by deleting  the provisions above
19  and replace  them with the notice and other provisions required by the GPL License.
20  If you do not delete the provisions above, a recipient may use your version of
21  this file under either the MPL or the GPL.
22  
23  */
24  package ca.uhn.hl7v2.parser;
25  
26  import java.io.BufferedReader;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.InputStreamReader;
30  import java.text.MessageFormat;
31  import java.util.ArrayList;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import ca.uhn.hl7v2.ErrorCode;
40  import ca.uhn.hl7v2.HL7Exception;
41  import ca.uhn.hl7v2.Version;
42  import ca.uhn.hl7v2.model.GenericMessage;
43  import ca.uhn.hl7v2.model.Group;
44  import ca.uhn.hl7v2.model.Message;
45  import ca.uhn.hl7v2.model.Segment;
46  import ca.uhn.hl7v2.model.Type;
47  
48  /**
49   * Default implementation of ModelClassFactory.  See packageList() for configuration instructions. 
50   * 
51   * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
52   * @version $Revision: 1.9 $ updated on $Date: 2010-08-05 17:51:16 $ by $Author: jamesagnew $
53   */
54  public class DefaultModelClassFactory extends AbstractModelClassFactory {
55  
56      private static final long serialVersionUID = 1;
57  
58      private static final Logger log = LoggerFactory.getLogger(DefaultModelClassFactory.class);
59      
60      static final String CUSTOM_PACKAGES_RESOURCE_NAME_TEMPLATE = "custom_packages/{0}";
61      private static final Map<String, String[]> packages = new HashMap<>();
62      private static List<String> ourVersions = null;
63  
64      static {
65          reloadPackages();
66      }
67      
68      
69      /** 
70       * <p>Attempts to return the message class corresponding to the given name, by 
71       * searching through default and user-defined (as per packageList()) packages. 
72       * Returns GenericMessage if the class is not found.</p>
73       * <p>It is important to note that there can only be one implementation of a particular message 
74       * structure (i.e. one class with the message structure name, regardless of its package) among 
75       * the packages defined as per the <code>packageList()</code> method.  If there are duplicates 
76       * (e.g. two ADT_A01 classes) the first one in the search order will always be used.  However, 
77       * this restriction only applies to message classes, not (normally) segment classes, etc.  This is because 
78       * classes representing parts of a message are referenced explicitly in the code for the message 
79       * class, rather than being looked up (using findMessageClass() ) based on the String value of MSH-9. 
80       * The exception is that Segments may have to be looked up by name when they appear 
81       * in unexpected locations (e.g. by local extension) -- see findSegmentClass().</p>  
82       * <p>Note: the current implementation will be slow if there are multiple user-
83       * defined packages, because the JVM will try to load a number of non-existent 
84       * classes every parse.  This should be changed so that specific classes, rather 
85       * than packages, are registered by name.</p>
86       * 
87       * @param theName name of the desired structure in the form XXX_YYY
88       * @param theVersion HL7 version (e.g. "2.3")
89       * @param isExplicit true if the structure was specified explicitly in MSH-9-3, false if it 
90       *      was inferred from MSH-9-1 and MSH-9-2.  If false, a lookup may be performed to find 
91       *      an alternate structure corresponding to that message type and event.   
92       * @return corresponding message subclass if found; GenericMessage otherwise
93       */
94      @SuppressWarnings("unchecked")
95  	public Class<? extends Message> getMessageClass(String theName, String theVersion, boolean isExplicit) throws HL7Exception {
96          if (!isExplicit) {
97          	theName = getMessageStructureForEvent(theName, Version.versionOf(theVersion));
98          }
99          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) {
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<>();
266         
267         if ( resourceInputStream != null) {
268 
269             try (BufferedReader in = new BufferedReader(new InputStreamReader(resourceInputStream))) {
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         return packageList.toArray(new String[0]);
289     }
290 
291     /**
292      * Finds a message or segment class by name and version.
293      * @param name the segment or message structure name 
294      * @param version the HL7 version
295      * @param type 'message', 'group', 'segment', or 'datatype'  
296      */
297     private static Class<?> findClass(String name, String version, String type) throws HL7Exception {
298         Parser.assertVersionExists(version);
299 
300         //get list of packages to search for the corresponding message class 
301         String[] packageList = packageList(version);
302 
303         if (packageList == null) {
304         	return null;
305         }
306         
307         //get subpackage for component type
308         String types = "message|group|segment|datatype";
309         if (!types.contains(type))
310             throw new HL7Exception("Can't find " + name + " for version " + version 
311                         + " -- type must be " + types + " but is " + type);
312         
313         //try to load class from each package
314         Class<?> compClass = null;
315         int c = 0;
316         while (compClass == null && c < packageList.length) {
317             String classNameToTry = null;
318             try {
319                 String p = packageList[c];
320                 if (!p.endsWith("."))
321                     p = p + ".";
322                 classNameToTry = p + type + "." + name;
323 
324                 if (log.isDebugEnabled()) {
325                     log.debug("Trying to load: {}", classNameToTry);                    
326                 }
327                 compClass = Class.forName(classNameToTry);
328                 if (log.isDebugEnabled()) {
329                     log.debug("Loaded: {} class: {}", classNameToTry, compClass);                    
330                 }
331             }
332             catch (ClassNotFoundException cne) {
333                 log.debug("Failed to load: {}", classNameToTry);                    
334                 /* just try next one */
335             }
336             c++;
337         }
338         return compClass;
339     }
340 
341 
342     /**
343 	 * Reloads the packages. Note that this should not be performed
344 	 * after and messages have been parsed or otherwise generated,
345 	 * as undetermined behaviour may result. 
346 	 */
347 	public static void reloadPackages() {
348         packages.clear();
349         ourVersions = new ArrayList<>();
350         for (Version v : Version.values()) {
351             try {
352                 String[] versionPackages = loadPackages(v.getVersion());
353                 if (versionPackages.length > 0) {
354                     ourVersions.add(v.getVersion());
355                 }
356                 packages.put(v.getVersion(), versionPackages);
357             } catch (HL7Exception e) {
358                 throw new Error("Version \"" + v.getVersion() + "\" is invalid. This is a programming error: ", e);
359             }
360         }		
361 	}
362 
363 	
364 	/**
365 	 * Returns a string containing the highest known version of HL7 known to HAPI (i.e. "2.6"). Note that this
366 	 * is determined by checking which structure JARs are available on the classpath, so if this release of
367 	 * HAPI supports version 2.6, but only the hapi-structures-v23.jar is available on the classpath,
368 	 * "2.3" will be returned
369      *
370      * @return the most recent HL7 version known to HAPI
371 	 */
372 	public static String getHighestKnownVersion() {
373 	    if (ourVersions == null || ourVersions.size() == 0) {
374 	        return null;
375 	    }
376 	    return ourVersions.get(ourVersions.size() - 1);
377 	}
378 	
379 	/**
380 	 * Returns the event structure. If nothing could be found, the event name is returned
381 	 * 
382 	 * @see ca.uhn.hl7v2.parser.AbstractModelClassFactory#getMessageStructureForEvent(java.lang.String, ca.uhn.hl7v2.Version)
383 	 */
384 	@Override
385 	public String getMessageStructureForEvent(String name, Version version) throws HL7Exception {
386 		String structure = super.getMessageStructureForEvent(name, version);
387 		return structure != null ? structure : name;
388 	}
389 
390 
391 
392 }