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