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}