
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}