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}