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}