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