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