001package org.hl7.fhir.convertors.misc;
002
003/*
004  Copyright (c) 2011+, HL7, Inc.
005  All rights reserved.
006  
007  Redistribution and use in source and binary forms, with or without modification, 
008  are permitted provided that the following conditions are met:
009    
010   * Redistributions of source code must retain the above copyright notice, this 
011     list of conditions and the following disclaimer.
012   * Redistributions in binary form must reproduce the above copyright notice, 
013     this list of conditions and the following disclaimer in the documentation 
014     and/or other materials provided with the distribution.
015   * Neither the name of HL7 nor the names of its contributors may be used to 
016     endorse or promote products derived from this software without specific 
017     prior written permission.
018  
019  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020  ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022  IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023  INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024  NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028  POSSIBILITY OF SUCH DAMAGE.
029  
030 */
031
032
033import org.apache.commons.lang3.CharUtils;
034import org.hl7.fhir.dstu3.formats.IParser.OutputStyle;
035import org.hl7.fhir.dstu3.formats.XmlParser;
036import org.hl7.fhir.dstu3.model.ElementDefinition;
037import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
038import org.hl7.fhir.dstu3.model.StructureDefinition;
039import org.hl7.fhir.utilities.xml.XMLUtil;
040import org.w3c.dom.Element;
041
042import javax.xml.parsers.DocumentBuilder;
043import javax.xml.parsers.DocumentBuilderFactory;
044import java.io.FileInputStream;
045import java.io.FileOutputStream;
046import java.util.ArrayList;
047import java.util.HashMap;
048import java.util.List;
049import java.util.Map;
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 = DocumentBuilderFactory.newInstance();
091    factory.setNamespaceAware(true);
092    DocumentBuilder builder = factory.newDocumentBuilder();
093    adlConfig = builder.parse(new FileInputStream(config)).getDocumentElement();
094
095    // load ADL
096    builder = factory.newDocumentBuilder();
097    adl = builder.parse(new FileInputStream(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.typeName = XMLUtil.getNamedChild(definition, "rm_type_name").getTextContent();
137    root.atCode = XMLUtil.getNamedChild(definition, "node_id").getTextContent();
138    root.name = generateToken(root.atCode, true);
139    sd.setName(root.name);
140    root.cardinality = 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.atCode, root, attributes);
147    }
148    dumpChildren("", root);
149    genElements(sd, root.name, root);
150
151    // save
152    new XmlParser().setOutputStyle(OutputStyle.PRETTY).compose(new FileOutputStream(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.cardinality.max);
160    ed.setMin(Integer.parseInt(item.cardinality.min));
161    ed.setShort(texts.get(item.atCode).text);
162    ed.setDefinition(texts.get(item.atCode).description);
163    ed.setComment(texts.get(item.atCode).comment);
164    Element te = findTypeElement(item.typeName);
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.children) {
172      genElements(sd, path + "." + child.name, 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.typeName);
189    if (te.hasAttribute("profile"))
190      System.out.println(prefix + item.atCode + " [" + item.cardinality.min + ".." + item.cardinality.max + "]:" + te.getAttribute("fhir") + "{" + te.getAttribute("profile") + "} // " + item.name + " = " + texts.get(item.atCode).text);
191    else
192      System.out.println(prefix + item.atCode + " [" + item.cardinality.min + ".." + item.cardinality.max + "]:" + te.getAttribute("fhir") + " // " + item.name + " = " + texts.get(item.atCode).text);
193
194    for (NodeTreeEntry child : item.children)
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.typeName = XMLUtil.getNamedChild(e, "rm_type_name").getTextContent();
204      item.atCode = XMLUtil.getNamedChild(e, "node_id").getTextContent();
205      item.name = generateToken(item.atCode, false);
206      item.cardinality = readCardinality(path + "/" + item.atCode, XMLUtil.getNamedChild(e, "occurrences"));
207      parent.children.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.typeName, "ELEMENT", "type for simple element: " + item.typeName);
212        checkCardSingle(path, XMLUtil.getNamedChild(attr, "existence"));
213        Element c = XMLUtil.getNamedChild(attr, "children");
214        checkCardSingle(path, XMLUtil.getNamedChild(c, "occurrences"));
215        item.typeName = XMLUtil.getNamedChild(c, "rm_type_name").getTextContent();
216      } else {
217        check(path, item.typeName, "CLUSTER", "type for complex element");
218        loadChildren(path + "/" + item.atCode, 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.min = XMLUtil.getNamedChild(element, "lower").getTextContent();
265    if ("true".equals(XMLUtil.getNamedChild(element, "upper_unbounded").getTextContent()))
266      card.max = "*";
267    else
268      card.max = 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  public class Cardinality {
297    private String min;
298    private String max;
299  }
300
301  public class TextSet {
302
303    private String text;
304    private String description;
305    private String comment;
306
307
308    public String getText() {
309      return text;
310    }
311
312    public void setText(String value) {
313      this.text = value;
314    }
315
316    public String getDescription() {
317      return description;
318    }
319
320    public void setDescription(String value) {
321      this.description = value;
322    }
323
324    public String getComment() {
325      return comment;
326    }
327
328    public void setComment(String value) {
329      this.comment = value;
330    }
331
332  }
333
334  public class NodeTreeEntry {
335    private final List<NodeTreeEntry> children = new ArrayList<NodeTreeEntry>();
336    private String name;
337    private String atCode;
338    private String typeName;
339    private Cardinality cardinality;
340  }
341
342}