001package ca.uhn.hl7v2.util; 002 003import java.io.BufferedReader; 004import java.io.File; 005import java.io.FileFilter; 006import java.io.FileReader; 007import java.io.FilenameFilter; 008import java.io.IOException; 009import java.util.HashMap; 010import java.util.Map; 011import java.util.StringTokenizer; 012 013import org.slf4j.Logger; 014import org.slf4j.LoggerFactory; 015 016import ca.uhn.hl7v2.ErrorCode; 017import ca.uhn.hl7v2.HL7Exception; 018 019/** 020 * <p>Implements CodeMapper using files to store code values. Files are arranged 021 * in the following directory structure. The base directory is called "codemap". 022 * This should be created under the hapi.home directory (see Home class; defaults to .). 023 * Under the base directory, there should be one directory for each interface, and 024 * each of these directories should be named after the interface. For example if you 025 * had interfaces to pharmacy and lab systems you might have the following directories:</p> 026 * <p> <hapi.home>/codemap/pharmacy<br> 027 * <hapi.home>/codemap/lab</p> 028 * <p>Each directory should contain two files per HL7 table, named after the table numbers as 029 * follows: "hl7nnnn.li" and "hl7nnnn.il", where nnnn is the 4 digit table number. The .il 030 * file contains maps from interface codes to local codes, and the .li file contains maps from 031 * local codes to interface codes (these unfortunately may not be symmetrical).</p> 032 * <p>Each line of a file contains a single code map, with the "from" value and the "to" value 033 * separated by a tab. For example, the file <hapi.home>/lab/HL70001.li (to map local codes to interface 034 * codes for the HL7 admnistrative sex table in your lab system interface) might contain the 035 * following line: </p> 036 * <p>male&tab;M</p> 037 * <p>This means that the local code "male" maps to the interface code "M".</p> 038 * <p>Lines that start with "//" are treated as comments.</p> 039 * @author Bryan Tripp 040 */ 041public class FileCodeMapper extends CodeMapper { 042 043 private static final Logger log = LoggerFactory.getLogger(FileCodeMapper.class); 044 045 private boolean throwIfNoMatch = false; 046 final File baseDir; 047 private Map<String, Map<String, Map<String, String>>> interfaceToLocal; 048 private Map<String, Map<String, Map<String, String>>> localToInterface; 049 050 /** 051 * Creates a new instance of FileCodeMapper. You should probably not 052 * construct a FileCodeMapper directly. Use CodeMapper.getInstance() 053 * instead ... this will ensure that only a single instance is created, 054 * which is important for performance because code maps are loaded from 055 * disk every time this constructor is called. 056 */ 057 public FileCodeMapper() throws HL7Exception { 058 baseDir = new File(Home.getHomeDirectory().getAbsolutePath() + "/codemap"); 059 refreshCache(); 060 } 061 062 /** 063 * If values are cached in such a way that they are not guaranteed to be current, a call 064 * to this method refreshes the values. 065 */ 066 public void refreshCache() throws HL7Exception { 067 localToInterface = new HashMap<>(10); 068 interfaceToLocal = new HashMap<>(10); 069 070 log.info("Refreshing cache"); 071 072 try { 073 //get list of child directories 074 File[] interfaceDirs = this.baseDir.listFiles(pathname -> { 075 boolean acc = false; 076 if (pathname.isDirectory()) 077 acc = true; 078 return acc; 079 }); 080 081 //loop through directories and set up maps 082 for (File interfaceDir : interfaceDirs) { 083 084 log.info( 085 "Checking directory {} for interface code maps.", interfaceDir.getName()); 086 087 //get list of .li (local -> interface) and .il (interface -> local) files 088 File[] mapFiles = interfaceDir.listFiles((dir, name) -> { 089 boolean acc = false; 090 if (name.toUpperCase().startsWith("HL7")) { 091 if (name.substring(name.lastIndexOf('.')).equals(".li") 092 || name.substring(name.lastIndexOf('.')).equals(".il")) 093 acc = true; 094 } 095 return acc; 096 }); 097 098 //read map entries from each file and add to hash maps for li and il codes 099 HashMap<String, Map<String, String>> li = new HashMap<>(50); 100 HashMap<String, Map<String, String>> il = new HashMap<>(50); 101 for (File mapFile : mapFiles) { 102 log.info("Reading map entries from file {}", mapFile); 103 104 String fName = mapFile.getName(); 105 String tableName = fName.substring(0, fName.lastIndexOf('.')); 106 String mapDirection = fName.substring(fName.lastIndexOf('.') + 1); 107 108 //read values and store in HashMap 109 Map<String, String> codeMap = new HashMap<>(25); 110 try (BufferedReader in = new BufferedReader(new FileReader(mapFile))) { 111 while (in.ready()) { 112 String line = in.readLine(); 113 if (!line.startsWith("//")) { 114 StringTokenizer tok = new StringTokenizer(line, "\t", false); 115 String from = null; 116 String to = null; 117 if (tok.hasMoreTokens()) 118 from = tok.nextToken(); 119 if (tok.hasMoreTokens()) 120 to = tok.nextToken(); 121 if (from != null && to != null) 122 codeMap.put(from, to); 123 } 124 } 125 } 126 127 //add to appropriate map for this interface 128 if (mapDirection.equals("il")) { 129 il.put(tableName.toUpperCase(), codeMap); 130 log.debug("Adding {} codes to interface -> local map for {} in {} interface", 131 codeMap.size(), tableName, interfaceDir.getName()); 132 } else { 133 li.put(tableName.toUpperCase(), codeMap); 134 log.debug("Adding {} codes to local -> interface map for {} in {} interface", 135 codeMap.size(), tableName, interfaceDir.getName()); 136 } 137 } 138 139 //add maps for this interface (this directory) to global list 140 interfaceToLocal.put(interfaceDir.getName(), il); 141 localToInterface.put(interfaceDir.getName(), li); 142 } 143 144 } 145 catch (IOException e) { 146 throw new HL7Exception( 147 "Can't read interface code maps from disk", e); 148 } 149 } 150 151 /** 152 * Returns the local code for the given interface code as it appears in 153 * the given interface. 154 */ 155 public String getLocalCode(String interfaceName, int hl7Table, String interfaceCode) throws HL7Exception { 156 String localCode = null; 157 try { 158 Map<String, Map<String, String>> interfaceMap = interfaceToLocal.get(interfaceName); 159 localCode = getCode(interfaceMap, hl7Table, interfaceCode); 160 } 161 catch (NullPointerException npe) { 162 if (this.throwIfNoMatch) 163 throw new HL7Exception( 164 "No local mapping for the interface code " 165 + interfaceCode 166 + " for HL7 table " 167 + hl7Table 168 + " for the interface '" 169 + interfaceName 170 + "'", 171 ErrorCode.TABLE_VALUE_NOT_FOUND); 172 } 173 return localCode; 174 } 175 176 /** 177 * Common code for getLocalcode and getInterfaceCode 178 */ 179 private String getCode(Map<String, Map<String, String>> interfaceMap, int hl7Table, String code) { 180 String ret; 181 182 //get map for the given table 183 StringBuilder tableName = new StringBuilder(); 184 tableName.append("HL7"); 185 if (hl7Table < 1000) 186 tableName.append("0"); 187 if (hl7Table < 100) 188 tableName.append("0"); 189 if (hl7Table < 10) 190 tableName.append("0"); 191 tableName.append(hl7Table); 192 Map<String, String> tableMap = interfaceMap.get(tableName.toString()); 193 194 //get code 195 ret = tableMap.get(code); 196 return ret; 197 } 198 199 /** 200 * Returns the interface code for the given local code, for use in the context 201 * of the given interface. 202 */ 203 public String getInterfaceCode(String interfaceName, int hl7Table, String localCode) throws HL7Exception { 204 String interfaceCode = null; 205 try { 206 Map<String, Map<String, String>> interfaceMap = localToInterface.get(interfaceName); 207 interfaceCode = getCode(interfaceMap, hl7Table, localCode); 208 } 209 catch (NullPointerException npe) { 210 if (this.throwIfNoMatch) 211 throw new HL7Exception( 212 "No interface mapping for the local code " 213 + localCode 214 + " for HL7 table " 215 + hl7Table 216 + " for the interface '" 217 + interfaceName 218 + "'", 219 ErrorCode.TABLE_VALUE_NOT_FOUND); 220 } 221 return interfaceCode; 222 } 223 224 /** 225 * Determines what happens if no matching code is found during a lookup. If set to true, 226 * an HL7Exception is thrown if there is no match. If false, null is returned. The default 227 * is false. 228 */ 229 public void throwExceptionIfNoMatch(boolean throwException) { 230 this.throwIfNoMatch = throwException; 231 } 232 233 /** 234 * Test harness. 235 */ 236 public static void main(String[] args) { 237 try { 238 //FileCodeMapper mapper = new FileCodeMapper(); 239 CodeMapper.getInstance().throwExceptionIfNoMatch(true); 240 System.out.println("Local code for M is " + CodeMapper.getLocal("test", 1, "M")); 241 System.out.println("Interface code for female is " + CodeMapper.getInt("test", 1, "female")); 242 243 } 244 catch (HL7Exception e) { 245 e.printStackTrace(); 246 } 247 } 248 249}