001package org.hl7.fhir.r4.conformance;
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
032import java.io.FileOutputStream;
033/*
034Copyright (c) 2011+, HL7, Inc
035All rights reserved.
036
037Redistribution and use in source and binary forms, with or without modification, 
038are permitted provided that the following conditions are met:
039
040 * Redistributions of source code must retain the above copyright notice, this 
041   list of conditions and the following disclaimer.
042 * Redistributions in binary form must reproduce the above copyright notice, 
043   this list of conditions and the following disclaimer in the documentation 
044   and/or other materials provided with the distribution.
045 * Neither the name of HL7 nor the names of its contributors may be used to 
046   endorse or promote products derived from this software without specific 
047   prior written permission.
048
049THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
050ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
051WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
052IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
053INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
054NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
055PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
056WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
057ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
058POSSIBILITY OF SUCH DAMAGE.
059
060 */
061import java.io.IOException;
062import java.io.OutputStreamWriter;
063import java.util.ArrayList;
064import java.util.HashMap;
065import java.util.HashSet;
066import java.util.LinkedList;
067import java.util.List;
068import java.util.Map;
069import java.util.Queue;
070import java.util.Set;
071
072import org.hl7.fhir.exceptions.FHIRException;
073import org.hl7.fhir.r4.context.IWorkerContext;
074import org.hl7.fhir.r4.model.ElementDefinition;
075import org.hl7.fhir.r4.model.ElementDefinition.PropertyRepresentation;
076import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
077import org.hl7.fhir.r4.model.StructureDefinition;
078import org.hl7.fhir.r4.utils.ToolingExtensions;
079import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
080import org.hl7.fhir.utilities.Utilities;
081import org.hl7.fhir.utilities.filesystem.ManagedFileAccess;
082
083public class XmlSchemaGenerator {
084
085  public class QName {
086
087    public String type;
088    public String typeNs;
089
090    @Override
091    public String toString() {
092      return typeNs + ":" + type;
093    }
094  }
095
096  public class ElementToGenerate {
097
098    private String tname;
099    private StructureDefinition sd;
100    private ElementDefinition ed;
101
102    public ElementToGenerate(String tname, StructureDefinition sd, ElementDefinition edc) {
103      this.tname = tname;
104      this.sd = sd;
105      this.ed = edc;
106    }
107
108  }
109
110  private String folder;
111  private IWorkerContext context;
112  private boolean single;
113  private String version;
114  private String genDate;
115  private String license;
116  private boolean annotations;
117
118  public XmlSchemaGenerator(String folder, IWorkerContext context) {
119    this.folder = folder;
120    this.context = context;
121  }
122
123  public boolean isSingle() {
124    return single;
125  }
126
127  public void setSingle(boolean single) {
128    this.single = single;
129  }
130
131  public String getVersion() {
132    return version;
133  }
134
135  public void setVersion(String version) {
136    this.version = version;
137  }
138
139  public String getGenDate() {
140    return genDate;
141  }
142
143  public void setGenDate(String genDate) {
144    this.genDate = genDate;
145  }
146
147  public String getLicense() {
148    return license;
149  }
150
151  public void setLicense(String license) {
152    this.license = license;
153  }
154
155  public boolean isAnnotations() {
156    return annotations;
157  }
158
159  public void setAnnotations(boolean annotations) {
160    this.annotations = annotations;
161  }
162
163  private Set<ElementDefinition> processed = new HashSet<ElementDefinition>();
164  private Set<StructureDefinition> processedLibs = new HashSet<StructureDefinition>();
165  private Set<String> typeNames = new HashSet<String>();
166  private OutputStreamWriter writer;
167  private Map<String, String> namespaces = new HashMap<String, String>();
168  private Queue<ElementToGenerate> queue = new LinkedList<ElementToGenerate>();
169  private Queue<StructureDefinition> queueLib = new LinkedList<StructureDefinition>();
170  private Map<String, StructureDefinition> library;
171  private boolean useNarrative;
172
173  private void w(String s) throws IOException {
174    writer.write(s);
175  }
176
177  private void ln(String s) throws IOException {
178    writer.write(s);
179    writer.write("\r\n");
180  }
181
182  private void close() throws IOException {
183    if (writer != null) {
184      ln("</xs:schema>");
185      writer.flush();
186      writer.close();
187      writer = null;
188    }
189  }
190
191  private String start(StructureDefinition sd, String ns) throws IOException, FHIRException {
192    String lang = "en";
193    if (sd.hasLanguage())
194      lang = sd.getLanguage();
195
196    if (single && writer != null) {
197      if (!ns.equals(getNs(sd)))
198        throw new FHIRException("namespace inconsistency: " + ns + " vs " + getNs(sd));
199      return lang;
200    }
201    close();
202
203    writer = new OutputStreamWriter(ManagedFileAccess.outStream(Utilities.path(folder, tail(sd.getType() + ".xsd"))), "UTF-8");
204    ln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
205    ln("<!-- ");
206    ln(license);
207    ln("");
208    ln("  Generated on " + genDate + " for FHIR v" + version + " ");
209    ln("");
210    ln("  Note: this schema does not contain all the knowledge represented in the underlying content model");
211    ln("");
212    ln("-->");
213    ln("<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:fhir=\"http://hl7.org/fhir\" xmlns:xhtml=\"http://www.w3.org/1999/xhtml\" "
214        + "xmlns:lm=\"" + ns + "\" targetNamespace=\"" + ns + "\" elementFormDefault=\"qualified\" version=\"1.0\">");
215    ln("  <xs:import schemaLocation=\"fhir-common.xsd\" namespace=\"http://hl7.org/fhir\"/>");
216    if (useNarrative) {
217      if (ns.equals("urn:hl7-org:v3"))
218        ln("  <xs:include schemaLocation=\"cda-narrative.xsd\"/>");
219      else
220        ln("  <xs:import schemaLocation=\"cda-narrative.xsd\" namespace=\"urn:hl7-org:v3\"/>");
221    }
222    namespaces.clear();
223    namespaces.put(ns, "lm");
224    namespaces.put("http://hl7.org/fhir", "fhir");
225    typeNames.clear();
226
227    return lang;
228  }
229
230  private String getNs(StructureDefinition sd) {
231    String ns = "http://hl7.org/fhir";
232    if (sd.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"))
233      ns = ToolingExtensions.readStringExtension(sd,
234          "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
235    return ns;
236  }
237
238  public void generate(StructureDefinition entry, Map<String, StructureDefinition> library) throws Exception {
239    processedLibs.clear();
240
241    this.library = library;
242    checkLib(entry);
243
244    String ns = getNs(entry);
245    String lang = start(entry, ns);
246
247    w("  <xs:element name=\"" + tail(entry.getType()) + "\" type=\"lm:" + tail(entry.getType()) + "\"");
248    if (annotations) {
249      ln(">");
250      ln("    <xs:annotation>");
251      ln("      <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(entry.getDescription())
252          + "</xs:documentation>");
253      ln("    </xs:annotation>");
254      ln("  </xs:element>");
255    } else
256      ln("/>");
257
258    produceType(entry, entry.getSnapshot().getElement().get(0), tail(entry.getType()),
259        getQN(entry, entry.getBaseDefinition()), lang);
260    while (!queue.isEmpty()) {
261      ElementToGenerate q = queue.poll();
262      produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false),
263          lang);
264    }
265    while (!queueLib.isEmpty()) {
266      generateInner(queueLib.poll());
267    }
268    close();
269  }
270
271  private void checkLib(StructureDefinition entry) {
272    for (ElementDefinition ed : entry.getSnapshot().getElement()) {
273      if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) {
274        useNarrative = true;
275      }
276    }
277    for (StructureDefinition sd : library.values()) {
278      for (ElementDefinition ed : sd.getSnapshot().getElement()) {
279        if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) {
280          useNarrative = true;
281        }
282      }
283    }
284  }
285
286  private void generateInner(StructureDefinition sd) throws IOException, FHIRException {
287    if (processedLibs.contains(sd))
288      return;
289    processedLibs.add(sd);
290
291    String ns = getNs(sd);
292    String lang = start(sd, ns);
293
294    if (sd.getSnapshot().getElement().isEmpty())
295      throw new FHIRException("no snap shot on " + sd.getUrl());
296
297    produceType(sd, sd.getSnapshot().getElement().get(0), tail(sd.getType()), getQN(sd, sd.getBaseDefinition()), lang);
298    while (!queue.isEmpty()) {
299      ElementToGenerate q = queue.poll();
300      produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false),
301          lang);
302    }
303  }
304
305  private String tail(String url) {
306    return url.contains("/") ? url.substring(url.lastIndexOf("/") + 1) : url;
307  }
308
309  private String root(String url) {
310    return url.contains("/") ? url.substring(0, url.lastIndexOf("/")) : "";
311  }
312
313  private String tailDot(String url) {
314    return url.contains(".") ? url.substring(url.lastIndexOf(".") + 1) : url;
315  }
316
317  private void produceType(StructureDefinition sd, ElementDefinition ed, String typeName, QName typeParent, String lang)
318      throws IOException, FHIRException {
319    if (processed.contains(ed))
320      return;
321    processed.add(ed);
322
323    // ok
324    ln("  <xs:complexType name=\"" + typeName + "\">");
325    if (annotations) {
326      ln("    <xs:annotation>");
327      ln("      <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(ed.getDefinition())
328          + "</xs:documentation>");
329      ln("    </xs:annotation>");
330    }
331    ln("    <xs:complexContent>");
332    ln("      <xs:extension base=\"" + typeParent.toString() + "\">");
333    ln("        <xs:sequence>");
334
335    // hack....
336    for (ElementDefinition edc : ProfileUtilities.getChildList(sd, ed)) {
337      if (!(edc.hasRepresentation(PropertyRepresentation.XMLATTR)
338          || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc))
339        produceElement(sd, ed, edc, lang);
340    }
341    ln("        </xs:sequence>");
342    for (ElementDefinition edc : ProfileUtilities.getChildList(sd, ed)) {
343      if ((edc.hasRepresentation(PropertyRepresentation.XMLATTR)
344          || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc))
345        produceAttribute(sd, ed, edc, lang);
346    }
347    ln("      </xs:extension>");
348    ln("    </xs:complexContent>");
349    ln("  </xs:complexType>");
350  }
351
352  private boolean inheritedElement(ElementDefinition edc) {
353    return !edc.getPath().equals(edc.getBase().getPath());
354  }
355
356  private void produceElement(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang)
357      throws IOException, FHIRException {
358    if (edc.getType().size() == 0)
359      throw new Error("No type at " + edc.getPath());
360
361    if (edc.getType().size() > 1 && edc.hasRepresentation(PropertyRepresentation.TYPEATTR)) {
362      // first, find the common base type
363      StructureDefinition lib = getCommonAncestor(edc.getType());
364      if (lib == null)
365        throw new Error("Common ancester not found at " + edc.getPath());
366      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
367      for (TypeRefComponent t : edc.getType()) {
368        b.append(getQN(sd, edc, t.getWorkingCode(), true).toString());
369      }
370
371      String name = tailDot(edc.getPath());
372      String min = String.valueOf(edc.getMin());
373      String max = edc.getMax();
374      if ("*".equals(max))
375        max = "unbounded";
376
377      QName qn = getQN(sd, edc, lib.getUrl(), true);
378
379      ln("        <xs:element name=\"" + name + "\" minOccurs=\"" + min + "\" maxOccurs=\"" + max + "\" type=\""
380          + qn.typeNs + ":" + qn.type + "\">");
381      ln("          <xs:annotation>");
382      ln("          <xs:appinfo xml:lang=\"en\">Possible types: " + b.toString() + "</xs:appinfo>");
383      if (annotations && edc.hasDefinition())
384        ln("            <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition())
385            + "</xs:documentation>");
386      ln("          </xs:annotation>");
387      ln("        </xs:element>");
388    } else
389      for (TypeRefComponent t : edc.getType()) {
390        String name = tailDot(edc.getPath());
391        if (edc.getType().size() > 1)
392          name = name + Utilities.capitalize(t.getWorkingCode());
393        QName qn = getQN(sd, edc, t.getWorkingCode(), true);
394        String min = String.valueOf(edc.getMin());
395        String max = edc.getMax();
396        if ("*".equals(max))
397          max = "unbounded";
398
399        w("        <xs:element name=\"" + name + "\" minOccurs=\"" + min + "\" maxOccurs=\"" + max + "\" type=\""
400            + qn.typeNs + ":" + qn.type + "\"");
401        if (annotations && edc.hasDefinition()) {
402          ln(">");
403          ln("          <xs:annotation>");
404          ln("            <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition())
405              + "</xs:documentation>");
406          ln("          </xs:annotation>");
407          ln("        </xs:element>");
408        } else
409          ln("/>");
410      }
411  }
412
413  public QName getQN(StructureDefinition sd, String type) throws FHIRException {
414    return getQN(sd, sd.getSnapshot().getElementFirstRep(), type, false);
415  }
416
417  public QName getQN(StructureDefinition sd, ElementDefinition edc, String t, boolean chase) throws FHIRException {
418    QName qn = new QName();
419    qn.type = Utilities.isAbsoluteUrl(t) ? tail(t) : t;
420    if (Utilities.isAbsoluteUrl(t)) {
421      String ns = root(t);
422      if (ns.equals(root(sd.getUrl())))
423        ns = getNs(sd);
424      if (ns.equals("http://hl7.org/fhir/StructureDefinition"))
425        ns = "http://hl7.org/fhir";
426      if (!namespaces.containsKey(ns))
427        throw new FHIRException("Unknown type namespace " + ns + " for " + edc.getPath());
428      qn.typeNs = namespaces.get(ns);
429      StructureDefinition lib = library.get(t);
430      if (lib == null && !Utilities.existsInList(t, "http://hl7.org/fhir/cda/StructureDefinition/StrucDoc.Text",
431          "http://hl7.org/fhir/StructureDefinition/Element"))
432        throw new FHIRException("Unable to resolve " + t + " for " + edc.getPath());
433      if (lib != null)
434        queueLib.add(lib);
435    } else
436      qn.typeNs = namespaces.get("http://hl7.org/fhir");
437
438    if (chase && qn.type.equals("Element")) {
439      String tname = typeNameFromPath(edc);
440      if (typeNames.contains(tname)) {
441        int i = 1;
442        while (typeNames.contains(tname + i))
443          i++;
444        tname = tname + i;
445      }
446      queue.add(new ElementToGenerate(tname, sd, edc));
447      qn.typeNs = "lm";
448      qn.type = tname;
449    }
450    return qn;
451  }
452
453  private StructureDefinition getCommonAncestor(List<TypeRefComponent> type) throws FHIRException {
454    StructureDefinition sd = library.get(type.get(0).getWorkingCode());
455    if (sd == null)
456      throw new FHIRException("Unable to find definition for " + type.get(0).getWorkingCode());
457    for (int i = 1; i < type.size(); i++) {
458      StructureDefinition t = library.get(type.get(i).getWorkingCode());
459      if (t == null)
460        throw new FHIRException("Unable to find definition for " + type.get(i).getWorkingCode());
461      sd = getCommonAncestor(sd, t);
462    }
463    return sd;
464  }
465
466  private StructureDefinition getCommonAncestor(StructureDefinition sd1, StructureDefinition sd2) throws FHIRException {
467    // this will always return something because everything comes from Element
468    List<StructureDefinition> chain1 = new ArrayList<>();
469    List<StructureDefinition> chain2 = new ArrayList<>();
470    chain1.add(sd1);
471    chain2.add(sd2);
472    StructureDefinition root = library.get("Element");
473    StructureDefinition common = findIntersection(chain1, chain2);
474    boolean chain1Done = false;
475    boolean chain2Done = false;
476    while (common == null) {
477      chain1Done = checkChain(chain1, root, chain1Done);
478      chain2Done = checkChain(chain2, root, chain2Done);
479      if (chain1Done && chain2Done)
480        return null;
481      common = findIntersection(chain1, chain2);
482    }
483    return common;
484  }
485
486  private StructureDefinition findIntersection(List<StructureDefinition> chain1, List<StructureDefinition> chain2) {
487    for (StructureDefinition sd1 : chain1)
488      for (StructureDefinition sd2 : chain2)
489        if (sd1 == sd2)
490          return sd1;
491    return null;
492  }
493
494  public boolean checkChain(List<StructureDefinition> chain1, StructureDefinition root, boolean chain1Done)
495      throws FHIRException {
496    if (!chain1Done) {
497      StructureDefinition sd = chain1.get(chain1.size() - 1);
498      String bu = sd.getBaseDefinition();
499      if (bu == null)
500        throw new FHIRException("No base definition for " + sd.getUrl());
501      StructureDefinition t = library.get(bu);
502      if (t == null)
503        chain1Done = true;
504      else
505        chain1.add(t);
506    }
507    return chain1Done;
508  }
509
510  private StructureDefinition getBase(StructureDefinition structureDefinition) {
511    return null;
512  }
513
514  private String typeNameFromPath(ElementDefinition edc) {
515    StringBuilder b = new StringBuilder();
516    boolean up = true;
517    for (char ch : edc.getPath().toCharArray()) {
518      if (ch == '.')
519        up = true;
520      else if (up) {
521        b.append(Character.toUpperCase(ch));
522        up = false;
523      } else
524        b.append(ch);
525    }
526    return b.toString();
527  }
528
529  private void produceAttribute(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang)
530      throws IOException, FHIRException {
531    TypeRefComponent t = edc.getTypeFirstRep();
532    String name = tailDot(edc.getPath());
533    String min = String.valueOf(edc.getMin());
534    String max = edc.getMax();
535    // todo: check it's a code...
536//    if (!max.equals("1"))
537//      throw new FHIRException("Illegal cardinality \""+max+"\" for attribute "+edc.getPath());
538
539    String tc = t.getWorkingCode();
540    if (Utilities.isAbsoluteUrl(tc))
541      throw new FHIRException("Only FHIR primitive types are supported for attributes (" + tc + ")");
542    String typeNs = namespaces.get("http://hl7.org/fhir");
543    String type = tc;
544
545    w("        <xs:attribute name=\"" + name + "\" use=\""
546        + (min.equals("0") || edc.hasFixed() || edc.hasDefaultValue() ? "optional" : "required") + "\" type=\"" + typeNs
547        + ":" + type + (typeNs.equals("fhir") ? "-primitive" : "") + "\""
548        + (edc.hasFixed() ? " fixed=\"" + edc.getFixed().primitiveValue() + "\"" : "")
549        + (edc.hasDefaultValue() && !edc.hasFixed() ? " default=\"" + edc.getDefaultValue().primitiveValue() + "\""
550            : "")
551        + "");
552    if (annotations && edc.hasDefinition()) {
553      ln(">");
554      ln("          <xs:annotation>");
555      ln("            <xs:documentation xml:lang=\"" + lang + "\">" + Utilities.escapeXml(edc.getDefinition())
556          + "</xs:documentation>");
557      ln("          </xs:annotation>");
558      ln("        </xs:attribute>");
559    } else
560      ln("/>");
561  }
562
563}