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