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