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
051@SuppressWarnings("checkstyle:systemout")
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 = XMLUtil.newXXEProtectedDocumentBuilderFactory();
092    factory.setNamespaceAware(true);
093    DocumentBuilder builder = factory.newDocumentBuilder();
094    adlConfig = builder.parse(ManagedFileAccess.inStream(config)).getDocumentElement();
095
096    // load ADL
097    builder = factory.newDocumentBuilder();
098    adl = builder.parse(ManagedFileAccess.inStream(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(ManagedFileAccess.outStream(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}