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