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