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