001package org.hl7.fhir.r4.conformance;
002import java.io.FileOutputStream;
003/*
004Copyright (c) 2011+, HL7, Inc
005All rights reserved.
006
007Redistribution and use in source and binary forms, with or without modification, 
008are 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
019THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
020ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
021WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
022IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
023INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
024NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
025PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
026WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
027ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
028POSSIBILITY OF SUCH DAMAGE.
029
030 */
031import java.io.IOException;
032import java.io.OutputStreamWriter;
033import java.util.ArrayList;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.LinkedList;
037import java.util.List;
038import java.util.Map;
039import java.util.Queue;
040import java.util.Set;
041
042import org.hl7.fhir.exceptions.FHIRException;
043import org.hl7.fhir.r4.context.IWorkerContext;
044import org.hl7.fhir.r4.model.ElementDefinition;
045import org.hl7.fhir.r4.model.ElementDefinition.PropertyRepresentation;
046import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent;
047import org.hl7.fhir.r4.model.StructureDefinition;
048import org.hl7.fhir.r4.utils.ToolingExtensions;
049import org.hl7.fhir.utilities.CommaSeparatedStringBuilder;
050import org.hl7.fhir.utilities.Utilities;
051
052
053public class XmlSchemaGenerator  {
054
055  public class QName {
056
057    public String type;
058    public String typeNs;
059
060    @Override
061    public String toString() {
062      return typeNs+":"+type;
063    }
064  }
065
066  public class ElementToGenerate {
067
068    private String tname;
069    private StructureDefinition sd;
070    private ElementDefinition ed;
071
072    public ElementToGenerate(String tname, StructureDefinition sd, ElementDefinition edc) {
073      this.tname = tname;
074      this.sd = sd;
075      this.ed = edc;
076    }
077
078
079  }
080
081
082  private String folder;
083        private IWorkerContext context;
084        private boolean single;
085        private String version;
086        private String genDate;
087        private String license;
088        private boolean annotations;
089
090        public XmlSchemaGenerator(String folder, IWorkerContext context) {
091    this.folder = folder;
092    this.context = context;
093        }
094
095  public boolean isSingle() {
096    return single;
097  }
098
099  public void setSingle(boolean single) {
100    this.single = single;
101  }
102  
103
104  public String getVersion() {
105    return version;
106  }
107
108  public void setVersion(String version) {
109    this.version = version;
110  }
111
112  public String getGenDate() {
113    return genDate;
114  }
115
116  public void setGenDate(String genDate) {
117    this.genDate = genDate;
118  }
119
120  public String getLicense() {
121    return license;
122  }
123
124  public void setLicense(String license) {
125    this.license = license;
126  }
127
128
129  public boolean isAnnotations() {
130    return annotations;
131  }
132
133  public void setAnnotations(boolean annotations) {
134    this.annotations = annotations;
135  }
136
137
138  private Set<ElementDefinition> processed = new HashSet<ElementDefinition>();
139  private Set<StructureDefinition> processedLibs = new HashSet<StructureDefinition>();
140  private Set<String> typeNames = new HashSet<String>();
141  private OutputStreamWriter writer;
142  private Map<String, String> namespaces = new HashMap<String, String>();
143  private Queue<ElementToGenerate> queue = new LinkedList<ElementToGenerate>();
144  private Queue<StructureDefinition> queueLib = new LinkedList<StructureDefinition>();
145  private Map<String, StructureDefinition> library;
146  private boolean useNarrative;
147
148  private void w(String s) throws IOException {
149    writer.write(s);
150  }
151  
152  private void ln(String s) throws IOException {
153    writer.write(s);
154    writer.write("\r\n");
155  }
156
157  private void close() throws IOException {
158    if (writer != null) {
159      ln("</xs:schema>");
160      writer.flush();
161      writer.close();
162      writer = null;
163    }
164  }
165
166  private String start(StructureDefinition sd, String ns) throws IOException, FHIRException {
167    String lang = "en";
168    if (sd.hasLanguage())
169      lang = sd.getLanguage();
170
171    if (single && writer != null) {
172      if (!ns.equals(getNs(sd)))
173        throw new FHIRException("namespace inconsistency: "+ns+" vs "+getNs(sd));
174      return lang;
175    }
176    close();
177    
178    writer = new OutputStreamWriter(new FileOutputStream(Utilities.path(folder, tail(sd.getType()+".xsd"))), "UTF-8");
179    ln("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
180    ln("<!-- ");
181    ln(license);
182    ln("");
183    ln("  Generated on "+genDate+" for FHIR v"+version+" ");
184    ln("");
185    ln("  Note: this schema does not contain all the knowledge represented in the underlying content model");
186    ln("");
187    ln("-->");
188    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\" "+
189        "xmlns:lm=\""+ns+"\" targetNamespace=\""+ns+"\" elementFormDefault=\"qualified\" version=\"1.0\">");
190    ln("  <xs:import schemaLocation=\"fhir-common.xsd\" namespace=\"http://hl7.org/fhir\"/>");
191    if (useNarrative) {
192      if (ns.equals("urn:hl7-org:v3"))
193        ln("  <xs:include schemaLocation=\"cda-narrative.xsd\"/>");
194      else
195        ln("  <xs:import schemaLocation=\"cda-narrative.xsd\" namespace=\"urn:hl7-org:v3\"/>");
196    }
197    namespaces.clear();
198    namespaces.put(ns, "lm");
199    namespaces.put("http://hl7.org/fhir", "fhir");
200    typeNames.clear();
201    
202    return lang;
203  }
204
205
206  private String getNs(StructureDefinition sd) {
207    String ns = "http://hl7.org/fhir";
208    if (sd.hasExtension("http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace"))
209      ns = ToolingExtensions.readStringExtension(sd, "http://hl7.org/fhir/StructureDefinition/elementdefinition-namespace");
210    return ns;
211  }
212
213        public void generate(StructureDefinition entry, Map<String, StructureDefinition> library) throws Exception {
214          processedLibs.clear();
215          
216          this.library = library;
217          checkLib(entry);
218          
219          String ns = getNs(entry);
220          String lang = start(entry, ns);
221
222          w("  <xs:element name=\""+tail(entry.getType())+"\" type=\"lm:"+tail(entry.getType())+"\"");
223    if (annotations) {
224      ln(">");
225      ln("    <xs:annotation>");
226      ln("      <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(entry.getDescription())+"</xs:documentation>");
227      ln("    </xs:annotation>");
228      ln("  </xs:element>");
229    } else
230      ln("/>");
231
232                produceType(entry, entry.getSnapshot().getElement().get(0), tail(entry.getType()), getQN(entry, entry.getBaseDefinition()), lang);
233                while (!queue.isEmpty()) {
234                  ElementToGenerate q = queue.poll();
235                  produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), lang);
236                }
237                while (!queueLib.isEmpty()) {
238                  generateInner(queueLib.poll());
239                }
240                close();
241        }
242
243
244
245
246  private void checkLib(StructureDefinition entry) {
247    for (ElementDefinition ed : entry.getSnapshot().getElement()) {
248      if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) {
249        useNarrative = true;
250      }
251    }
252    for (StructureDefinition sd : library.values()) {
253      for (ElementDefinition ed : sd.getSnapshot().getElement()) {
254        if (ed.hasRepresentation(PropertyRepresentation.CDATEXT)) {
255          useNarrative = true;
256        }
257      }
258    }
259  }
260
261  private void generateInner(StructureDefinition sd) throws IOException, FHIRException {
262    if (processedLibs.contains(sd))
263      return;
264    processedLibs.add(sd);
265    
266    String ns = getNs(sd);
267    String lang = start(sd, ns);
268
269    if (sd.getSnapshot().getElement().isEmpty())
270      throw new FHIRException("no snap shot on "+sd.getUrl());
271    
272    produceType(sd, sd.getSnapshot().getElement().get(0), tail(sd.getType()), getQN(sd, sd.getBaseDefinition()), lang);
273    while (!queue.isEmpty()) {
274      ElementToGenerate q = queue.poll();
275      produceType(q.sd, q.ed, q.tname, getQN(q.sd, q.ed, "http://hl7.org/fhir/StructureDefinition/Element", false), lang);
276    }
277  }
278
279  private String tail(String url) {
280    return url.contains("/") ? url.substring(url.lastIndexOf("/")+1) : url;
281  }
282  private String root(String url) {
283    return url.contains("/") ? url.substring(0, url.lastIndexOf("/")) : "";
284  }
285
286
287  private String tailDot(String url) {
288    return url.contains(".") ? url.substring(url.lastIndexOf(".")+1) : url;
289  }
290  private void produceType(StructureDefinition sd, ElementDefinition ed, String typeName, QName typeParent, String lang) throws IOException, FHIRException {
291    if (processed.contains(ed))
292      return;
293    processed.add(ed);
294    
295    // ok 
296    ln("  <xs:complexType name=\""+typeName+"\">");
297    if (annotations) {
298      ln("    <xs:annotation>");
299      ln("      <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(ed.getDefinition())+"</xs:documentation>");
300      ln("    </xs:annotation>");
301    }
302    ln("    <xs:complexContent>");
303    ln("      <xs:extension base=\""+typeParent.toString()+"\">");
304    ln("        <xs:sequence>");
305    
306    // hack....
307    for (ElementDefinition edc : ProfileUtilities.getChildList(sd,  ed)) {
308      if (!(edc.hasRepresentation(PropertyRepresentation.XMLATTR) || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc))
309        produceElement(sd, ed, edc, lang);
310    }
311    ln("        </xs:sequence>");
312    for (ElementDefinition edc : ProfileUtilities.getChildList(sd,  ed)) {
313      if ((edc.hasRepresentation(PropertyRepresentation.XMLATTR) || edc.hasRepresentation(PropertyRepresentation.XMLTEXT)) && !inheritedElement(edc))
314        produceAttribute(sd, ed, edc, lang);
315    }
316    ln("      </xs:extension>");
317    ln("    </xs:complexContent>");
318    ln("  </xs:complexType>");    
319  }
320
321
322  private boolean inheritedElement(ElementDefinition edc) {
323    return !edc.getPath().equals(edc.getBase().getPath());
324  }
325
326  private void produceElement(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang) throws IOException, FHIRException {
327    if (edc.getType().size() == 0) 
328      throw new Error("No type at "+edc.getPath());
329    
330    if (edc.getType().size() > 1 && edc.hasRepresentation(PropertyRepresentation.TYPEATTR)) {
331      // first, find the common base type
332      StructureDefinition lib = getCommonAncestor(edc.getType());
333      if (lib == null)
334        throw new Error("Common ancester not found at "+edc.getPath());
335      CommaSeparatedStringBuilder b = new CommaSeparatedStringBuilder();
336      for (TypeRefComponent t : edc.getType()) {
337        b.append(getQN(sd, edc, t.getCode(), true).toString());
338      }
339      
340      String name = tailDot(edc.getPath());
341      String min = String.valueOf(edc.getMin());
342      String max = edc.getMax();
343      if ("*".equals(max))
344        max = "unbounded";
345
346      QName qn = getQN(sd, edc, lib.getUrl(), true);
347
348      ln("        <xs:element name=\""+name+"\" minOccurs=\""+min+"\" maxOccurs=\""+max+"\" type=\""+qn.typeNs+":"+qn.type+"\">");
349      ln("          <xs:annotation>");
350      ln("          <xs:appinfo xml:lang=\"en\">Possible types: "+b.toString()+"</xs:appinfo>");
351      if (annotations && edc.hasDefinition()) 
352        ln("            <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</xs:documentation>");
353      ln("          </xs:annotation>");
354      ln("        </xs:element>");
355    } else for (TypeRefComponent t : edc.getType()) {
356      String name = tailDot(edc.getPath());
357      if (edc.getType().size() > 1)
358        name = name + Utilities.capitalize(t.getCode());
359      QName qn = getQN(sd, edc, t.getCode(), true);
360      String min = String.valueOf(edc.getMin());
361      String max = edc.getMax();
362      if ("*".equals(max))
363        max = "unbounded";
364
365
366      w("        <xs:element name=\""+name+"\" minOccurs=\""+min+"\" maxOccurs=\""+max+"\" type=\""+qn.typeNs+":"+qn.type+"\"");
367      if (annotations && edc.hasDefinition()) {
368        ln(">");
369        ln("          <xs:annotation>");
370        ln("            <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</xs:documentation>");
371        ln("          </xs:annotation>");
372        ln("        </xs:element>");
373      } else
374        ln("/>");
375    }
376  }
377
378  public QName getQN(StructureDefinition sd, String type) throws FHIRException {
379    return getQN(sd, sd.getSnapshot().getElementFirstRep(), type, false);
380  }
381  
382  public QName getQN(StructureDefinition sd, ElementDefinition edc, String t, boolean chase) throws FHIRException {
383    QName qn = new QName();
384    qn.type = Utilities.isAbsoluteUrl(t) ? tail(t) : t;
385    if (Utilities.isAbsoluteUrl(t)) {
386      String ns = root(t);
387      if (ns.equals(root(sd.getUrl())))
388        ns = getNs(sd);
389      if (ns.equals("http://hl7.org/fhir/StructureDefinition"))
390        ns = "http://hl7.org/fhir";
391      if (!namespaces.containsKey(ns))
392        throw new FHIRException("Unknown type namespace "+ns+" for "+edc.getPath());
393      qn.typeNs = namespaces.get(ns);
394      StructureDefinition lib = library.get(t);
395      if (lib == null && !Utilities.existsInList(t, "http://hl7.org/fhir/cda/StructureDefinition/StrucDoc.Text", "http://hl7.org/fhir/StructureDefinition/Element"))
396        throw new FHIRException("Unable to resolve "+t+" for "+edc.getPath());
397      if (lib != null) 
398        queueLib.add(lib);
399    } else
400      qn.typeNs = namespaces.get("http://hl7.org/fhir");
401
402    if (chase && qn.type.equals("Element")) {
403      String tname = typeNameFromPath(edc);
404      if (typeNames.contains(tname)) {
405        int i = 1;
406        while (typeNames.contains(tname+i)) 
407          i++;
408        tname = tname+i;
409      }
410      queue.add(new ElementToGenerate(tname, sd, edc));
411      qn.typeNs = "lm";
412      qn.type = tname;
413    }
414    return qn;
415  }
416  
417  private StructureDefinition getCommonAncestor(List<TypeRefComponent> type) throws FHIRException {
418    StructureDefinition sd = library.get(type.get(0).getCode());
419    if (sd == null)
420      throw new FHIRException("Unable to find definition for "+type.get(0).getCode()); 
421    for (int i = 1; i < type.size(); i++) {
422      StructureDefinition t = library.get(type.get(i).getCode());
423      if (t == null)
424        throw new FHIRException("Unable to find definition for "+type.get(i).getCode()); 
425      sd = getCommonAncestor(sd, t);
426    }
427    return sd;
428  }
429
430  private StructureDefinition getCommonAncestor(StructureDefinition sd1, StructureDefinition sd2) throws FHIRException {
431    // this will always return something because everything comes from Element
432    List<StructureDefinition> chain1 = new ArrayList<>();
433    List<StructureDefinition> chain2 = new ArrayList<>();
434    chain1.add(sd1);
435    chain2.add(sd2);
436    StructureDefinition root = library.get("Element");
437    StructureDefinition common = findIntersection(chain1, chain2);
438    boolean chain1Done = false;
439    boolean chain2Done = false;
440    while (common == null) {
441       chain1Done = checkChain(chain1, root, chain1Done);
442       chain2Done = checkChain(chain2, root, chain2Done);
443       if (chain1Done && chain2Done)
444         return null;
445       common = findIntersection(chain1, chain2);
446    }
447    return common;
448  }
449
450  
451  private StructureDefinition findIntersection(List<StructureDefinition> chain1, List<StructureDefinition> chain2) {
452    for (StructureDefinition sd1 : chain1)
453      for (StructureDefinition sd2 : chain2)
454        if (sd1 == sd2)
455          return sd1;
456    return null;
457  }
458
459  public boolean checkChain(List<StructureDefinition> chain1, StructureDefinition root, boolean chain1Done) throws FHIRException {
460    if (!chain1Done) {
461       StructureDefinition sd = chain1.get(chain1.size()-1);
462      String bu = sd.getBaseDefinition();
463       if (bu == null)
464         throw new FHIRException("No base definition for "+sd.getUrl());
465       StructureDefinition t = library.get(bu);
466       if (t == null)
467         chain1Done = true;
468       else
469         chain1.add(t);
470     }
471    return chain1Done;
472  }
473
474  private StructureDefinition getBase(StructureDefinition structureDefinition) {
475    return null;
476  }
477
478  private String typeNameFromPath(ElementDefinition edc) {
479    StringBuilder b = new StringBuilder();
480    boolean up = true;
481    for (char ch : edc.getPath().toCharArray()) {
482      if (ch == '.')
483        up = true;
484      else if (up) {
485        b.append(Character.toUpperCase(ch));
486        up = false;
487      } else
488        b.append(ch);
489    }
490    return b.toString();
491  }
492
493  private void produceAttribute(StructureDefinition sd, ElementDefinition ed, ElementDefinition edc, String lang) throws IOException, FHIRException {
494    TypeRefComponent t = edc.getTypeFirstRep();
495    String name = tailDot(edc.getPath());
496    String min = String.valueOf(edc.getMin());
497    String max = edc.getMax();
498    // todo: check it's a code...
499//    if (!max.equals("1"))
500//      throw new FHIRException("Illegal cardinality \""+max+"\" for attribute "+edc.getPath());
501    
502    if (Utilities.isAbsoluteUrl(t.getCode())) 
503      throw new FHIRException("Only FHIR primitive types are supported for attributes ("+t.getCode()+")");
504    String typeNs = namespaces.get("http://hl7.org/fhir");
505    String type = t.getCode(); 
506    
507    w("        <xs:attribute name=\""+name+"\" use=\""+(min.equals("0") || edc.hasFixed() || edc.hasDefaultValue() ? "optional" : "required")+"\" type=\""+typeNs+":"+type+(typeNs.equals("fhir") ? "-primitive" : "")+"\""+
508    (edc.hasFixed() ? " fixed=\""+edc.getFixed().primitiveValue()+"\"" : "")+(edc.hasDefaultValue() && !edc.hasFixed() ? " default=\""+edc.getDefaultValue().primitiveValue()+"\"" : "")+"");
509    if (annotations && edc.hasDefinition()) {
510      ln(">");
511      ln("          <xs:annotation>");
512      ln("            <xs:documentation xml:lang=\""+lang+"\">"+Utilities.escapeXml(edc.getDefinition())+"</xs:documentation>");
513      ln("          </xs:annotation>");
514      ln("        </xs:attribute>");
515    } else
516      ln("/>");
517  }
518
519        
520}