001package org.hl7.fhir.convertors.misc;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033
034import java.io.FileNotFoundException;
035import java.io.FileOutputStream;
036import java.io.IOException;
037
038import javax.xml.parsers.DocumentBuilder;
039import javax.xml.parsers.DocumentBuilderFactory;
040import javax.xml.parsers.ParserConfigurationException;
041
042import lombok.extern.slf4j.Slf4j;
043import org.hl7.fhir.exceptions.FHIRFormatError;
044import org.hl7.fhir.r5.formats.XmlParser;
045import org.hl7.fhir.r5.model.Bundle;
046import org.hl7.fhir.r5.model.Bundle.BundleType;
047import org.hl7.fhir.r5.model.CodeableConcept;
048import org.hl7.fhir.r5.model.Coding;
049import org.hl7.fhir.r5.model.DateTimeType;
050import org.hl7.fhir.r5.model.InstantType;
051import org.hl7.fhir.r5.model.Meta;
052import org.hl7.fhir.utilities.Utilities;
053import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
054import org.hl7.fhir.utilities.xml.XMLUtil;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.xml.sax.SAXException;
058import org.xmlpull.v1.XmlPullParserException;
059
060/**
061 * This class converts the LOINC XML representation that the FHIR build tool uses internally to a set of DataElements in an atom feed
062 * 
063 * @author Grahame
064 *
065 */
066@Slf4j
067public class LoincToDEConvertor {
068
069  @SuppressWarnings("checkstyle:systemout")
070        public static void main(String[] args) throws FHIRFormatError, IOException, XmlPullParserException, SAXException, ParserConfigurationException {
071                if (args.length == 0) {
072                        System.out.println("FHIR LOINC to CDE convertor. ");
073                        System.out.println("");
074                        System.out.println("This tool converts from LOINC to A set of DataElement definitions.");
075                        System.out.println("");
076                        System.out.println("Usage: [jar(path?)] [dest] (-defn [definitions]) where: ");
077                        System.out.println("* [dest] is a file name of the bundle to produce");
078                        System.out.println("* [definitions] is the file name of a file produced by exporting the main LOINC table from the mdb to XML");
079                        System.out.println("");
080                } else {
081                        LoincToDEConvertor exe = new LoincToDEConvertor();
082                        exe.setDest(args[0]);
083                        for (int i = 1; i < args.length; i++) {
084                                if (args[i].equals("-defn"))
085                                        exe.setDefinitions(args[i+1]);
086                        }
087                        exe.process();
088                }
089
090        }
091
092        private String dest;
093        private String definitions;
094        public String getDest() {
095                return dest;
096        }
097        public void setDest(String dest) {
098                this.dest = dest;
099        }
100        public String getDefinitions() {
101                return definitions;
102        }
103        public void setDefinitions(String definitions) {
104                this.definitions = definitions;
105        }
106
107        private Document xml;
108        private Bundle bundle;
109        private DateTimeType now;
110
111  public Bundle process(String sourceFile) throws FileNotFoundException, SAXException, IOException, ParserConfigurationException {
112    this.definitions = sourceFile;
113    log.info("Begin. Produce Loinc CDEs in "+dest+" from "+definitions);
114
115    loadLoinc();
116    log.info("LOINC loaded");
117
118    now = DateTimeType.now();
119
120    bundle = new Bundle();
121    bundle.setType(BundleType.COLLECTION);
122    bundle.setId("http://hl7.org/fhir/commondataelement/loinc");
123    bundle.setMeta(new Meta().setLastUpdatedElement(InstantType.now()));
124
125    processLoincCodes();
126    return bundle;
127  }
128  
129        public void process() throws FHIRFormatError, IOException, XmlPullParserException, SAXException, ParserConfigurationException {
130    log.info("Begin. Produce Loinc CDEs in "+dest+" from "+definitions);
131
132    loadLoinc();
133    log.info("LOINC loaded");
134
135    now = DateTimeType.now();
136
137                bundle = new Bundle();
138                bundle.setId("http://hl7.org/fhir/commondataelement/loinc");
139    bundle.setMeta(new Meta().setLastUpdatedElement(InstantType.now()));
140
141                processLoincCodes();
142                if (dest != null) {
143      log.info("Saving...");
144
145      saveBundle();
146                }
147    log.info("Done");
148
149  }
150
151  private void loadLoinc() throws FileNotFoundException, SAXException, IOException, ParserConfigurationException {
152                DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory();
153                factory.setNamespaceAware(true);
154                DocumentBuilder builder = factory.newDocumentBuilder();
155
156                xml = builder.parse(ManagedFileAccess.inStream(definitions)); 
157        }
158
159        private void saveBundle() throws FHIRFormatError, IOException, XmlPullParserException {
160                XmlParser xml = new XmlParser();
161                FileOutputStream s = ManagedFileAccess.outStream(dest);
162    xml.compose(s, bundle, true);
163    s.close();
164        }
165
166        private String col(Element row, String name) {
167                Element e = XMLUtil.getNamedChild(row, name);
168                if (e == null)
169                        return null;
170                String text = e.getTextContent();
171                return text;
172        }
173
174        private boolean hasCol(Element row, String name) {
175                return Utilities.noString(col(row, name));
176        }
177
178
179  /*
180 The following prints to both log and System to track progress. Ideally, this should be done with a more generic
181 progress tracking class.
182 */
183  @SuppressWarnings("checkstyle:systemout")
184        private void processLoincCodes() {
185                Element row = XMLUtil.getFirstChild(xml.getDocumentElement());
186                int i = 0;
187                while (row != null) {
188                        i++;
189                        if (i % 1000 == 0)
190        log.debug("Processed {} LOINC codes", i);
191                                System.out.print(".");
192                                String code = col(row, "LOINC_NUM");
193                                String comp = col(row, "COMPONENT");
194//                              DataElement de = new DataElement();
195//                              de.setId("loinc-"+code);
196//                  de.setMeta(new Meta().setLastUpdatedElement(InstantType.now()));
197//                              bundle.getEntry().add(new BundleEntryComponent().setResource(de));
198//                              Identifier id = new Identifier();
199//                              id.setSystem("http://hl7.org/fhir/commondataelement/loinc");
200//                              id.setValue(code);
201//                              de.addIdentifier(id);
202//                              de.setPublisher("Regenstrief + FHIR Project Team");
203//                              if (!col(row, "STATUS").equals("ACTIVE"))
204//                                      de.setStatus(PublicationStatus.DRAFT); // till we get good at this
205//                              else
206//                                      de.setStatus(PublicationStatus.RETIRED);
207//                              de.setDateElement(DateTimeType.now());
208//                              de.setName(comp);
209//                              ElementDefinition dee = de.addElement();
210//
211//                              // PROPERTY     ignore
212//                              // TIME_ASPCT   
213//                              // SYSTEM       
214//                              // SCALE_TYP    
215//                              // METHOD_TYP   
216//                              // dee.getCategory().add(new CodeableConcept().setText(col(row, "CLASS")));
217//                              // SOURCE       
218//                              // DATE_LAST_CHANGED - should be in ?   
219//                              // CHNG_TYPE    
220//                              dee.setComment(col(row , "COMMENTS"));
221//                              if (hasCol(row, "CONSUMER_NAME"))
222//                                      dee.addAlias(col(row, "CONSUMER_NAME"));        
223//                              // MOLAR_MASS   
224//                              // CLASSTYPE    
225//                              // FORMULA      
226//                              // SPECIES      
227//                              // EXMPL_ANSWERS        
228//                              // ACSSYM       
229//                              // BASE_NAME - ? this is a relationship 
230//                              // NAACCR_ID    
231//                              // ---------- CODE_TABLE todo   
232//                              // SURVEY_QUEST_TEXT    
233//                              // SURVEY_QUEST_SRC     
234//                              if (hasCol(row, "RELATEDNAMES2")) {
235//              String n = col(row, "RELATEDNAMES2");
236//              for (String s : n.split("\\;")) {
237//                                              if (!Utilities.noString(s))
238//                                                      dee.addAlias(s);        
239//              }
240//                              }
241//                              dee.addAlias(col(row, "SHORTNAME"));    
242//                              // ORDER_OBS    
243//                              // CDISC Code   
244//                              // HL7_FIELD_SUBFIELD_ID        
245//                              //  ------------------ EXTERNAL_COPYRIGHT_NOTICE todo   
246//                              dee.setDefinition(col(row, "LONG_COMMON_NAME"));        
247//                              // HL7_V2_DATATYPE      
248//                              String cc = makeType(col(row, "HL7_V3_DATATYPE"), code);
249//                              if (cc != null)
250//                                dee.addType().setCode(cc);    
251//                              // todo... CURATED_RANGE_AND_UNITS      
252//                              // todo: DOCUMENT_SECTION       
253//                              // STATUS_REASON        
254//                              // STATUS_TEXT  
255//                              // CHANGE_REASON_PUBLIC 
256//                              // COMMON_TEST_RANK     
257//                              // COMMON_ORDER_RANK    
258//                              // COMMON_SI_TEST_RANK  
259//                              // HL7_ATTACHMENT_STRUCTURE
260//
261//                              // units:
262//                              // UNITSREQUIRED        
263//                              // SUBMITTED_UNITS
264//                              ToolingExtensions.setAllowableUnits(dee, makeUnits(col(row, "EXAMPLE_UNITS"), col(row, "EXAMPLE_UCUM_UNITS")));
265//                              // EXAMPLE_SI_UCUM_UNITS        
266                        
267                        row = XMLUtil.getNextSibling(row);
268                }
269                System.out.println("done");
270    log.info("Processing complete");
271        }
272
273        private String makeType(String type, String id) {
274                if (Utilities.noString(type))
275                        return null;
276                if (type.equals("PQ"))
277                        return "Quantity";
278                else if (type.equals("ED"))
279                  return "Attachment";
280                else if (type.equals("TS"))
281                  return "dateTime";
282                else if (type.equals("ST"))
283                  return "string";
284                else if (type.equals("II"))
285                  return "Identifier";
286                else if (type.equals("CWE"))
287                  return "CodeableConcept";
288                else if (type.equals("CD") || type.equals("CO"))
289                  return "CodeableConcept";
290                else if (type.equals("PN"))
291                  return "HumanName";
292                else if (type.equals("EN"))
293                  return "HumanName";
294                else if (type.equals("AD"))
295                  return "Address";
296                else if (type.equals("BL"))
297                  return "boolean";
298                else if (type.equals("GTS"))
299                  return "Schedule";
300                else if (type.equals("INT"))
301                  return "integer";
302                else if (type.equals("CS"))
303                  return "code";
304                else if (type.equals("IVL_TS"))
305                  return "Period";
306                else if (type.equals("MMAT") || type.equals("PRF") || type.equals("TX") || type.equals("DT") || type.equals("FT"))
307                  return null;
308                else
309                        throw new Error("unmapped type "+type+" for LOINC code "+id);
310        } // 18606-4: MMAT.  18665-0: PRF. 18671-8: TX. 55400-6: DT; 8251-1: FT 
311
312        private CodeableConcept makeUnits(String text, String ucum) {
313                if (Utilities.noString(text) && Utilities.noString(ucum))
314                        return null;
315                CodeableConcept cc = new CodeableConcept();
316                cc.setText(text);
317                cc.getCoding().add(new Coding().setCode(ucum).setSystem("http://unitsofmeasure.org"));
318                return cc;
319        }
320  public Bundle getBundle() {
321    return bundle;
322  }
323}