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