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}