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 }