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