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