001package org.hl7.fhir.r4.utils; 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 033 034// todo: 035// - generate sort order parameters 036// - generate inherited search parameters 037 038import java.io.BufferedWriter; 039import java.io.IOException; 040import java.io.OutputStream; 041import java.io.OutputStreamWriter; 042import java.util.ArrayList; 043import java.util.Collections; 044import java.util.EnumSet; 045import java.util.HashMap; 046import java.util.HashSet; 047import java.util.List; 048import java.util.Map; 049import java.util.Set; 050 051import org.hl7.fhir.exceptions.FHIRException; 052import org.hl7.fhir.r4.conformance.ProfileUtilities; 053import org.hl7.fhir.r4.context.IWorkerContext; 054import org.hl7.fhir.r4.model.Constants; 055import org.hl7.fhir.r4.model.ElementDefinition; 056import org.hl7.fhir.r4.model.ElementDefinition.TypeRefComponent; 057import org.hl7.fhir.r4.model.Enumerations.SearchParamType; 058import org.hl7.fhir.r4.model.SearchParameter; 059import org.hl7.fhir.r4.model.StructureDefinition; 060import org.hl7.fhir.r4.model.StructureDefinition.StructureDefinitionKind; 061import org.hl7.fhir.r4.model.StructureDefinition.TypeDerivationRule; 062import org.hl7.fhir.utilities.Utilities; 063 064public class GraphQLSchemaGenerator { 065 066 public enum FHIROperationType {READ, SEARCH, CREATE, UPDATE, DELETE}; 067 068 private static final String INNER_TYPE_NAME = "gql.type.name"; 069 private static final Set<String> JSON_NUMBER_TYPES = new HashSet<String>() {{ 070 add("decimal"); 071 add("positiveInt"); 072 add("unsignedInt"); 073 }}; 074 075 IWorkerContext context; 076 077 public GraphQLSchemaGenerator(IWorkerContext context) { 078 super(); 079 this.context = context; 080 } 081 082 public void generateTypes(OutputStream stream) throws IOException, FHIRException { 083 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream)); 084 085 Map<String, StructureDefinition> pl = new HashMap<String, StructureDefinition>(); 086 Map<String, StructureDefinition> tl = new HashMap<String, StructureDefinition>(); 087 for (StructureDefinition sd : context.allStructures()) { 088 if (sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { 089 pl.put(sd.getName(), sd); 090 } 091 if (sd.getKind() == StructureDefinitionKind.COMPLEXTYPE && sd.getDerivation() == TypeDerivationRule.SPECIALIZATION) { 092 tl.put(sd.getName(), sd); 093 } 094 } 095 writer.write("# FHIR GraphQL Schema. Version "+Constants.VERSION+"\r\n\r\n"); 096 writer.write("# FHIR Defined Primitive types\r\n"); 097 for (String n : sorted(pl.keySet())) 098 generatePrimitive(writer, pl.get(n)); 099 writer.write("\r\n"); 100 writer.write("# FHIR Defined Search Parameter Types\r\n"); 101 for (SearchParamType dir : SearchParamType.values()) { 102 if (dir != SearchParamType.NULL) 103 generateSearchParamType(writer, dir.toCode()); 104 } 105 writer.write("\r\n"); 106 generateElementBase(writer); 107 for (String n : sorted(tl.keySet())) 108 generateType(writer, tl.get(n)); 109 writer.flush(); 110 writer.close(); 111 } 112 113 public void generateResource(OutputStream stream, StructureDefinition sd, List<SearchParameter> parameters, EnumSet<FHIROperationType> operations) throws IOException, FHIRException { 114 BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream)); 115 writer.write("# FHIR GraphQL Schema. Version "+Constants.VERSION+"\r\n\r\n"); 116 writer.write("# import * from 'types.graphql'\r\n\r\n"); 117 generateType(writer, sd); 118 if (operations.contains(FHIROperationType.READ)) 119 generateIdAccess(writer, sd.getName()); 120 if (operations.contains(FHIROperationType.SEARCH)) { 121 generateListAccess(writer, parameters, sd.getName()); 122 generateConnectionAccess(writer, parameters, sd.getName()); 123 } 124 if (operations.contains(FHIROperationType.CREATE)) 125 generateCreate(writer, sd.getName()); 126 if (operations.contains(FHIROperationType.UPDATE)) 127 generateUpdate(writer, sd.getName()); 128 if (operations.contains(FHIROperationType.DELETE)) 129 generateDelete(writer, sd.getName()); 130 writer.flush(); 131 writer.close(); 132 } 133 134 private void generateCreate(BufferedWriter writer, String name) throws IOException { 135 writer.write("type "+name+"CreateType {\r\n"); 136 writer.write(" "+name+"Create("); 137 param(writer, "resource", name+"Input", false, false); 138 writer.write(") : "+name+"Creation\r\n"); 139 writer.write("}\r\n"); 140 writer.write("\r\n"); 141 writer.write("type "+name+"Creation {\r\n"); 142 writer.write(" location : String\r\n"); 143 writer.write(" resource : "+name+"\r\n"); 144 writer.write(" information : OperationOutcome\r\n"); 145 writer.write("}\r\n"); 146 writer.write("\r\n"); 147 } 148 149 private void generateUpdate(BufferedWriter writer, String name) throws IOException { 150 writer.write("type "+name+"UpdateType {\r\n"); 151 writer.write(" "+name+"Update("); 152 param(writer, "id", "ID", false, false); 153 writer.write(", "); 154 param(writer, "resource", name+"Input", false, false); 155 writer.write(") : "+name+"Update\r\n"); 156 writer.write("}\r\n"); 157 writer.write("\r\n"); 158 writer.write("type "+name+"Update {\r\n"); 159 writer.write(" resource : "+name+"\r\n"); 160 writer.write(" information : OperationOutcome\r\n"); 161 writer.write("}\r\n"); 162 writer.write("\r\n"); 163 } 164 165 private void generateDelete(BufferedWriter writer, String name) throws IOException { 166 writer.write("type "+name+"DeleteType {\r\n"); 167 writer.write(" "+name+"Delete("); 168 param(writer, "id", "ID", false, false); 169 writer.write(") : "+name+"Delete\r\n"); 170 writer.write("}\r\n"); 171 writer.write("\r\n"); 172 writer.write("type "+name+"Delete {\r\n"); 173 writer.write(" information : OperationOutcome\r\n"); 174 writer.write("}\r\n"); 175 writer.write("\r\n"); 176 } 177 178 private void generateListAccess(BufferedWriter writer, List<SearchParameter> parameters, String name) throws IOException { 179 writer.write("type "+name+"ListType {\r\n"); 180 writer.write(" "+name+"List("); 181 param(writer, "_filter", "String", false, false); 182 for (SearchParameter sp : parameters) 183 param(writer, sp.getName().replace("-", "_"), getGqlname(sp.getType().toCode()), true, true); 184 param(writer, "_sort", "String", false, true); 185 param(writer, "_count", "Int", false, true); 186 param(writer, "_cursor", "String", false, true); 187 writer.write(") : ["+name+"]\r\n"); 188 writer.write("}\r\n"); 189 writer.write("\r\n"); 190 } 191 192 private void param(BufferedWriter writer, String name, String type, boolean list, boolean line) throws IOException { 193 if (line) 194 writer.write("\r\n "); 195 writer.write(name); 196 writer.write(" : "); 197 if (list) 198 writer.write("["); 199 writer.write(type); 200 if (list) 201 writer.write("]"); 202 } 203 204 private void generateConnectionAccess(BufferedWriter writer, List<SearchParameter> parameters, String name) throws IOException { 205 writer.write("type "+name+"ConnectionType {\r\n"); 206 writer.write(" "+name+"Conection("); 207 param(writer, "_filter", "String", false, false); 208 for (SearchParameter sp : parameters) 209 param(writer, sp.getName().replace("-", "_"), getGqlname(sp.getType().toCode()), true, true); 210 param(writer, "_sort", "String", false, true); 211 param(writer, "_count", "Int", false, true); 212 param(writer, "_cursor", "String", false, true); 213 writer.write(") : "+name+"Connection\r\n"); 214 writer.write("}\r\n"); 215 writer.write("\r\n"); 216 writer.write("type "+name+"Connection {\r\n"); 217 writer.write(" count : Int\r\n"); 218 writer.write(" offset : Int\r\n"); 219 writer.write(" pagesize : Int\r\n"); 220 writer.write(" first : ID\r\n"); 221 writer.write(" previous : ID\r\n"); 222 writer.write(" next : ID\r\n"); 223 writer.write(" last : ID\r\n"); 224 writer.write(" edges : ["+name+"Edge]\r\n"); 225 writer.write("}\r\n"); 226 writer.write("\r\n"); 227 writer.write("type "+name+"Edge {\r\n"); 228 writer.write(" mode : String\r\n"); 229 writer.write(" score : Float\r\n"); 230 writer.write(" resource : "+name+"\r\n"); 231 writer.write("}\r\n"); 232 writer.write("\r\n"); 233 } 234 235 236 private void generateIdAccess(BufferedWriter writer, String name) throws IOException { 237 writer.write("type "+name+"ReadType {\r\n"); 238 writer.write(" "+name+"(id : ID!) : "+name+"\r\n"); 239 writer.write("}\r\n"); 240 writer.write("\r\n"); 241 } 242 243 private void generateElementBase(BufferedWriter writer) throws IOException { 244 writer.write("type ElementBase {\r\n"); 245 writer.write(" id : ID\r\n"); 246 writer.write(" extension: [Extension]\r\n"); 247 writer.write("}\r\n"); 248 writer.write("\r\n"); 249 250 writer.write("input ElementBaseInput {\r\n"); 251 writer.write(" id : ID\r\n"); 252 writer.write(" extension: [ExtensionInput]\r\n"); 253 writer.write("}\r\n"); 254 writer.write("\r\n"); 255 } 256 257 private void generateType(BufferedWriter writer, StructureDefinition sd) throws IOException { 258 if (sd.getAbstract()) 259 return; 260 261 List<StringBuilder> list = new ArrayList<StringBuilder>(); 262 StringBuilder b = new StringBuilder(); 263 list.add(b); 264 b.append("type "); 265 b.append(sd.getName()); 266 b.append(" {\r\n"); 267 ElementDefinition ed = sd.getSnapshot().getElementFirstRep(); 268 generateProperties(list, b, sd.getName(), sd, ed, "type", ""); 269 b.append("}"); 270 b.append("\r\n"); 271 b.append("\r\n"); 272 for (StringBuilder bs : list) 273 writer.write(bs.toString()); 274 list.clear(); 275 b = new StringBuilder(); 276 list.add(b); 277 b.append("input "); 278 b.append(sd.getName()); 279 b.append("Input {\r\n"); 280 ed = sd.getSnapshot().getElementFirstRep(); 281 generateProperties(list, b, sd.getName(), sd, ed, "input", "Input"); 282 b.append("}"); 283 b.append("\r\n"); 284 b.append("\r\n"); 285 for (StringBuilder bs : list) 286 writer.write(bs.toString()); 287 } 288 289 private void generateProperties(List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition ed, String mode, String suffix) throws IOException { 290 List<ElementDefinition> children = ProfileUtilities.getChildList(sd, ed); 291 for (ElementDefinition child : children) { 292 if (child.hasContentReference()) { 293 ElementDefinition ref = resolveContentReference(sd, child.getContentReference()); 294 generateProperty(list, b, typeName, sd, child, ref.getType().get(0), false, ref, mode, suffix); 295 } else if (child.getType().size() == 1) { 296 generateProperty(list, b, typeName, sd, child, child.getType().get(0), false, null, mode, suffix); 297 } else { 298 boolean ref = false; 299 for (TypeRefComponent t : child.getType()) { 300 if (!t.hasTarget()) 301 generateProperty(list, b, typeName, sd, child, t, true, null, mode, suffix); 302 else if (!ref) { 303 ref = true; 304 generateProperty(list, b, typeName, sd, child, t, true, null, mode, suffix); 305 } 306 } 307 } 308 } 309 } 310 311 private ElementDefinition resolveContentReference(StructureDefinition sd, String contentReference) { 312 String id = contentReference.substring(1); 313 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 314 if (id.equals(ed.getId())) 315 return ed; 316 } 317 throw new Error("Unable to find "+id); 318 } 319 320 private void generateProperty(List<StringBuilder> list, StringBuilder b, String typeName, StructureDefinition sd, ElementDefinition child, TypeRefComponent typeDetails, boolean suffix, ElementDefinition cr, String mode, String suffixS) throws IOException { 321 if (isPrimitive(typeDetails)) { 322 String n = getGqlname(typeDetails.getWorkingCode()); 323 b.append(" "); 324 b.append(tail(child.getPath(), suffix)); 325 if (suffix) 326 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 327 b.append(": "); 328 b.append(n); 329 if (!child.getPath().endsWith(".id")) { 330 b.append(" _"); 331 b.append(tail(child.getPath(), suffix)); 332 if (suffix) 333 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 334 if (!child.getMax().equals("1")) { 335 b.append(": [ElementBase"); 336 b.append(suffixS); 337 b.append("]\r\n"); 338 } else { 339 b.append(": ElementBase"); 340 b.append(suffixS); 341 b.append("\r\n"); 342 } 343 } else 344 b.append("\r\n"); 345 } else { 346 b.append(" "); 347 b.append(tail(child.getPath(), suffix)); 348 if (suffix) 349 b.append(Utilities.capitalize(typeDetails.getWorkingCode())); 350 b.append(": "); 351 if (!child.getMax().equals("1")) 352 b.append("["); 353 String type = typeDetails.getWorkingCode(); 354 if (cr != null) 355 b.append(generateInnerType(list, sd, typeName, cr, mode, suffixS)); 356 else if (Utilities.existsInList(type, "Element", "BackboneElement")) 357 b.append(generateInnerType(list, sd, typeName, child, mode, suffixS)); 358 else 359 b.append(type+suffixS); 360 if (!child.getMax().equals("1")) 361 b.append("]"); 362 if (child.getMin() != 0 && !suffix) 363 b.append("!"); 364 b.append("\r\n"); 365 } 366 } 367 368 private String generateInnerType(List<StringBuilder> list, StructureDefinition sd, String name, ElementDefinition child, String mode, String suffix) throws IOException { 369 if (child.hasUserData(INNER_TYPE_NAME+"."+mode)) 370 return child.getUserString(INNER_TYPE_NAME+"."+mode); 371 372 String typeName = name+Utilities.capitalize(tail(child.getPath(), false)); 373 child.setUserData(INNER_TYPE_NAME+"."+mode, typeName); 374 StringBuilder b = new StringBuilder(); 375 list.add(b); 376 b.append(mode); 377 b.append(" "); 378 b.append(typeName); 379 b.append(suffix); 380 b.append(" {\r\n"); 381 generateProperties(list, b, typeName, sd, child, mode, suffix); 382 b.append("}"); 383 b.append("\r\n"); 384 b.append("\r\n"); 385 return typeName+suffix; 386 } 387 388 private String tail(String path, boolean suffix) { 389 if (suffix) 390 path = path.substring(0, path.length()-3); 391 int i = path.lastIndexOf("."); 392 return i < 0 ? path : path.substring(i + 1); 393 } 394 395 private boolean isPrimitive(TypeRefComponent type) { 396 String typeName = type.getWorkingCode(); 397 StructureDefinition sd = context.fetchTypeDefinition(typeName); 398 if (sd == null) 399 return false; 400 return sd.getKind() == StructureDefinitionKind.PRIMITIVETYPE; 401 } 402 403 private List<String> sorted(Set<String> keys) { 404 List<String> sl = new ArrayList<>(); 405 sl.addAll(keys); 406 Collections.sort(sl); 407 return sl; 408 } 409 410 private void generatePrimitive(BufferedWriter writer, StructureDefinition sd) throws IOException, FHIRException { 411 String gqlName = getGqlname(sd.getName()); 412 if (gqlName.equals(sd.getName())) { 413 writer.write("scalar "); 414 writer.write(sd.getName()); 415 writer.write(" # JSON Format: "); 416 writer.write(getJsonFormat(sd)); 417 } else { 418 writer.write("# Type "); 419 writer.write(sd.getName()); 420 writer.write(": use GraphQL Scalar type "); 421 writer.write(gqlName); 422 } 423 writer.write("\r\n"); 424 } 425 426 private void generateSearchParamType(BufferedWriter writer, String name) throws IOException, FHIRException { 427 String gqlName = getGqlname(name); 428 if (gqlName.equals("date")) { 429 writer.write("# Search Param "); 430 writer.write(name); 431 writer.write(": already defined as Primitive with JSON Format: string "); 432 } else if (gqlName.equals(name)) { 433 writer.write("scalar "); 434 writer.write(name); 435 writer.write(" # JSON Format: string"); 436 } else { 437 writer.write("# Search Param "); 438 writer.write(name); 439 writer.write(": use GraphQL Scalar type "); 440 writer.write(gqlName); 441 } 442 writer.write("\r\n"); 443 } 444 445 private String getJsonFormat(StructureDefinition sd) throws FHIRException { 446 for (ElementDefinition ed : sd.getSnapshot().getElement()) { 447 if (!ed.getType().isEmpty() && ed.getType().get(0).getCodeElement().hasExtension("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type")) 448 return ed.getType().get(0).getCodeElement().getExtensionString("http://hl7.org/fhir/StructureDefinition/structuredefinition-json-type"); 449 } 450 // all primitives but JSON_NUMBER_TYPES are represented as JSON strings 451 if (JSON_NUMBER_TYPES.contains(sd.getName())) { 452 return "number"; 453 } else { 454 return "string"; 455 } 456 } 457 458 private String getGqlname(String name) { 459 if (name.equals("string")) 460 return "String"; 461 if (name.equals("integer")) 462 return "Int"; 463 if (name.equals("boolean")) 464 return "Boolean"; 465 if (name.equals("id")) 466 return "ID"; 467 return name; 468 } 469}