001package org.hl7.fhir.convertors.misc.adl; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.List; 006import java.util.Map; 007 008import javax.xml.parsers.DocumentBuilder; 009import javax.xml.parsers.DocumentBuilderFactory; 010 011/* 012 Copyright (c) 2011+, HL7, Inc. 013 All rights reserved. 014 015 Redistribution and use in source and binary forms, with or without modification, 016 are permitted provided that the following conditions are met: 017 018 * Redistributions of source code must retain the above copyright notice, this 019 list of conditions and the following disclaimer. 020 * Redistributions in binary form must reproduce the above copyright notice, 021 this list of conditions and the following disclaimer in the documentation 022 and/or other materials provided with the distribution. 023 * Neither the name of HL7 nor the names of its contributors may be used to 024 endorse or promote products derived from this software without specific 025 prior written permission. 026 027 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 028 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 029 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 030 IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 031 INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 032 NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 033 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 034 WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 035 ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 036 POSSIBILITY OF SUCH DAMAGE. 037 038 */ 039 040 041import org.apache.commons.lang3.CharUtils; 042import org.hl7.fhir.dstu3.formats.IParser.OutputStyle; 043import org.hl7.fhir.dstu3.formats.XmlParser; 044import org.hl7.fhir.dstu3.model.ElementDefinition; 045import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus; 046import org.hl7.fhir.dstu3.model.StructureDefinition; 047import org.hl7.fhir.utilities.filesystem.ManagedFileAccess; 048import org.hl7.fhir.utilities.xml.XMLUtil; 049import org.w3c.dom.Element; 050 051public class ADLImporter { 052 053 private final Map<String, TextSet> texts = new HashMap<String, TextSet>(); 054 private String source; 055 private String dest; 056 private String config; 057 private String info; 058 private Element adl; 059 private Element adlConfig; 060 061 public static void main(String[] args) throws Exception { 062 ADLImporter self = new ADLImporter(); 063 self.source = getParam(args, "source"); 064 self.dest = getParam(args, "dest"); 065 self.config = getParam(args, "config"); 066 self.info = getParam(args, "info"); 067 if (self.source == null || self.dest == null || self.config == null) { 068 System.out.println("ADL to FHIR StructureDefinition Converter"); 069 System.out.println("This tool takes 4 parameters:"); 070 System.out.println("-source: ADL 1.4 XML representation of an archetype (required)"); 071 System.out.println("-dest: filename of structure definition to produce (required)"); 072 System.out.println("-config: filename of OpenEHR/FHIR knowlege base (required)"); 073 System.out.println("-info: filename of additional knowlege for this adl file (optional)"); 074 } else { 075 self.execute(); 076 } 077 } 078 079 private static String getParam(String[] args, String name) { 080 for (int i = 0; i < args.length - 1; i++) { 081 if (args[i].equals("-" + name)) { 082 return args[i + 1]; 083 } 084 } 085 return null; 086 } 087 088 private void execute() throws Exception { 089 // load config 090 DocumentBuilderFactory factory = XMLUtil.newXXEProtectedDocumentBuilderFactory(); 091 factory.setNamespaceAware(true); 092 DocumentBuilder builder = factory.newDocumentBuilder(); 093 adlConfig = builder.parse(ManagedFileAccess.inStream(config)).getDocumentElement(); 094 095 // load ADL 096 builder = factory.newDocumentBuilder(); 097 adl = builder.parse(ManagedFileAccess.inStream(source)).getDocumentElement(); 098 099 check("root", adl.getNamespaceURI(), "http://schemas.openehr.org/v1", "Wrong namespace for ADL XML"); 100 check("root", adl.getNodeName(), "archetype", "Wrong XML for ADL XML"); 101 check("root", XMLUtil.getNamedChild(adl, "adl_version").getTextContent(), "1.4", "unsupported ADL version"); 102 103 String id = XMLUtil.getFirstChild(XMLUtil.getNamedChild(adl, "archetype_id")).getTextContent().split("\\.")[1]; 104 // create structure definition 105 StructureDefinition sd = new StructureDefinition(); 106 sd.setId(id); 107 108 // populate metadata 109 Element description = XMLUtil.getNamedChild(adl, "description"); 110 Element details = XMLUtil.getNamedChild(description, "details"); 111 sd.setDescription(XMLUtil.getNamedChild(details, "purpose").getTextContent()); 112 sd.setCopyright(XMLUtil.getNamedChild(details, "copyright").getTextContent()); 113 sd.setPurpose("Use:\r\n" + XMLUtil.getNamedChild(details, "use").getTextContent() + "\r\n\r\nMisuse:\r\n" + XMLUtil.getNamedChild(details, "misuse").getTextContent()); 114 List<Element> set = new ArrayList<Element>(); 115 XMLUtil.getNamedChildren(details, "keywords", set); 116 for (Element e : set) 117 sd.addKeyword().setDisplay(e.getTextContent()); 118 String status = XMLUtil.getNamedChild(description, "lifecycle_state").getTextContent(); 119 if ("CommitteeDraft".equals(status) || "AuthorDraft".equals(status)) 120 sd.setStatus(PublicationStatus.DRAFT); 121 else 122 throw new Exception("Unknown life cycle state " + XMLUtil.getNamedChild(description, "lifecycle_state").getTextContent()); 123 124 // load texts from ontology 125 Element ontology = XMLUtil.getNamedChild(adl, "ontology"); 126 Element term_definitions = XMLUtil.getNamedChild(ontology, "term_definitions"); 127 set.clear(); 128 XMLUtil.getNamedChildren(term_definitions, "items", set); 129 for (Element item : set) { 130 processTextItem(item); 131 } 132 133 // load data and protocol 134 Element definition = XMLUtil.getNamedChild(adl, "definition"); 135 NodeTreeEntry root = new NodeTreeEntry(); 136 root.setTypeName(XMLUtil.getNamedChild(definition, "rm_type_name").getTextContent()); 137 root.setAtCode(XMLUtil.getNamedChild(definition, "node_id").getTextContent()); 138 root.setName(generateToken(root.getAtCode(), true)); 139 sd.setName(root.getName()); 140 root.setCardinality(readCardinality("root", XMLUtil.getNamedChild(definition, "occurrences"))); 141 set.clear(); 142 XMLUtil.getNamedChildren(definition, "attributes", set); 143 for (Element item : set) { 144 // we're actually skipping this level - we don't care about data protocol etc. 145 Element attributes = item; // XMLUtil.getNamedChild(XMLUtil.getNamedChild(item, "children"), "attributes"); 146 loadChildren(root.getAtCode(), root, attributes); 147 } 148 dumpChildren("", root); 149 genElements(sd, root.getName(), root); 150 151 // save 152 new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(ManagedFileAccess.outStream(dest), sd); 153 System.out.println("done. saved as " + dest); 154 } 155 156 private void genElements(StructureDefinition sd, String path, NodeTreeEntry item) throws Exception { 157 ElementDefinition ed = sd.getSnapshot().addElement(); 158 ed.setPath(path); 159 ed.setMax(item.getCardinality().getMax()); 160 ed.setMin(Integer.parseInt(item.getCardinality().getMin())); 161 ed.setShort(texts.get(item.getAtCode()).getText()); 162 ed.setDefinition(texts.get(item.getAtCode()).getDescription()); 163 ed.setComment(texts.get(item.getAtCode()).getComment()); 164 Element te = findTypeElement(item.getTypeName()); 165 if (te.hasAttribute("profile")) 166 ed.addType().setCode(te.getAttribute("fhir")).setProfile(te.getAttribute("profile")); 167 else 168 ed.addType().setCode(te.getAttribute("fhir")); 169 ed.getBase().setPath(ed.getPath()).setMin(ed.getMin()).setMax(ed.getMax()); 170 171 for (NodeTreeEntry child : item.getChildren()) { 172 genElements(sd, path + "." + child.getName(), child); 173 } 174 } 175 176 private Element findTypeElement(String typeName) throws Exception { 177 Element dataTypes = XMLUtil.getNamedChild(adlConfig, "dataTypes"); 178 List<Element> set = new ArrayList<Element>(); 179 XMLUtil.getNamedChildren(dataTypes, "dataType", set); 180 for (Element e : set) { 181 if (typeName.equals(e.getAttribute("name"))) 182 return e; 183 } 184 throw new Exception("No FHIR equivalent found for " + typeName); 185 } 186 187 private void dumpChildren(String prefix, NodeTreeEntry item) throws Exception { 188 Element te = findTypeElement(item.getTypeName()); 189 if (te.hasAttribute("profile")) 190 System.out.println(prefix + item.getAtCode() + " [" + item.getCardinality().getMin() + ".." + item.getCardinality().getMax() + "]:" + te.getAttribute("fhir") + "{" + te.getAttribute("profile") + "} // " + item.getName() + " = " + texts.get(item.getAtCode()).getText()); 191 else 192 System.out.println(prefix + item.getAtCode() + " [" + item.getCardinality().getMin() + ".." + item.getCardinality().getMax() + "]:" + te.getAttribute("fhir") + " // " + item.getName() + " = " + texts.get(item.getAtCode()).getText()); 193 194 for (NodeTreeEntry child : item.getChildren()) 195 dumpChildren(prefix + " ", child); 196 } 197 198 private void loadChildren(String path, NodeTreeEntry parent, Element attributes) throws Exception { 199 List<Element> set = new ArrayList<Element>(); 200 XMLUtil.getNamedChildren(attributes, "children", set); 201 for (Element e : set) { 202 NodeTreeEntry item = new NodeTreeEntry(); 203 item.setTypeName(XMLUtil.getNamedChild(e, "rm_type_name").getTextContent()); 204 item.setAtCode(XMLUtil.getNamedChild(e, "node_id").getTextContent()); 205 item.setName(generateToken(item.getAtCode(), false)); 206 item.setCardinality(readCardinality(path + "/" + item.getAtCode(), XMLUtil.getNamedChild(e, "occurrences"))); 207 parent.getChildren().add(item); 208 Element attr = XMLUtil.getNamedChild(e, "attributes"); 209 String type = attr.getAttribute("xsi:type"); 210 if ("C_SINGLE_ATTRIBUTE".equals(type)) { 211 check(path, item.getTypeName(), "ELEMENT", "type for simple element: " + item.getTypeName()); 212 checkCardSingle(path, XMLUtil.getNamedChild(attr, "existence")); 213 Element c = XMLUtil.getNamedChild(attr, "children"); 214 checkCardSingle(path, XMLUtil.getNamedChild(c, "occurrences")); 215 item.setTypeName(XMLUtil.getNamedChild(c, "rm_type_name").getTextContent()); 216 } else { 217 check(path, item.getTypeName(), "CLUSTER", "type for complex element"); 218 loadChildren(path + "/" + item.getAtCode(), item, attr); 219 } 220 } 221 } 222 223 private String generateToken(String atCode, boolean upFirst) { 224 if (!texts.containsKey(atCode)) 225 return atCode; 226 String text = texts.get(atCode).getText(); 227 boolean lastText = false; 228 StringBuilder b = new StringBuilder(); 229 for (char c : text.toCharArray()) { 230 boolean ok = CharUtils.isAscii(c); 231 if (ok) 232 if (b.length() == 0) 233 ok = Character.isAlphabetic(c); 234 else 235 ok = Character.isAlphabetic(c) || Character.isDigit(c); 236 if (!ok) { 237 lastText = false; 238 } else { 239 if (!lastText && (b.length() > 0 || upFirst)) 240 b.append(Character.toUpperCase(c)); 241 else 242 b.append(Character.toLowerCase(c)); 243 lastText = true; 244 } 245 } 246 return b.toString(); 247 } 248 249 private void checkCardSingle(String path, Element element) throws Exception { 250 check(path, XMLUtil.getNamedChild(element, "lower_included").getTextContent(), "true", "Cardinality check"); 251 check(path, XMLUtil.getNamedChild(element, "upper_included").getTextContent(), "true", "Cardinality check"); 252 check(path, XMLUtil.getNamedChild(element, "lower_unbounded").getTextContent(), "false", "Cardinality check"); 253 check(path, XMLUtil.getNamedChild(element, "upper_unbounded").getTextContent(), "false", "Cardinality check"); 254 check(path, XMLUtil.getNamedChild(element, "lower").getTextContent(), "1", "Cardinality check"); 255 check(path, XMLUtil.getNamedChild(element, "upper").getTextContent(), "1", "Cardinality check"); 256 } 257 258 private Cardinality readCardinality(String path, Element element) throws Exception { 259 check(path, XMLUtil.getNamedChild(element, "lower_included").getTextContent(), "true", "Cardinality check"); 260 if (XMLUtil.getNamedChild(element, "upper_included") != null) 261 check(path, XMLUtil.getNamedChild(element, "upper_included").getTextContent(), "true", "Cardinality check"); 262 check(path, XMLUtil.getNamedChild(element, "lower_unbounded").getTextContent(), "false", "Cardinality check"); 263 Cardinality card = new Cardinality(); 264 card.setMin(XMLUtil.getNamedChild(element, "lower").getTextContent()); 265 if ("true".equals(XMLUtil.getNamedChild(element, "upper_unbounded").getTextContent())) 266 card.setMax("*"); 267 else 268 card.setMax(XMLUtil.getNamedChild(element, "upper").getTextContent()); 269 return card; 270 } 271 272 private void processTextItem(Element item) throws Exception { 273 String atcode = item.getAttribute("code"); 274 TextSet ts = new TextSet(); 275 List<Element> set = new ArrayList<Element>(); 276 XMLUtil.getNamedChildren(item, "items", set); 277 for (Element e : set) { 278 String code = e.getAttribute("id"); 279 if (code.equals("text")) 280 ts.setText(e.getTextContent()); 281 else if (code.equals("description")) 282 ts.setDescription(e.getTextContent()); 283 else if (code.equals("comment")) 284 ts.setComment(e.getTextContent()); 285 else 286 throw new Exception("unknown code " + code); 287 } 288 texts.put(atcode, ts); 289 } 290 291 private void check(String path, String found, String expected, String message) throws Exception { 292 if (!expected.equals(found.trim())) 293 throw new Exception(message + ". Expected '" + expected + "' but found '" + found.trim() + "', at " + path); 294 } 295 296}