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