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