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