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}