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}