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