From 791e99c3c79e278cc417e7b060d35ef9bad4d492 Mon Sep 17 00:00:00 2001 From: Lee Surprenant Date: Wed, 17 May 2023 09:59:13 -0400 Subject: [PATCH] add the missing fhir-swagger-generator source Signed-off-by: Lee Surprenant --- .../generator/FHIROpenApiGenerator.java | 2686 +++++++++++++++++ .../swagger/generator/APIConnectAdapter.java | 84 + .../generator/FHIRSwaggerGenerator.java | 2058 +++++++++++++ 3 files changed, 4828 insertions(+) create mode 100644 fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/openapi/generator/FHIROpenApiGenerator.java create mode 100644 fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/APIConnectAdapter.java create mode 100644 fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/FHIRSwaggerGenerator.java diff --git a/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/openapi/generator/FHIROpenApiGenerator.java b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/openapi/generator/FHIROpenApiGenerator.java new file mode 100644 index 00000000000..764092c7581 --- /dev/null +++ b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/openapi/generator/FHIROpenApiGenerator.java @@ -0,0 +1,2686 @@ +/* + * (C) Copyright IBM Corp. 2018, 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.linuxforhealth.fhir.openapi.generator; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.linuxforhealth.fhir.core.FHIRMediaType; +import org.linuxforhealth.fhir.examples.ExamplesUtil; +import org.linuxforhealth.fhir.model.annotation.Required; +import org.linuxforhealth.fhir.model.format.Format; +import org.linuxforhealth.fhir.model.parser.FHIRParser; +import org.linuxforhealth.fhir.model.resource.ActivityDefinition; +import org.linuxforhealth.fhir.model.resource.Bundle; +import org.linuxforhealth.fhir.model.resource.Bundle.Entry; +import org.linuxforhealth.fhir.model.resource.CapabilityStatement; +import org.linuxforhealth.fhir.model.resource.DeviceDefinition; +import org.linuxforhealth.fhir.model.resource.DomainResource; +import org.linuxforhealth.fhir.model.resource.EventDefinition; +import org.linuxforhealth.fhir.model.resource.EvidenceVariable; +import org.linuxforhealth.fhir.model.resource.Group; +import org.linuxforhealth.fhir.model.resource.GuidanceResponse; +import org.linuxforhealth.fhir.model.resource.Library; +import org.linuxforhealth.fhir.model.resource.Measure; +import org.linuxforhealth.fhir.model.resource.MedicationDispense; +import org.linuxforhealth.fhir.model.resource.MedicationKnowledge; +import org.linuxforhealth.fhir.model.resource.MedicationRequest; +import org.linuxforhealth.fhir.model.resource.MedicationStatement; +import org.linuxforhealth.fhir.model.resource.OperationOutcome; +import org.linuxforhealth.fhir.model.resource.Parameters; +import org.linuxforhealth.fhir.model.resource.Patient; +import org.linuxforhealth.fhir.model.resource.PlanDefinition; +import org.linuxforhealth.fhir.model.resource.RequestGroup; +import org.linuxforhealth.fhir.model.resource.ResearchElementDefinition; +import org.linuxforhealth.fhir.model.resource.Resource; +import org.linuxforhealth.fhir.model.resource.SearchParameter; +import org.linuxforhealth.fhir.model.resource.StructureDefinition; +import org.linuxforhealth.fhir.model.resource.StructureMap; +import org.linuxforhealth.fhir.model.resource.Task; +import org.linuxforhealth.fhir.model.type.BackboneElement; +import org.linuxforhealth.fhir.model.type.Code; +import org.linuxforhealth.fhir.model.type.ContactDetail; +import org.linuxforhealth.fhir.model.type.Contributor; +import org.linuxforhealth.fhir.model.type.DataRequirement; +import org.linuxforhealth.fhir.model.type.Dosage; +import org.linuxforhealth.fhir.model.type.Element; +import org.linuxforhealth.fhir.model.type.ElementDefinition; +import org.linuxforhealth.fhir.model.type.Expression; +import org.linuxforhealth.fhir.model.type.Extension; +import org.linuxforhealth.fhir.model.type.Identifier; +import org.linuxforhealth.fhir.model.type.MarketingStatus; +import org.linuxforhealth.fhir.model.type.Oid; +import org.linuxforhealth.fhir.model.type.ParameterDefinition; +import org.linuxforhealth.fhir.model.type.Population; +import org.linuxforhealth.fhir.model.type.ProdCharacteristic; +import org.linuxforhealth.fhir.model.type.ProductShelfLife; +import org.linuxforhealth.fhir.model.type.Reference; +import org.linuxforhealth.fhir.model.type.RelatedArtifact; +import org.linuxforhealth.fhir.model.type.TriggerDefinition; +import org.linuxforhealth.fhir.model.type.UsageContext; +import org.linuxforhealth.fhir.model.type.Uuid; +import org.linuxforhealth.fhir.model.util.ModelSupport; +import org.linuxforhealth.fhir.model.visitor.AbstractVisitable; +import org.linuxforhealth.fhir.search.compartment.CompartmentHelper; +import org.linuxforhealth.fhir.search.exception.FHIRSearchException; +import org.linuxforhealth.fhir.search.util.SearchHelper; +import org.linuxforhealth.fhir.swagger.generator.APIConnectAdapter; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonReader; +import jakarta.json.JsonStructure; +import jakarta.json.JsonWriter; +import jakarta.json.JsonWriterFactory; +import jakarta.json.stream.JsonGenerator; + +/** + * Generate OpenAPI 3.0 from the HL7 FHIR R4 artifacts and IBM FHIR object model. + * + *

+ * By default, this class will generate: + *

    + *
  1. an "all-in-one" OpenAPI definition for the entire api + *
  2. a separate OpenAPI definition for each resource type and compartment, with all HTTP interactions enabled + *
+ * + *

+ * To limit the output to a given set of resources and/or interactions, pass a set of semicolon-delimited + * filter strings of the form {@code ResourceType1(interaction1,interaction2)}. + * + * For example: + *

+ * Patient(create,read,vread,history,search,update,delete);Contract(create,read,vread,history,search);RiskAssessment(read)
+ * 
+ */ +public class FHIROpenApiGenerator { + /** + * This should match the webApplication contextRoot in server.xml (unless the path is rewritten in front of the server) + */ + private static final String CONTEXT_ROOT = "/fhir-server/api/v4"; + // TODO: make this configurable + private static final String OUTDIR = "openapi"; + private static final JsonBuilderFactory factory = Json.createBuilderFactory(null); + private static final Map, StructureDefinition> structureDefinitionMap = buildStructureDefinitionMap(); + private static boolean includeDeleteOperation = true; + public static final List> DATA_EXPORT_TYPES = Arrays.asList(Patient.class, Group.class); + + public static final String TYPEPACKAGENAME = "org.linuxforhealth.fhir.model.type"; + public static final String RESOURCEPACKAGENAME = "org.linuxforhealth.fhir.model.resource"; + public static final String APPLICATION_FORM = "application/x-www-form-urlencoded"; + private static List classNamesList = null; + + private static final CompartmentHelper compartmentHelper = new CompartmentHelper(); + private static final SearchHelper searchHelper = new SearchHelper(); + + public static void main(String[] args) throws Exception { + File file = new File(OUTDIR); + if (!file.exists()) { + file.mkdirs(); + } + System.out.println("Generating openapi definitions at " + file.getCanonicalPath()); + + Filter filter = null; + if (args.length == 1) { + filter = createFilter(args[0]); + } else { + filter = createAcceptAllFilter(); + } + + generateAllInOne(filter); // outputs to all-openapi.json + + generateOneFilePerResource(filter); // outputs to ResourceType-openapi.json + generateOneFilePerCompartment(filter); // outputs to ResourceType-compartment-openapi.json + generateMetadataOpenApi(); + generateBatchTransactionOpenApi(filter); + generateWholeSystemHistoryOpenApi(); + if (filter.acceptOperation("export")) { + generateExportOpenApi(filter); // outputs to export-openapi.json + } + } + + private static void generateAllInOne(Filter filter) throws Exception { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Simplified FHIR API"); + info.add("description", "A simplified version of the HL7 FHIR API"); + info.add("version", "4.0.1"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + // LinkedHashSet to preserve the order but still avoid duplicates + Set> tagsToAdd = new LinkedHashSet<>(); + Set> requestBodiesToAdd = new LinkedHashSet<>(); + Set> definitionsToAdd = new LinkedHashSet<>(); + // common entries for all types + definitionsToAdd.add(Resource.class); + definitionsToAdd.add(DomainResource.class); + // for search response + if (filter.acceptOperation("search")) { + definitionsToAdd.add(Bundle.class); + definitionsToAdd.add(Bundle.Link.class); + definitionsToAdd.add(Bundle.Entry.class); + definitionsToAdd.add(Bundle.Entry.Request.class); + definitionsToAdd.add(Bundle.Entry.Response.class); + definitionsToAdd.add(Bundle.Entry.Search.class); + } + // for /metadata + definitionsToAdd.add(CapabilityStatement.class); + definitionsToAdd.add(CapabilityStatement.Software.class); + definitionsToAdd.add(CapabilityStatement.Implementation.class); + definitionsToAdd.add(CapabilityStatement.Rest.class); + definitionsToAdd.add(CapabilityStatement.Rest.Security.class); + definitionsToAdd.add(CapabilityStatement.Rest.Resource.class); + definitionsToAdd.add(CapabilityStatement.Rest.Resource.Interaction.class); + definitionsToAdd.add(CapabilityStatement.Rest.Resource.SearchParam.class); + definitionsToAdd.add(CapabilityStatement.Rest.Resource.Operation.class); + definitionsToAdd.add(CapabilityStatement.Rest.Interaction.class); + definitionsToAdd.add(CapabilityStatement.Messaging.class); + definitionsToAdd.add(CapabilityStatement.Messaging.Endpoint.class); + definitionsToAdd.add(CapabilityStatement.Messaging.SupportedMessage.class); + definitionsToAdd.add(CapabilityStatement.Document.class); + // for error response + definitionsToAdd.add(OperationOutcome.class); + definitionsToAdd.add(OperationOutcome.Issue.class); + + if (filter.acceptOperation("export")) { + definitionsToAdd.add(Parameters.class); + } + + JsonArrayBuilder tags = factory.createArrayBuilder(); + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder requestBodies = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + for (String resourceClassName : getClassNames()) { + Class resourceModelClass = Class.forName(RESOURCEPACKAGENAME + "." + resourceClassName); + if (DomainResource.class.isAssignableFrom(resourceModelClass) + && DomainResource.class != resourceModelClass) { + if (filter.acceptResourceType(resourceModelClass)) { + + generatePaths(resourceModelClass, paths, filter); + tagsToAdd.add(resourceModelClass); + requestBodiesToAdd.add(resourceModelClass); + definitionsToAdd.add(resourceModelClass); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton(resourceClassName); + addDefinitionsForInnerClasses(classFilter, definitionsToAdd); + + // add all the applicable data types to the set of definitions + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, resourceModelClass)) { + definitionsToAdd.add(typeModelClass); + } + } + } + + // Within compartments, only include resource types that are accepted by the filter + for (String withinCompartmentResourceClassName : getCompartmentClassNames(resourceClassName)) { + Class withinCompartmentResourceModelClass = Class.forName(RESOURCEPACKAGENAME + "." + withinCompartmentResourceClassName); + if (DomainResource.class.isAssignableFrom(withinCompartmentResourceModelClass) + && DomainResource.class != withinCompartmentResourceModelClass + && filter.acceptResourceType(withinCompartmentResourceModelClass)) { + + generateCompartmentPaths(resourceModelClass, withinCompartmentResourceModelClass, paths, filter); + tagsToAdd.add(withinCompartmentResourceModelClass); + requestBodiesToAdd.add(withinCompartmentResourceModelClass); + definitionsToAdd.add(withinCompartmentResourceModelClass); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton(withinCompartmentResourceClassName); + addDefinitionsForInnerClasses(classFilter, definitionsToAdd); + + // add all the applicable data types to the set of definitions + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, withinCompartmentResourceModelClass)) { + definitionsToAdd.add(typeModelClass); + } + } + } + } + } + } + + for (Class clazz : tagsToAdd) { + JsonObjectBuilder tag = factory.createObjectBuilder(); + tag.add("name", clazz.getSimpleName()); + tags.add(tag); + } + + for (Class clazz : requestBodiesToAdd) { + generateRequestBody(clazz, requestBodies); + } + + for (Class clazz : definitionsToAdd) { + generateDefinition(clazz, definitions); + } + + JsonObjectBuilder components = factory.createObjectBuilder(); + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateSearchParameters(parameters, filter); + generateWholeSystemHistoryParameters(parameters, true); + + // Generate export parameters if enabled + if (filter.acceptOperation("export")) { + generateExportParameters(parameters, filter.acceptOperation(Group.class, "export")); + generateDefinition(Parameters.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton("Parameters"); + generateDefinitionForInnerClasses(classFilter, definitions); + } + + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + components.add("parameters", parametersObject); + } + + // FHIR metadata operation + JsonObjectBuilder path = factory.createObjectBuilder(); + generateMetadataPathItem(path); + paths.add("/metadata", path); + + // FHIR batch/transaction operation + path = factory.createObjectBuilder(); + generateBatchPathItem(path); + paths.add("/", path); + + // FHIR Whole System History operation + path = factory.createObjectBuilder(); + generateWholeSystemHistoryPathItem(path, "Other", "Get the whole system history", false); + paths.add("/_history", path); + + // FHIR Export operation + if (filter.acceptOperation("export")) { + addExportOperationPaths(filter, paths); + } + + components.add("requestBodies", requestBodies); + components.add("schemas", definitions); + + swagger.add("tags", tags); + swagger.add("paths", paths); + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "all-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateOneFilePerResource(Filter filter) throws Exception { + List classNames = getClassNames(); + for (String resourceClassName : classNames) { + Class resourceModelClass = Class.forName(RESOURCEPACKAGENAME + "." + resourceClassName); + if (DomainResource.class.isAssignableFrom(resourceModelClass) + && DomainResource.class != resourceModelClass + && filter.acceptResourceType(resourceModelClass)) { + + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", resourceClassName + " API"); + info.add("description", "A simplified version of the HL7 FHIR API for " + resourceClassName + " resources."); + info.add("version", "4.0.1"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + JsonObjectBuilder requestBodies = factory.createObjectBuilder(); + + // generate Resource and DomainResource definitions + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + + generatePaths(resourceModelClass, paths, filter); + JsonObjectBuilder tag = factory.createObjectBuilder(); + tag.add("name", resourceModelClass.getSimpleName()); + tags.add(tag); + generateRequestBody(resourceModelClass, requestBodies); + generateDefinition(resourceModelClass, definitions); + // for search response + generateDefinition(Bundle.class, definitions); + // for error response + generateDefinition(OperationOutcome.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Stream.of(resourceClassName, "DomainResource", "Resource", "Bundle", "OperationOutcome").collect(Collectors.toSet()); + generateDefinitionForInnerClasses(classFilter, definitions); + + // generate definition for all the applicable data types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, resourceModelClass)) { + generateDefinition(typeModelClass, definitions); + } + } + + swagger.add("tags", tags); + swagger.add("paths", paths); + + JsonObjectBuilder components = factory.createObjectBuilder(); + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateSearchParameters(parameters, filter); + generateWholeSystemHistoryParameters(parameters, filter); + + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + components.add("parameters", parametersObject); + } + components.add("requestBodies", requestBodies); + components.add("schemas", definitions); + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + resourceClassName + "-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + } + } + + private static void generateOneFilePerCompartment(Filter filter) throws Exception { + List classNames = getClassNames(); + for (String compartmentClassName : classNames) { + Class compartmentModelClass = Class.forName(RESOURCEPACKAGENAME + "." + compartmentClassName); + if (DomainResource.class.isAssignableFrom(compartmentModelClass) + && DomainResource.class != compartmentModelClass) { + + Set resourceClassNames = getCompartmentClassNames(compartmentClassName); + if (resourceClassNames == null || resourceClassNames.isEmpty()) { + continue; + } + + JsonArrayBuilder tags = factory.createArrayBuilder(); + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + JsonObjectBuilder requestBodies = factory.createObjectBuilder(); + boolean addedContent = false; + + // Only include resource types that are accepted by the filter + for (String resourceClassName : resourceClassNames) { + Class resourceModelClass = Class.forName(RESOURCEPACKAGENAME + "." + resourceClassName); + if (DomainResource.class.isAssignableFrom(resourceModelClass) + && DomainResource.class != resourceModelClass + && filter.acceptResourceType(resourceModelClass)) { + addedContent = true; + + generateCompartmentPaths(compartmentModelClass, resourceModelClass, paths, filter); + JsonObjectBuilder tag = factory.createObjectBuilder(); + tag.add("name", resourceModelClass.getSimpleName()); + tags.add(tag); + generateRequestBody(resourceModelClass, requestBodies); + generateDefinition(resourceModelClass, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Stream.of(resourceClassName, "DomainResource", "Resource", "Bundle", "OperationOutcome").collect(Collectors.toSet()); + generateDefinitionForInnerClasses(classFilter, definitions); + + // generate definition for all the applicable data types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, resourceModelClass)) { + generateDefinition(typeModelClass, definitions); + } + } + } + } + + if (!addedContent) { + continue; + } + + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", compartmentClassName + " compartment API"); + info.add("description", "A simplified version of the HL7 FHIR API for " + compartmentClassName + " compartment."); + info.add("version", "4.0.1"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + // generate Resource and DomainResource definitions + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + // for search response + generateDefinition(Bundle.class, definitions); + // for error response + generateDefinition(OperationOutcome.class, definitions); + + swagger.add("tags", tags); + swagger.add("paths", paths); + + JsonObjectBuilder components = factory.createObjectBuilder(); + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateSearchParameters(parameters, filter); + generateWholeSystemHistoryParameters(parameters, filter); + + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + components.add("parameters", parametersObject); + } + components.add("requestBodies", requestBodies); + components.add("schemas", definitions); + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + compartmentClassName + "-compartment-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + } + } + + private static void generateMetadataOpenApi() throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Capabilities"); + info.add("description", "The capabilities interaction retrieves the information about a server's capabilities - which portions of the HL7 FHIR specification it supports."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR capabilities interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateMetadataPathItem(path); + paths.add("/metadata", path); + swagger.add("paths", paths); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + generateDefinition(CapabilityStatement.class, definitions); + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton("CapabilityStatement"); + generateDefinitionForInnerClasses(classFilter, definitions); + + // generate definition for all the applicable defined Types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, CapabilityStatement.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + JsonObjectBuilder components = factory.createObjectBuilder(); + + components.add("schemas", definitions); + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "metadata-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateWholeSystemHistoryOpenApi() throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Whole System History"); + info.add("description", "The whole system history interaction can be used to obtain a list of changes (create, update, delete) to resources in the IBM FHIR Server."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR _history interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateWholeSystemHistoryPathItem(path, "Other", "Get the whole system history", false); + paths.add("/_history", path); + swagger.add("paths", paths); + + generateDefinition(Bundle.class, definitions); + generateDefinition(Resource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton("Bundle"); + generateDefinitionForInnerClasses(classFilter, definitions); + + // generate definition for all the applicable data types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, Bundle.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + JsonObjectBuilder components = factory.createObjectBuilder(); + + components.add("schemas", definitions); + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateWholeSystemHistoryParameters(parameters, true); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + components.add("parameters", parametersObject); + } + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "wholeSystemHistory-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateBatchTransactionOpenApi(Filter filter) throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Batch/Transaction"); + info.add("description", "The batch and transaction interactions submit a set of actions to perform on a server in a single HTTP request/response."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR batch/transaction interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateBatchPathItem(path); + paths.add("/", path); + swagger.add("paths", paths); + + generateDefinition(Bundle.class, definitions); + generateDefinition(Resource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton("Bundle"); + generateDefinitionForInnerClasses(classFilter, definitions); + + // generate definition for all the defined Types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + generateDefinition(typeModelClass, definitions); + } + + JsonObjectBuilder components = factory.createObjectBuilder(); + + components.add("schemas", definitions); + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "batch-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateExportOpenApi(Filter filter) throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("openapi", "3.0.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Bulk Data Export"); + info.add("description", "Export Data from the FHIR server."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + JsonArrayBuilder servers = factory.createArrayBuilder(); + JsonObjectBuilder server = factory.createObjectBuilder(); + server.add("url", CONTEXT_ROOT); + servers.add(server); + swagger.add("servers", servers); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + addExportOperationPaths(filter, paths); + + swagger.add("paths", paths); + + generateDefinition(Parameters.class, definitions); + generateDefinition(Resource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + Set classFilter = Collections.singleton("Parameters"); + generateDefinitionForInnerClasses(classFilter, definitions); + + for (Class modelClass : DATA_EXPORT_TYPES) { + if (filter.acceptOperation(modelClass, "export")) { + generateDefinitionForInnerClasses(Set.of(modelClass.getSimpleName()), definitions); + } + } + + // generate definition for all the applicable data types. + for (String typeClassName : getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (isApplicableForClass(typeModelClass, Parameters.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + generateDefinitionForInnerClasses(classFilter, definitions); + + JsonObjectBuilder components = factory.createObjectBuilder(); + + components.add("schemas", definitions); + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateExportParameters(parameters, filter.acceptOperation(Group.class, "export")); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + components.add("parameters", parametersObject); + } + + swagger.add("components", components); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "export-openapi.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void addExportOperationPaths(Filter filter, JsonObjectBuilder paths) { + JsonObjectBuilder path; + + // System-level export + if (filter.acceptOperation(Resource.class, "export")) { + path = factory.createObjectBuilder(); + generateExportPathItem(path, "System", "Export data from the FHIR server", false, "get", false); + generateExportPathItem(path, "System", "Export data from the FHIR server", false, "post", false); + paths.add("/$export", path); + } + + // Patient and Group export + for (Class modelClass : DATA_EXPORT_TYPES) { + String typeName = modelClass.getSimpleName(); + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "export")) { + generateExportPathItem(path, typeName, "Export " + typeName + " data from the FHIR server", true, "get", "Group".equals(typeName)); + generateExportPathItem(path, typeName, "Export " + typeName + " data from the FHIR server", true, "post", "Group".equals(typeName)); + } + JsonObject pathObject = path.build(); + if (!pathObject.isEmpty()) { + StringBuilder pathStringBuilder = new StringBuilder(); + pathStringBuilder.append("/"); + pathStringBuilder.append(typeName); + if (modelClass.getSimpleName().equals("Group")) + pathStringBuilder.append("/{id}"); + pathStringBuilder.append("/$export"); + + paths.add(pathStringBuilder.toString(), pathObject); + } + } + + // Bulkdata status (custom polling endpoint) + path = factory.createObjectBuilder(); + generateBulkDataStatusPathItem(path); + paths.add("/$bulkdata-status", path); + } + + private static Map, StructureDefinition> buildStructureDefinitionMap() { + Map, StructureDefinition> structureDefinitionMap = new HashMap, StructureDefinition>(); + try { + populateStructureDefinitionMap(structureDefinitionMap, "profiles-resources.json"); + populateStructureDefinitionMap(structureDefinitionMap, "profiles-types.json"); + } catch (Exception e) { + throw new Error(e); + } + return structureDefinitionMap; + } + + public static void populateStructureDefinitionMap(Map, StructureDefinition> structureDefinitionMap, + String structureDefinitionFile) throws Exception { + + Bundle bundle; + try (InputStream stream = FHIROpenApiGenerator.class.getClassLoader().getResourceAsStream(structureDefinitionFile)) { + bundle = FHIRParser.parser(Format.JSON).parse(stream); + } + for (Entry entry : bundle.getEntry()) { + if (entry.getResource() instanceof StructureDefinition) { + StructureDefinition structureDefinition = (StructureDefinition) entry.getResource(); + if (structureDefinition != null) { + String className = structureDefinition.getName().getValue(); + className = className.substring(0, 1).toUpperCase() + className.substring(1); + Class modelClass = null; + try { + modelClass = Class.forName(RESOURCEPACKAGENAME + "." + className); + } catch (ClassNotFoundException e1) { + try { + // special case for MetadataResource which has a structuredefinition but is not a legal resource type + if (!className.equals("MetadataResource")) { + modelClass = Class.forName(TYPEPACKAGENAME + "." + className); + } + } catch (ClassNotFoundException e2) { + modelClass = null; + System.err.println(" -- PopulateStructureDefinition failed: " + className); + } + } finally { + if (modelClass != null) { + structureDefinitionMap.put(modelClass, structureDefinition); + } + } + } + } + } + } + + private static void generateSearchParameters(JsonObjectBuilder parameters, Filter filter) throws Exception { + if (filter.acceptOperation("search")) { + for (Map.Entry entry : searchHelper.getSearchParameters(Resource.class.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = entry.getKey(); + parameter.add("name", name); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "query"); + parameter.add("required", false); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + parameter.add("schema", schema); + + parameters.add(name + "Param", parameter); + } + } + + } + + private static void generateWholeSystemHistoryParameters(JsonObjectBuilder parameters, Filter filter) throws Exception { + if (filter.acceptOperation("history")) { + generateWholeSystemHistoryParameters(parameters, false); + } + + } + + private static void generateWholeSystemHistoryParameters(JsonObjectBuilder parameters, boolean includeType) throws Exception { + // Add the _sort parameter (_sort=[-_lastUpdated, _lastUpdated, none]) + // curl -k -u ':' 'https://:/fhir-server/api/v4/_history?_since=2021-02-21T00:00:00Z&_sort=_lastUpdated' + + JsonObjectBuilder sort = factory.createObjectBuilder(); + sort.add("name", "_sort"); + sort.add("description", "Sort the returned data using one of three options: \r\n\r\n* -_lastUpdated: [Default] Decreasing time - most recent changes first\r\n* _lastUpdated: Increasing time - oldest changes first\r\n* none: Increasing id order - oldest changes first\r\n"); + sort.add("in", "query"); + sort.add("required", false); + + JsonArrayBuilder enums = factory.createArrayBuilder(); + enums.add("-_lastUpdated"); + enums.add("_lastUpdated"); + enums.add("none"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + schema.add("enum", enums); + sort.add("schema", schema); + + parameters.add("_sortParam", sort); + + // add _since= + JsonObjectBuilder since = factory.createObjectBuilder(); + since.add("name", "_since"); + since.add("description", "Return changes since a known timestamp"); + since.add("in", "query"); + since.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + since.add("schema", schema); + + parameters.add("_sinceParam", since); + + // add _before= + JsonObjectBuilder before = factory.createObjectBuilder(); + before.add("name", "_before"); + before.add("description", "Return changes before a known timestamp"); + before.add("in", "query"); + before.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + before.add("schema", schema); + + parameters.add("_beforeParam", before); + + // add _count + JsonObjectBuilder count = factory.createObjectBuilder(); + count.add("name", "_count"); + count.add("description", "Specifies a maximum number of results that are required"); + count.add("in", "query"); + count.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "integer"); + schema.add("default", 100); + count.add("schema", schema); + + parameters.add("_countParam", count); + + // add _type + if (includeType) { + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + type.add("schema", schema); + + parameters.add("_typeParam", type); + } + } + + private static void generateExportParameters(JsonObjectBuilder parameters, boolean includePathIdParam) throws Exception { + JsonObjectBuilder outputFormat = factory.createObjectBuilder(); + outputFormat.add("name", "_outputFormat"); + outputFormat.add("description", "The format for the requested bulk data files to be generated"); + outputFormat.add("in", "query"); + outputFormat.add("required", false); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + outputFormat.add("schema", schema); + + parameters.add("_outputFormatParam", outputFormat); + + // add _since= + JsonObjectBuilder since = factory.createObjectBuilder(); + since.add("name", "_since"); + since.add("description", "Resources updated after this period will be included in the response."); + since.add("in", "query"); + since.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + since.add("schema", schema); + + parameters.add("_sinceParam", since); + + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + type.add("schema", schema); + + parameters.add("_typeParam", type); + + if (includePathIdParam) { + JsonObjectBuilder id = factory.createObjectBuilder(); + id.add("name", "id"); + id.add("description", "logical identifier"); + id.add("in", "path"); + id.add("required", true); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + id.add("schema", schema); + parameters.add("idParam", id); + } + } + + + private static void generatePaths(Class modelClass, JsonObjectBuilder paths, Filter filter) throws Exception { + JsonObjectBuilder path = factory.createObjectBuilder(); + // FHIR create operation + if (filter.acceptOperation(modelClass, "create")) { + generateCreatePathItem(modelClass, path); + } + // FHIR search operation + if (filter.acceptOperation(modelClass, "search")) { + generateSearchPathItem(null, modelClass, path); + } + JsonObject pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName(), pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR search (via POST) operation + if (filter.acceptOperation(modelClass, "search")) { + generateSearchViaPostPathItem(null, modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/_search", pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR vread operation + if (filter.acceptOperation(modelClass, "vread")) { + generateVreadPathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}/_history/{vid}", pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR read operation + if (filter.acceptOperation(modelClass, "read")) { + generateReadPathItem(modelClass, path); + } + // FHIR update operation + if (filter.acceptOperation(modelClass, "update")) { + generateUpdatePathItem(modelClass, path); + } + // FHIR delete operation + if (filter.acceptOperation(modelClass, "delete") && includeDeleteOperation) { + generateDeletePathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}", pathObject); + } + + // FHIR history operation + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "history")) { + generateHistoryPathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}/_history", pathObject); + } + + // Add Whole system History for type + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "history")) { + generateWholeSystemHistoryPathItem(path, modelClass.getSimpleName(), "Get the whole system history for " + modelClass.getSimpleName() + " resources", true); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/_history", pathObject); + } + + // Add Patient Everything + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "everything") && "Patient".equals(modelClass.getSimpleName())) { + generatePatientEverythingPathItem(path, true); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/Patient/{id}/$everything", pathObject); + } + + // Skip this flavor of patient-everything for now because its not really supported. + // See https://github.com/IBM/FHIR/issues/2402 for the details. + /* + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "everything") && "Patient".equals(modelClass.getSimpleName())) { + generatePatientEverythingPathItem(path, false); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/Patient/$everything", pathObject); + } + */ + + // TODO: add patch + } + + private static void generateCompartmentPaths(Class compartmentModelClass, Class resourceModelClass, JsonObjectBuilder paths, Filter filter) throws Exception { + JsonObjectBuilder path = factory.createObjectBuilder(); + + // FHIR search operation + if (filter.acceptOperation(resourceModelClass, "search")) { + generateSearchPathItem(compartmentModelClass, resourceModelClass, path); + } + JsonObject pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + compartmentModelClass.getSimpleName() + "/{id}/" + resourceModelClass.getSimpleName(), pathObject); + } + + path = factory.createObjectBuilder(); + + // FHIR search (via POST) operation + if (filter.acceptOperation(resourceModelClass, "search")) { + generateSearchViaPostPathItem(compartmentModelClass, resourceModelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + compartmentModelClass.getSimpleName() + "/{id}/" + resourceModelClass.getSimpleName() + "/_search", pathObject); + } + } + + private static void generateCreatePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + post.add("tags", tags); + post.add("summary", "Create" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + post.add("operationId", "create" + modelClass.getSimpleName()); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Create " + modelClass.getSimpleName() + " operation successful"); + responses.add("201", response); + post.add("responses", responses); + + /** + * "requestBody": { "$ref": "#/components/requestBodies/Account" } + */ + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + requestBody.add("$ref", "#/components/requestBodies/" + modelClass.getSimpleName()); + post.add("requestBody", requestBody); + + path.add("post", post); + } + + private static void generateReadPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", "Read" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "read" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Read " + modelClass.getSimpleName() + " operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/" + modelClass.getSimpleName()); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateVreadPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", + "Read specific version of" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "vread" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + + JsonObjectBuilder vidParamRef = factory.createObjectBuilder(); + addVidParamRef(vidParamRef); + parameters.add(vidParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Versioned read " + modelClass.getSimpleName() + " operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/" + modelClass.getSimpleName()); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void addIdPathParam(JsonObjectBuilder idParamRef) { + idParamRef.add("name", "id"); + idParamRef.add("in", "path"); + idParamRef.add("required", true); + idParamRef.add("description", "logical identifier"); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + idParamRef.add("schema", schema); + } + + private static void addVidParamRef(JsonObjectBuilder vidParamRef) { + vidParamRef.add("name", "vid"); + vidParamRef.add("in", "path"); + vidParamRef.add("required", true); + vidParamRef.add("description", "version identifier"); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + vidParamRef.add("schema", schema); + } + + private static void generateUpdatePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder put = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + put.add("tags", tags); + put.add("summary", "Update an existing " + modelClass.getSimpleName() + " resource"); + put.add("operationId", "update" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + + put.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response1 = factory.createObjectBuilder(); + response1.add("description", "Update " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder response2 = factory.createObjectBuilder(); + response2.add("description", "Create " + modelClass.getSimpleName() + + " operation successful (requires 'updateCreateEnabled' configuration option)"); + + responses.add("200", response1); + responses.add("201", response2); + + put.add("responses", responses); + + /** + * "requestBody": { "$ref": "#/components/requestBodies/Account" } + */ + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + requestBody.add("$ref", "#/components/requestBodies/" + modelClass.getSimpleName()); + put.add("requestBody", requestBody); + + path.add("put", put); + } + + private static void generateDeletePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder delete = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + delete.add("tags", tags); + delete.add("summary", "Delete" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + delete.add("operationId", "delete" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + + delete.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Delete " + modelClass.getSimpleName() + " operation successful"); + responses.add("204", response); + delete.add("responses", responses); + + path.add("delete", delete); + } + + private static void generateSearchPathItem(Class compartmentModelClass, Class modelClass, JsonObjectBuilder path) throws Exception { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + get.add("tags", tags); + + if (compartmentModelClass != null) { + get.add("summary", "Search for " + modelClass.getSimpleName() + " resources within " + compartmentModelClass.getSimpleName() + " compartment"); + get.add("operationId", "search" + compartmentModelClass.getSimpleName() + "Compartment"+ modelClass.getSimpleName()); + } else { + get.add("summary", "Search for " + modelClass.getSimpleName() + " resources"); + get.add("operationId", "search" + modelClass.getSimpleName()); + } + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // For compartment search, include parameter for {id} + if (compartmentModelClass != null) { + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + } + generateSearchParameters(modelClass, parameters); + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Search " + modelClass.getSimpleName() + " operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateSearchParameters(Class modelClass, JsonArrayBuilder parameters) throws Exception { + for (Map.Entry entry : searchHelper.getSearchParameters(modelClass.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = entry.getKey(); + if (name.startsWith("_")) { + parameter.add("$ref", "#/components/parameters/" + name + "Param"); + } else { + parameter.add("name", name); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "query"); + parameter.add("required", false); + + /** + * "schema": { "type": "string" } + */ + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + parameter.add("schema", schema); + } + parameters.add(parameter); + } + } + + private static void generateSearchViaPostPathItem(Class compartmentModelClass, Class modelClass, JsonObjectBuilder path) throws Exception { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + post.add("tags", tags); + + if (compartmentModelClass != null) { + post.add("summary", "Search for " + modelClass.getSimpleName() + " resources within " + compartmentModelClass.getSimpleName() + " compartment"); + post.add("operationId", "searchViaPost" + compartmentModelClass.getSimpleName() + "Compartment"+ modelClass.getSimpleName()); + } else { + post.add("summary", "Search for " + modelClass.getSimpleName() + " resources"); + post.add("operationId", "searchViaPost" + modelClass.getSimpleName()); + } + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // For compartment search, include parameter for {id} + if (compartmentModelClass != null) { + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + } + generateSearchParameters(modelClass, parameters); + post.add("parameters", parameters); + + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + + /** + * "content": { "application/x-www-form-urlencoded": { "schema": { "type": "object" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + JsonObjectBuilder formParameters = factory.createObjectBuilder(); + schema.add("type", "object"); + generateSearchFormParameters(modelClass, formParameters); + schema.add("properties", formParameters); + contentType.add("schema", schema); + content.add(APPLICATION_FORM, contentType); + + requestBody.add("content", content); + post.add("requestBody", requestBody); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Search " + modelClass.getSimpleName() + " operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + content = factory.createObjectBuilder(); + contentType = factory.createObjectBuilder(); + schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + post.add("responses", responses); + + path.add("post", post); + } + + private static void generateSearchFormParameters(Class modelClass, JsonObjectBuilder parameters) throws Exception { + // + // As of OpenAPI 3.0.0, empty form fields are still passed to the REST API, which is not valid + // for the FHIR search API. + // + // For example: curl -X POST "https://localhost:9443/fhir-server/api/v4/Appointment/_search" + // -H "accept: application/fhir+json" -H "Content-Type: application/x-www-form-urlencoded" + // -d "reason-reference=&patient=&_lastUpdated=&supporting-info=&appointment-type=&identifier= + // &_profile=&part-status=&date=&service-type=&status=&actor=&location=&service-category= + // &reason-code=&based-on=&_source=&_id=&practitioner=&_tag=&specialty=&slot=&_security=" + // + // So for now, do not generate form parameters for OpenAPI 3.0. + // Swagger does not pass empty form fields, so form parameters are generated for Swagger. + // +// List searchParameters = new ArrayList( +// SearchUtil.getApplicableSearchParameters(modelClass.getSimpleName())); +// for (SearchParameter searchParameter : searchParameters) { +// String name = searchParameter.getName().getValue(); +// +// JsonObjectBuilder propertyValues = factory.createObjectBuilder(); +// /** +// * "": { "type": "string" } +// */ +// propertyValues.add("type", "string"); +// propertyValues.add("description", searchParameter.getDescription().getValue()); +// +// parameters.add(name, propertyValues); +// } + } + + private static void generateHistoryPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", "Return the history of" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "history" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + addIdPathParam(idParamRef); + parameters.add(idParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "History " + modelClass.getSimpleName() + " operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateMetadataPathItem(JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Other"); + + get.add("tags", tags); + get.add("summary", "Get the FHIR Capability statement for this endpoint"); + get.add("operationId", "metadata"); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "metadata operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/CapabilityStatement"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + /** + * This method is used by both the resource specific whole system history path generation and the general whole system history path generation. + * If it is resource specific, the type parameter will not be generated and the resource type will be included in the operationId. + * @param path + * @param tag + * @param summary + * @param resourceSpecific + */ + private static void generateWholeSystemHistoryPathItem(JsonObjectBuilder path, String tag, String summary, boolean resourceSpecific) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(tag); + + get.add("tags", tags); + get.add("summary", summary); + + String operationId = "wholeSystemHistory"; + if (resourceSpecific) { + operationId = operationId + tag; + } + get.add("operationId", operationId); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // Add parameters to api call + JsonObjectBuilder sortParameter = factory.createObjectBuilder(); + sortParameter.add("$ref", "#/components/parameters/_sortParam"); + parameters.add(sortParameter); + + JsonObjectBuilder sinceParameter = factory.createObjectBuilder(); + sinceParameter.add("$ref", "#/components/parameters/_sinceParam"); + parameters.add(sinceParameter); + + JsonObjectBuilder beforeParameter = factory.createObjectBuilder(); + beforeParameter.add("$ref", "#/components/parameters/_beforeParam"); + parameters.add(beforeParameter); + + JsonObjectBuilder countParameter = factory.createObjectBuilder(); + countParameter.add("$ref", "#/components/parameters/_countParam"); + parameters.add(countParameter); + + // Add the type parameter if non resource specific + if (!resourceSpecific) { + JsonObjectBuilder typeParameter = factory.createObjectBuilder(); + typeParameter.add("$ref", "#/components/parameters/_typeParam"); + parameters.add(typeParameter); + } + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "whole system history operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateBatchPathItem(JsonObjectBuilder path) { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Other"); + + post.add("tags", tags); + post.add("summary", "Perform a batch operation"); + post.add("operationId", "batch"); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "batch operation successful"); + + /** + * "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } } + */ + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + responses.add("200", response); + post.add("responses", responses); + + /** + * "requestBody": { "content": { "application/fhir+json": { "schema": { "$ref": + * "#/components/schemas/Bundle" } } }, "required": true } + */ + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + content = factory.createObjectBuilder(); + contentType = factory.createObjectBuilder(); + schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + requestBody.add("content", content.build()); + requestBody.add("required", true); + post.add("requestBody", requestBody); + + path.add("post", post); + } + + /** + * Common method used for both the general export function and the Patient or Group specific function. + * Expected tag parameter is set to "other" for non resource specific generation. Tags "Patient" or "Group" for resource specific generation. + * HttpMethod parameter is "get" or "post". + * + * @param path + * @param type + * @param summary + * @param resourceSpecific + * @param httpMethod + */ + private static void generateExportPathItem(JsonObjectBuilder path, String type, String summary, boolean resourceSpecific, String httpMethod, boolean addIdParam) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Bulk Data"); + + httpMethodBuilder.add("tags", tags); + httpMethodBuilder.add("summary", summary); + + String operationId = httpMethod + "Export"; + if (resourceSpecific) { + operationId = operationId + type; + } + httpMethodBuilder.add("operationId", operationId); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + if (addIdParam) { + JsonObjectBuilder idParameter = factory.createObjectBuilder(); + addIdPathParam(idParameter); + parameters.add(idParameter); + } + + if ("post".equalsIgnoreCase(httpMethod)) { + if (addIdParam) + httpMethodBuilder.add("parameters", parameters); + + // Generate POST request body + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Parameters"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + requestBody.add("content", content); + requestBody.add("required", true); + httpMethodBuilder.add("requestBody", requestBody); + + } else { + // Generate GET parameters + JsonObjectBuilder outputFormatParameter = factory.createObjectBuilder(); + outputFormatParameter.add("$ref", "#/components/parameters/_outputFormatParam"); + parameters.add(outputFormatParameter); + + JsonObjectBuilder sinceParameter = factory.createObjectBuilder(); + sinceParameter.add("$ref", "#/components/parameters/_sinceParam"); + parameters.add(sinceParameter); + + JsonObjectBuilder typeParameter = factory.createObjectBuilder(); + typeParameter.add("$ref", "#/components/parameters/_typeParam"); + parameters.add(typeParameter); + + httpMethodBuilder.add("parameters", parameters); + + } + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Export job is initiated"); + + // Add Response headers + JsonObjectBuilder headers = factory.createObjectBuilder(); + JsonObjectBuilder contentLocation = factory.createObjectBuilder(); + contentLocation.add("description", "The polling location"); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + contentLocation.add("schema", schema); + + headers.add("Content-Location", contentLocation); + response.add("headers", headers); + + responses.add("202", response); + httpMethodBuilder.add("responses", responses); + + path.add(httpMethod, httpMethodBuilder); + } + + /** + * Generate a path item for the {@code $bulkdata-status} endpoint + * @param path + */ + private static void generateBulkDataStatusPathItem(JsonObjectBuilder path) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Bulk Data"); + + httpMethodBuilder.add("tags", tags); + httpMethodBuilder.add("summary", "Retrieves the status of the buld data request"); + httpMethodBuilder.add("operationId", "bulkDataStatus"); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + // Generate GET parameters + JsonObjectBuilder jobParameter = factory.createObjectBuilder(); + jobParameter.add("name", "job"); + jobParameter.add("description", "Job id from the Content-Location header response value after the export request"); + jobParameter.add("in", "query"); + jobParameter.add("required", true); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + jobParameter.add("schema", schema); + parameters.add(jobParameter); + + httpMethodBuilder.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + // 202 not completed + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Export job is queued and not yet complete"); + responses.add("202", response); + + // Add 200 completed + response = factory.createObjectBuilder(); + response.add("description", "Export job completed"); + + // add response body + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + schema = factory.createObjectBuilder(); + schema.add("type", "object"); + JsonObjectBuilder properties = factory.createObjectBuilder(); + JsonObjectBuilder transactionTime = factory.createObjectBuilder(); + transactionTime.add("type", "string"); + properties.add("transactionTime", transactionTime); + + JsonObjectBuilder request = factory.createObjectBuilder(); + request.add("type", "string"); + properties.add("request", request); + + JsonObjectBuilder requiresAccessToken = factory.createObjectBuilder(); + requiresAccessToken.add("type", "boolean"); + properties.add("requiresAccessToken", requiresAccessToken); + + JsonObjectBuilder output = factory.createObjectBuilder(); + output.add("type", "array"); + JsonObjectBuilder items = factory.createObjectBuilder(); + items.add("type", "object"); + JsonObjectBuilder itemProperties = factory.createObjectBuilder(); + + JsonObjectBuilder outputArrayEntryTypeProperty = factory.createObjectBuilder(); + outputArrayEntryTypeProperty.add("type", "string"); + itemProperties.add("type", outputArrayEntryTypeProperty); + + JsonObjectBuilder outputArrayEntryUrlProperty = factory.createObjectBuilder(); + outputArrayEntryUrlProperty.add("type", "string"); + itemProperties.add("url", outputArrayEntryUrlProperty); + + JsonObjectBuilder outputArrayEntryCountProperty = factory.createObjectBuilder(); + outputArrayEntryCountProperty.add("type", "integer"); + itemProperties.add("count", outputArrayEntryCountProperty); + + items.add("properties", itemProperties); + output.add("items", items); + properties.add("output", output); + + schema.add("properties", properties); + contentType.add("schema", schema); + content.add("application/json", contentType); + + response.add("content", content); + responses.add("200", response); + httpMethodBuilder.add("responses", responses); + + path.add("get", httpMethodBuilder); + } + + /** + * Generates the $everything path for a Patient resource + * + * @param path + * @param addIdParam + */ + private static void generatePatientEverythingPathItem(JsonObjectBuilder path, boolean addIdParam) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Patient"); + httpMethodBuilder.add("tags", tags); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + if (addIdParam) { + httpMethodBuilder.add("summary", "Return all the information related to a given patient"); + httpMethodBuilder.add("operationId", "everythingPatient"); + + JsonObjectBuilder idParameter = factory.createObjectBuilder(); + addIdPathParam(idParameter); + parameters.add(idParameter); + } else { + httpMethodBuilder.add("summary", "Return all the information related to one or more patients"); + httpMethodBuilder.add("operationId", "everythingAllPatients"); + } + + JsonObjectBuilder startParam = factory.createObjectBuilder(); + startParam.add("name", "start"); + startParam.add("description", "All records relating to care provided in a certain date range"); + startParam.add("in", "query"); + startParam.add("required", false); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "string"); + startParam.add("schema", schema); + parameters.add(startParam); + + JsonObjectBuilder endParam = factory.createObjectBuilder(); + endParam.add("name", "end"); + endParam.add("description", "All records relating to care provided in a certain date range"); + endParam.add("in", "query"); + endParam.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + endParam.add("schema", schema); + parameters.add(endParam); + + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + schema = factory.createObjectBuilder(); + schema.add("type", "string"); + type.add("schema", schema); + + parameters.add(type); + + httpMethodBuilder.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + JsonObjectBuilder response = factory.createObjectBuilder(); + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/Bundle"); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + response.add("content", content); + response.add("description", "everything operation successful"); + responses.add("200", response); + httpMethodBuilder.add("responses", responses); + + path.add("get", httpMethodBuilder); + } + + + private static void generateRequestBody(Class modelClass, JsonObjectBuilder requestBodies) { + if (ModelSupport.isResourceType(modelClass.getSimpleName())) { +// "Coverage": { +// "content": { +// "application/fhir+json": { +// "schema": { +// "$ref": "#/components/schemas/Coverage" +// } +// } +// }, +// "required": true +// }, + JsonObjectBuilder requestBody = factory.createObjectBuilder(); + + JsonObjectBuilder content = factory.createObjectBuilder(); + JsonObjectBuilder contentType = factory.createObjectBuilder(); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/components/schemas/" + modelClass.getSimpleName()); + contentType.add("schema", schema); + content.add(FHIRMediaType.APPLICATION_FHIR_JSON, contentType); + + requestBody.add("content", content); + requestBody.add("required", true); + requestBodies.add(modelClass.getSimpleName(), requestBody); + } + } + + private static void generateDefinition(Class modelClass, JsonObjectBuilder definitions) throws Exception { + if (!ModelSupport.isPrimitiveType(modelClass)) { + JsonObjectBuilder definition = factory.createObjectBuilder(); + JsonObjectBuilder properties = factory.createObjectBuilder(); + JsonArrayBuilder required = factory.createArrayBuilder(); + + StructureDefinition structureDefinition = getStructureDefinition(modelClass); + + if (structureDefinition == null) { + System.err.println("Failed generateDefinition for: " + modelClass.getName()); + return; + } + + if (Resource.class.isAssignableFrom(modelClass)) { + // if the modelClass is a resource, then add the 'resourceType' property + JsonObjectBuilder property = factory.createObjectBuilder(); + + // Convert all the primitive types to json types. + property.add("type", "string"); + if (Resource.class == modelClass) { + // TODO: when a filter was passed, limit this to just the resource types included in the filter + List typeNames = getClassNames(); + JsonArrayBuilder enumValues = factory.createArrayBuilder(typeNames); + property.add("enum", enumValues); + properties.add("resourceType", property.build()); + required.add("resourceType"); + } else { + // TODO how to "overwrite" the Resource definition and say that the value is fixed? + // https://github.com/OAI/OpenAPI-Specification/issues/1313 +// property.add("enum", modelClass.getSimpleName()); + } + } + + for (Field field : modelClass.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && !Modifier.isVolatile(field.getModifiers())) { + if (!ModelSupport.isChoiceElement(modelClass, ModelSupport.getElementName(field)) && + field.isAnnotationPresent(Required.class)) { + required.add(ModelSupport.getElementName(field)); + } + generateProperties(structureDefinition, modelClass, field, properties); + } + } + + JsonArray requiredArray = required.build(); + + Class superClass = modelClass.getSuperclass(); + if (superClass != null && superClass.getPackage().getName().startsWith("org.linuxforhealth.fhir.model") + && !superClass.equals(AbstractVisitable.class)) { + JsonArrayBuilder allOf = factory.createArrayBuilder(); + + JsonObjectBuilder ref = factory.createObjectBuilder(); + ref.add("$ref", "#/components/schemas/" + superClass.getSimpleName()); + allOf.add(ref); + + JsonObjectBuilder wrapper = factory.createObjectBuilder(); + wrapper.add("type", "object"); + wrapper.add("properties", properties); + if (!requiredArray.isEmpty()) { + wrapper.add("required", requiredArray); + } + allOf.add(wrapper); + + definition.add("allOf", allOf); + } else { + definition.add("type", "object"); + if (Resource.class.equals(modelClass)) { + JsonObject discriminator = factory.createObjectBuilder().add("propertyName", "resourceType").build(); + definition.add("discriminator", discriminator); + } + definition.add("properties", properties); + if (!requiredArray.isEmpty()) { + definition.add("required", requiredArray); + } + } + + if (Resource.class.isAssignableFrom(modelClass)) { + addExamples(modelClass, definition); + } + + definitions.add(getSimpleNameWithEnclosingNames(modelClass), definition); + } + } + + public static void addExamples(Class modelClass, JsonObjectBuilder definition) throws IOException { + if (!Modifier.isAbstract(modelClass.getModifiers())) { + // Change this from "complete-mock" to "minimal" to reduce the size of the generated definition + Reader example = ExamplesUtil.resourceReader("json/complete-mock/" + modelClass.getSimpleName() + "-1.json"); + JsonReader jsonReader = Json.createReader(example); + definition.add("example", jsonReader.readObject()); + } + } + + private static String getSimpleNameWithEnclosingNames(Class modelClass) { + StringBuilder fullName = new StringBuilder(modelClass.getSimpleName()); + while (modelClass.isMemberClass()) { + modelClass = modelClass.getEnclosingClass(); + fullName.insert(0, modelClass.getSimpleName() + "_"); + } + return fullName.toString(); + } + + private static StructureDefinition getStructureDefinition(Class modelClass) { + StructureDefinition structureDefinition = null; + // Use Code definition for all its sub-classes for now + if (Code.class.isAssignableFrom(modelClass)) { + try { + structureDefinition = structureDefinitionMap.get(Class.forName(TYPEPACKAGENAME + "." + "Code")); + } catch (ClassNotFoundException e) { + structureDefinition = null; + } + } else { + structureDefinition = structureDefinitionMap.get(modelClass); + structureDefinition = (structureDefinition != null) + ? structureDefinition : getEnclosingStructureDefinition(modelClass); + } + + return structureDefinition; + } + + private static StructureDefinition getEnclosingStructureDefinition(Class modelClass) { + StructureDefinition structureDefinition = null; + int nameLength = 0; + for (Class key : structureDefinitionMap.keySet()) { + String modelClassName; + if (modelClass.getName().contains(TYPEPACKAGENAME)) { + modelClassName = modelClass.getName().substring(TYPEPACKAGENAME.length()+1); + } else { + modelClassName = modelClass.getName().substring(RESOURCEPACKAGENAME.length()+1); + } + + if (modelClassName.startsWith(key.getSimpleName()) + && key.getSimpleName().length() > nameLength) { + structureDefinition = structureDefinitionMap.get(key); + nameLength = key.getSimpleName().length(); + } + } + return structureDefinition; + } + + private static void generateProperties(StructureDefinition structureDefinition, Class modelClass, Field field, + JsonObjectBuilder properties) throws Exception { + + boolean many = false; + + Type fieldType = field.getType(); + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + fieldType = parameterizedType.getActualTypeArguments()[0]; + many = true; + } + + String elementName = ModelSupport.getElementName(field); + + if (ModelSupport.isChoiceElement(modelClass, elementName)) { + Set> choiceElementTypes = ModelSupport.getChoiceElementTypes(modelClass, elementName); + ElementDefinition elementDefinition = getElementDefinition(structureDefinition, modelClass, elementName + "[x]"); + String description = elementDefinition.getDefinition().getValue(); + Integer min = elementDefinition.getMin() != null ? elementDefinition.getMin().getValue() : null; + for (Class choiceType : choiceElementTypes) { + if (isApplicableForClass(choiceType, modelClass)) { + String choiceElementName = ModelSupport.getChoiceElementName(elementName, choiceType); + generateProperty(structureDefinition, modelClass, field, properties, choiceElementName, choiceType, many, description, min); + } + } + } else { + ElementDefinition elementDefinition = getElementDefinition(structureDefinition, modelClass, elementName); + String description = elementDefinition.getDefinition().getValue(); + Integer min = elementDefinition.getMin() != null ? elementDefinition.getMin().getValue() : null; + generateProperty(structureDefinition, modelClass, field, properties, elementName, (Class)fieldType, many, description, min); + } + } + + private static void generateProperty(StructureDefinition structureDefinition, Class modelClass, Field field, + JsonObjectBuilder properties, String elementName, Class fieldClass, boolean many, String description, Integer min) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + JsonObjectBuilder property = factory.createObjectBuilder(); + + // Convert all the primitive types to json types. + if (isEnumerationWrapperClass(fieldClass)) { + property.add("type", "string"); + JsonArrayBuilder constants = factory.createArrayBuilder(); + Class enumClass = Class.forName(fieldClass.getName() + "$ValueSet"); + for (Object constant : enumClass.getEnumConstants()) { + Method method = constant.getClass().getMethod("value"); + String value = (String) method.invoke(constant); + constants.add(value); + } + property.add("enum", constants); + } else if (org.linuxforhealth.fhir.model.type.String.class.isAssignableFrom(fieldClass)) { + property.add("type", "string"); + if (org.linuxforhealth.fhir.model.type.Code.class.isAssignableFrom(fieldClass)) { + property.add("pattern", "[^\\s]+(\\s[^\\s]+)*"); + } else if (org.linuxforhealth.fhir.model.type.Id.class.equals(fieldClass)) { + property.add("pattern", "[A-Za-z0-9\\-\\.]{1,64}"); + } else { + property.add("pattern", "[ \\r\\n\\t\\S]+"); + } + } else if (org.linuxforhealth.fhir.model.type.Uri.class.isAssignableFrom(fieldClass)) { + property.add("type", "string"); + if (Oid.class.equals(fieldClass)) { + property.add("pattern", "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+"); + } else if (Uuid.class.equals(fieldClass)) { + property.add("pattern", "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } else { + property.add("pattern", "\\S*"); + } + } else if (org.linuxforhealth.fhir.model.type.Boolean.class.equals(fieldClass)) { + property.add("type", "boolean"); + } else if (org.linuxforhealth.fhir.model.type.Instant.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern", "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]" + + "|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]" + + "|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"); + } else if (org.linuxforhealth.fhir.model.type.Time.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?"); + } else if (org.linuxforhealth.fhir.model.type.Date.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)" + + "|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?"); + } else if (org.linuxforhealth.fhir.model.type.DateTime.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]" + + "|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]" + + "|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?"); + } else if (org.linuxforhealth.fhir.model.type.Decimal.class.equals(fieldClass)) { + property.add("type", "number"); + } else if (org.linuxforhealth.fhir.model.type.Integer.class.isAssignableFrom(fieldClass)) { + property.add("type", "integer"); + property.add("format", "int32"); + } else if (org.linuxforhealth.fhir.model.type.Base64Binary.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+"); + } else if (String.class.equals(fieldClass)) { + property.add("type", "string"); + if ("id".equals(elementName)) { + property.add("pattern", "[A-Za-z0-9\\-\\.]{1,64}"); + } else if ("url".equals(elementName)) { + property.add("pattern", "\\S*"); + } + } else if (org.linuxforhealth.fhir.model.type.Xhtml.class.equals(fieldClass)) { + property.add("type", "string"); + } else { + // For non-primitives, point to the corresponding definition + property.add("$ref", "#/components/schemas/" + getSimpleNameWithEnclosingNames(fieldClass)); + } + + if (description != null) { + property.add("description", cleanse(description)); + } + + // Include select examples to help tools avoid bumping into infinite recursion (if they try generate examples) + JsonStructure example = null; + if (Element.class.equals(modelClass) && Extension.class.equals(fieldClass)) { + // "example": [{"url":"http://example.com","valueString":"textValue"}] + JsonObject obj = factory.createObjectBuilder() + .add("url", "http://example.com") + .add("valueString", "text value") + .build(); + example = factory.createArrayBuilder().add(obj).build(); + } else if (Identifier.class.equals(modelClass) && Reference.class.equals(fieldClass)) { + // "example": {"reference":"Organization/123","type":"Organization","display":"The Assigning Organization"} + example = factory.createObjectBuilder() + .add("reference", "Organization/123") + .add("type", "Organization") + .add("display", "The Assigning Organization") + // skip assigner to break the recursion + .build(); + } + + if (many) { + JsonObjectBuilder wrapper = factory.createObjectBuilder(); + wrapper.add("type", "array"); + wrapper.add("items", property); + if (min != null && min > 0) { + wrapper.add("minItems", min.intValue()); + } + if (example != null) { + wrapper.add("example", example); + } + properties.add(elementName, wrapper); + } else { + if (example != null) { + property.add("example", example); + } + properties.add(elementName, property); + } + } + + /* + * clean up description to make valid unicode encoded output. + */ + private static String cleanse(String description) { + String cleansed = description.replaceAll("“", "“") + .replaceAll("”", "”") + .replaceAll("…", "…") + .replaceAll("’", "’") + .replaceAll("‘", "‘") + .replaceAll("-", "–") + .replaceAll("—", "—") + .replaceAll("‑", "‑"); // Non-Breaking Hyphen + + if (!Charset.forName("US-ASCII").newEncoder().canEncode(cleansed)) { + // If a new character sneaks in - it'll output here. + // Best place to look up entity is https://unicodelookup.com + System.out.println("[Failure to cleanse the description] - " + cleansed); + } + + return cleansed; + } + + /** + * Returns the ElementDefinition for the given elementName in the Type represented by modelClass + */ + private static ElementDefinition getElementDefinition(StructureDefinition structureDefinition, Class modelClass, String elementName) { + String structureDefinitionName = structureDefinition.getName().getValue(); + String path = structureDefinitionName; + + String pathEnding = elementName; + if (BackboneElement.class.isAssignableFrom(modelClass) && !BackboneElement.class.equals(modelClass) && modelClass.isMemberClass()) { + String modelClassName = modelClass.getSimpleName(); + modelClassName = modelClassName.substring(0, 1).toLowerCase() + modelClassName.substring(1); + + if (Character.isDigit(modelClassName.charAt(modelClassName.length() - 1))) { + modelClassName = modelClassName.substring(0, modelClassName.length() - 1); + } + + path += "." + modelClassName; + pathEnding = modelClassName + "." + elementName; + } + + path += "." + elementName; + + for (ElementDefinition elementDefinition : structureDefinition.getDifferential().getElement()) { + String elementDefinitionPath = elementDefinition.getPath().getValue(); + if (elementDefinitionPath.equals(path) || (elementDefinitionPath.startsWith(structureDefinitionName) + && elementDefinitionPath.endsWith(pathEnding))) { + return elementDefinition; + } + } + + throw new RuntimeException("Unable to retrieve element definition for " + elementName + " in " + modelClass.getName()); + } + + public static List getClassNames() { + if (classNamesList == null) { + classNamesList = ModelSupport.getResourceTypes().stream().map(r -> ModelSupport.getTypeName(r)).collect(Collectors.toList()); + classNamesList = Collections.unmodifiableList(classNamesList); + } + + return classNamesList; + } + + private static Set getCompartmentClassNames(String compartment) { + try { + return compartmentHelper.getCompartmentResourceTypes(compartment); + } catch (FHIRSearchException e) { + return Collections.emptySet(); + } + } + + private static boolean isEnumerationWrapperClass(Class type) { + try { + Class.forName(type.getName() + "$ValueSet"); + return true; + } catch (Exception e) { + // do nothing + } + return false; + } + + private static class Filter { + private Map> filterMap = null; + + public Filter(Map> filterMap) { + this.filterMap = filterMap; + } + + public boolean acceptResourceType(Class resourceType) { + return filterMap.containsKey(resourceType.getSimpleName()); + } + + /** + * @return true if the operation is enables for the specified resourceType, otherwise false + */ + public boolean acceptOperation(Class resourceType, String operation) { + List interactions = filterMap.get(resourceType.getSimpleName()); + return interactions != null && interactions.contains(operation); + } + + /** + * @return true if one or more of the resources in the filterMap includes the passed operation, otherwise false + */ + public boolean acceptOperation(String operation) { + for (String resourceType : filterMap.keySet()) { + List operationList = filterMap.get(resourceType); + if (operationList.contains(operation)) { + return true; + } + } + return false; + } + } + + private static Filter createFilter(String filterString) { + return new Filter(parseFilterString(filterString)); + } + + private static Map> parseFilterString(String filterString) { + Map> filterMap = new HashMap>(); + for (String component : filterString.split(";")) { + String resourceType = component.substring(0, component.indexOf("(")); + String operations = component.substring(component.indexOf("(") + 1, component.indexOf(")")); + List operationList = new ArrayList(); + for (String operation : operations.split(",")) { + operationList.add(operation); + } + filterMap.put(resourceType, operationList); + } + return filterMap; + } + + private static Filter createAcceptAllFilter() throws Exception { + return new Filter(buildAcceptAllFilterMap()); + } + + // build filter map for all domain resources and operations + private static Map> buildAcceptAllFilterMap() throws Exception { + Map> filterMap = new HashMap>(); + for (String className : getClassNames()) { + Class modelClass = Class.forName(RESOURCEPACKAGENAME + "." + className); + if (Resource.class == modelClass) { + filterMap.put(className, List.of("search", "history", "batch", "transaction", "export")); + } else if (DomainResource.class.isAssignableFrom(modelClass)) { + String resourceType = className; + // TODO: add patch + List operationList = Arrays.asList("create", "read", "vread", "update", "delete", "search", + "history", "export", "everything"); + filterMap.put(resourceType, operationList); + } + } + return filterMap; + } + + private static String getArticle(Class modelClass) { + List prefixes = Arrays.asList("A", "E", "I", "O", "Un"); + for (String prefix : prefixes) { + if (modelClass.getSimpleName().startsWith(prefix)) { + return " an "; + } + } + return " a "; + } + + private static void addInnerClassNames(Class classToIterate, List resultList) { + try { + Class[] InnerClassArray = classToIterate.getDeclaredClasses(); + + for (Class InnerClass : InnerClassArray) { + if (InnerClass.getSimpleName().equals("Builder") || InnerClass.getSimpleName().equals("ValueSet")) { + continue; + } + int classNameOffset; + if (InnerClass.getName().contains(RESOURCEPACKAGENAME)) { + classNameOffset = RESOURCEPACKAGENAME.length() +1; + } else { + classNameOffset = TYPEPACKAGENAME.length() +1; + } + + resultList.add(InnerClass.getName().substring(classNameOffset)); + + if (InnerClass.getDeclaredClasses() != null) { + addInnerClassNames(InnerClass, resultList); + } + } + + } catch (Exception e) { + System.err.println("Failed to Iterate class: " + classToIterate.getSimpleName()); + } + } + + public static List getAllTypesList() { + Set> allTypes = ModelSupport.getDataTypes(); + + List toAddTypesList = new ArrayList(); + + // Explicitly add the abstract supertypes + toAddTypesList.add("Element"); + toAddTypesList.add("BackboneElement"); + + for (Class resourceTypeClass : allTypes) { + String typeName = resourceTypeClass.getSimpleName(); + toAddTypesList.add(typeName); + addInnerClassNames(resourceTypeClass, toAddTypesList); + } + + return toAddTypesList; + } + + public static List getAllResourceInnerClasses() { + List allResourceInnerClassList = new ArrayList(); + + for (Class resourceTypeClass : ModelSupport.getResourceTypes()) { + try { + addInnerClassNames(resourceTypeClass, allResourceInnerClassList); + } catch (Exception e) { + System.err.println("Failed to get resource: " + resourceTypeClass.getSimpleName()); + } + } + return allResourceInnerClassList; + } + + /** + * @return true if the type model class is applicable for the outer model class + */ + public static boolean isApplicableForClass(Class typeModelClass, Class outerModelClass) { + // if the resource class has a choice element (other than Extension.value[x]) + // with a type of "*" (any), then include it + if (StructureMap.class == outerModelClass || + Task.class == outerModelClass) { + return true; + } + + boolean isApplicable = true; + + if (Dosage.class == typeModelClass || + Dosage.class == typeModelClass.getEnclosingClass()) { + // Special case for Dosage + if (outerModelClass != ActivityDefinition.class && + outerModelClass != MedicationDispense.class && + outerModelClass != MedicationKnowledge.class && + outerModelClass != MedicationRequest.class && + outerModelClass != MedicationStatement.class) { + isApplicable = false; + } + } else if (ElementDefinition.class == typeModelClass || + ElementDefinition.class.equals(typeModelClass.getEnclosingClass()) || + ElementDefinition.Slicing.Discriminator.class == typeModelClass) { + if (outerModelClass != StructureDefinition.class) { + isApplicable = false; + } + } else if (ContactDetail.class == typeModelClass) { + // TODO: introspect the resourceModelClass to tell if its truly applicable or not + isApplicable = true; + } else if (Contributor.class == typeModelClass) { + // Not used anywhere yet + isApplicable = false; + } else if (DataRequirement.class == typeModelClass || + DataRequirement.class == typeModelClass.getEnclosingClass()) { + if (outerModelClass != TriggerDefinition.class && + outerModelClass != EvidenceVariable.class && + outerModelClass != GuidanceResponse.class && + outerModelClass != Library.class && + outerModelClass != PlanDefinition.class && + outerModelClass != ResearchElementDefinition.class) { + isApplicable = false; + } + } else if (ParameterDefinition.class == typeModelClass || + ParameterDefinition.class == typeModelClass.getEnclosingClass()) { + if (outerModelClass != ActivityDefinition.class && + outerModelClass != MedicationDispense.class && + outerModelClass != MedicationKnowledge.class && + outerModelClass != MedicationRequest.class && + outerModelClass != MedicationStatement.class) { + isApplicable = false; + } + } else if (RelatedArtifact.class == typeModelClass) { + // TODO: introspect the resourceModelClass to tell if its truly applicable or not + isApplicable = true; + } else if (TriggerDefinition.class == typeModelClass || + TriggerDefinition.class == typeModelClass.getEnclosingClass()) { + if (outerModelClass != EventDefinition.class && + outerModelClass != EvidenceVariable.class && + outerModelClass != PlanDefinition.class) { + isApplicable = false; + } + } else if (Expression.class == typeModelClass || + Expression.class == typeModelClass.getEnclosingClass()) { + if (outerModelClass != ActivityDefinition.class && + outerModelClass != TriggerDefinition.class && + outerModelClass != ActivityDefinition.class && + outerModelClass != EvidenceVariable.class && + outerModelClass != Measure.class && + outerModelClass != PlanDefinition.class && + outerModelClass != RequestGroup.class && + outerModelClass != ResearchElementDefinition.class) { + isApplicable = false; + } + } else if (UsageContext.class == typeModelClass || + UsageContext.class == typeModelClass.getEnclosingClass()) { + // TODO: introspect the resourceModelClass to tell if its truly applicable or not + isApplicable = true; + } else if (ProdCharacteristic.class == typeModelClass || + ProdCharacteristic.class == typeModelClass.getEnclosingClass()) { + if (DeviceDefinition.class != outerModelClass) { + isApplicable = false; + } + } else if (ProductShelfLife.class == typeModelClass || + ProductShelfLife.class == typeModelClass.getEnclosingClass()) { + if (DeviceDefinition.class != outerModelClass) { + isApplicable = false; + } + } else if (MarketingStatus.class == typeModelClass || + MarketingStatus.class == typeModelClass.getEnclosingClass()) { + if (DeviceDefinition.class != outerModelClass) { + isApplicable = false; + } + } else if (Population.class == typeModelClass || + Population.class == typeModelClass.getEnclosingClass()) { + isApplicable = false; + } + + return isApplicable; + } + + /** + * Generate definition for all inner classes inside the top level resources. + * @param classFilter + * @param definitions + * @throws Exception + */ + private static void generateDefinitionForInnerClasses(Set classFilter, JsonObjectBuilder definitions) throws Exception { + for (String innerClassName : getAllResourceInnerClasses()) { + String parentClassName = innerClassName.split("\\$")[0]; + if (classFilter.contains(parentClassName)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + } + + /** + * Adds the definitions for all inner classes inside the top level resources to the given definitions set. + * @param classFilter + * @param definitions + * @throws Exception + */ + private static void addDefinitionsForInnerClasses(Set classFilter, Set> definitions) throws Exception { + for (String innerClassName : getAllResourceInnerClasses()) { + String parentClassName = innerClassName.split("\\$")[0]; + if (classFilter.contains(parentClassName)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + definitions.add(innerModelClass); + } + } + } + + +} diff --git a/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/APIConnectAdapter.java b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/APIConnectAdapter.java new file mode 100644 index 00000000000..35544031c3f --- /dev/null +++ b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/APIConnectAdapter.java @@ -0,0 +1,84 @@ +/* + * (C) Copyright IBM Corp. 2019, 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.linuxforhealth.fhir.swagger.generator; + +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObjectBuilder; + +/** + * + */ +public class APIConnectAdapter { + + /** + * Set this to the hostname of your FHIR Server and the generated assembly will proxy this + */ + private static final String HOST = "localhost"; + + private static final JsonBuilderFactory factory = Json.createBuilderFactory(null); + + public static void addApiConnectStuff(JsonObjectBuilder swagger) { + JsonObjectBuilder ibmConfig = factory.createObjectBuilder() + .add("gateway", "datapower-api-gateway") + .add("type", "rest") + .add("phase", "realized") + .add("enforced", true) + .add("testable", true) + .add("cors", factory.createObjectBuilder().add("enabled", true)) + .add("assembly", buildAssembly()) + .add("properties", buildProperties()) + .add("activity-log", factory.createObjectBuilder() + .add("enabled", true) + .add("error-content", "header") + .add("success-content", "activity")) + .add("application-authentication", factory.createObjectBuilder() + .add("certificate", false)) + .add("catalogs", factory.createObjectBuilder()); + swagger.add("x-ibm-configuration", ibmConfig); + } + + private static JsonObjectBuilder buildAssembly() { + JsonObjectBuilder executeInvoke = factory.createObjectBuilder().add("invoke", buildExecuteInvoke()); + + JsonArrayBuilder execute = factory.createArrayBuilder().add(executeInvoke); + + return factory.createObjectBuilder() + .add("execute", execute) + .add("catch", factory.createArrayBuilder()); + } + + private static JsonObjectBuilder buildExecuteInvoke() { + JsonObjectBuilder headerControl = factory.createObjectBuilder() + .add("type", "blacklist") + .add("values", factory.createArrayBuilder()); + JsonObjectBuilder paramControl = factory.createObjectBuilder() + .add("type", "whitelist") + .add("values", factory.createArrayBuilder()); + return factory.createObjectBuilder() + .add("version", "2.0.0") + .add("title", "invoke") + .add("header-control", headerControl) + .add("parameter-control",paramControl) + .add("timeout", 60) + .add("verb", "keep") + .add("cache-response", "protocol") + .add("cache-ttl", 900) + .add("stop-on-error", factory.createArrayBuilder()) + .add("target-url", "$(target-url)$(api.operation.path)$(request.search)"); + } + + private static JsonObjectBuilder buildProperties() { + JsonObjectBuilder targetUrl = factory.createObjectBuilder() + .add("value", "https://" + HOST + "/fhir-server/api/v4/") + .add("description", "The URL of the target service") + .add("encoded", false); + + return factory.createObjectBuilder().add("target-url", targetUrl); + } +} diff --git a/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/FHIRSwaggerGenerator.java b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/FHIRSwaggerGenerator.java new file mode 100644 index 00000000000..61e580d11c9 --- /dev/null +++ b/fhir-swagger-generator/src/main/java/org/linuxforhealth/fhir/swagger/generator/FHIRSwaggerGenerator.java @@ -0,0 +1,2058 @@ +/* + * (C) Copyright IBM Corp. 2016, 2022 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.linuxforhealth.fhir.swagger.generator; + +import java.io.File; +import java.io.FileOutputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.linuxforhealth.fhir.core.FHIRMediaType; +import org.linuxforhealth.fhir.model.annotation.Required; +import org.linuxforhealth.fhir.model.resource.Bundle; +import org.linuxforhealth.fhir.model.resource.CapabilityStatement; +import org.linuxforhealth.fhir.model.resource.DomainResource; +import org.linuxforhealth.fhir.model.resource.Group; +import org.linuxforhealth.fhir.model.resource.OperationOutcome; +import org.linuxforhealth.fhir.model.resource.Parameters; +import org.linuxforhealth.fhir.model.resource.Resource; +import org.linuxforhealth.fhir.model.resource.SearchParameter; +import org.linuxforhealth.fhir.model.resource.StructureDefinition; +import org.linuxforhealth.fhir.model.type.BackboneElement; +import org.linuxforhealth.fhir.model.type.Code; +import org.linuxforhealth.fhir.model.type.Element; +import org.linuxforhealth.fhir.model.type.ElementDefinition; +import org.linuxforhealth.fhir.model.type.Extension; +import org.linuxforhealth.fhir.model.type.Identifier; +import org.linuxforhealth.fhir.model.type.Oid; +import org.linuxforhealth.fhir.model.type.Reference; +import org.linuxforhealth.fhir.model.type.Uuid; +import org.linuxforhealth.fhir.model.util.ModelSupport; +import org.linuxforhealth.fhir.model.visitor.AbstractVisitable; +import org.linuxforhealth.fhir.openapi.generator.FHIROpenApiGenerator; +import org.linuxforhealth.fhir.search.compartment.CompartmentHelper; +import org.linuxforhealth.fhir.search.exception.FHIRSearchException; +import org.linuxforhealth.fhir.search.util.SearchHelper; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.JsonStructure; +import jakarta.json.JsonWriter; +import jakarta.json.JsonWriterFactory; +import jakarta.json.stream.JsonGenerator; + +/** + * Generate Swagger 2.0 from the HL7 FHIR R4 artifacts and the IBM FHIR object model. + * + *

+ * By default, this class will create a separate Swagger definition for each resource type and compartment, + * with all applicable HTTP interactions enabled. + * + *

+ * To limit the output to a given set of resources and/or interactions, pass a set of semicolon-delimited + * filter strings of the form {@code ResourceType1(interaction1,interaction2)}. + * + * For example: + *

+ * Patient(create,update,read,vread,history,search);Contract(create,read,vread,history,search);RiskAssessment(read)
+ * 
+ */ +public class FHIRSwaggerGenerator { + private static final String OUTDIR = "swagger"; + private static final JsonBuilderFactory factory = Json.createBuilderFactory(null); + private static final Map, StructureDefinition> structureDefinitionMap = buildStructureDefinitionMap(); + private static boolean includeDeleteOperation = true; + + public static final String TYPEPACKAGENAME = "org.linuxforhealth.fhir.model.type"; + public static final String RESOURCEPACKAGENAME = "org.linuxforhealth.fhir.model.resource"; + public static final String APPLICATION_FORM = "application/x-www-form-urlencoded"; + + private static final CompartmentHelper compartmentHelper = new CompartmentHelper(); + private static final SearchHelper searchHelper = new SearchHelper(); + + public static void main(String[] args) throws Exception { + File file = new File(OUTDIR); + if (!file.exists()) { + file.mkdirs(); + } + System.out.println("Generating swagger definitions at " + file.getCanonicalPath()); + + Filter filter = null; + if (args.length == 1) { + filter = createFilter(args[0]); + } else { + filter = createAcceptAllFilter(); + } + + List classNames = FHIROpenApiGenerator.getClassNames(); + for (String resourceClassName : classNames) { + Class resourceModelClass = Class.forName(FHIROpenApiGenerator.RESOURCEPACKAGENAME + "." + resourceClassName); + if (DomainResource.class.isAssignableFrom(resourceModelClass) + && DomainResource.class != resourceModelClass) { + if (filter.acceptResourceType(resourceModelClass)) { + + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", resourceClassName + " API"); + info.add("description", "A simplified version of the HL7 FHIR API for " + resourceClassName + " resources."); + info.add("version", "4.0.1"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // generate Resource and DomainResource definitions + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + + generatePaths(resourceModelClass, paths, filter); + JsonObjectBuilder tag = factory.createObjectBuilder(); + tag.add("name", resourceModelClass.getSimpleName()); + tags.add(tag); + generateDefinition(resourceModelClass, definitions); + // for search response + generateDefinition(Bundle.class, definitions); + // for error response + generateDefinition(OperationOutcome.class, definitions); + + // generate definition for all inner classes inside the top level resource. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + String parentClassName = innerClassName.split("\\$")[0]; + if (resourceClassName.equals(parentClassName) || + "DomainResource".equals(parentClassName) || + "Resource".equals(parentClassName) || + "Bundle".equals(parentClassName) || + "OperationOutcome".equals(parentClassName)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the applicable defined Types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (FHIROpenApiGenerator.isApplicableForClass(typeModelClass, resourceModelClass)) { + generateDefinition(typeModelClass, definitions); + } + } + + swagger.add("tags", tags); + swagger.add("paths", paths); + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateParameters(parameters, filter); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + swagger.add("parameters", parametersObject); + } + swagger.add("definitions", definitions); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + resourceClassName + "-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + // generate file for compartment + generateCompartmentSwagger(resourceModelClass, filter); + } + } + + generateMetadataSwagger(); + generateBatchTransactionSwagger(); + generateWholeSystemHistorySwagger(); + if (filter.acceptOperation("export")) { + generateExportSwagger(filter); + } + } + + private static void generateCompartmentSwagger(Class compartmentModelClass, Filter filter) throws Exception, ClassNotFoundException, Error { + String compartmentClassName = compartmentModelClass.getSimpleName(); + Set resourceClassNames = getCompartmentClassNames(compartmentClassName); + if (resourceClassNames == null || resourceClassNames.isEmpty()) { + return; + } + + JsonArrayBuilder tags = factory.createArrayBuilder(); + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + boolean addedContent = false; + + // Only include resource types that are accepted by the filter + for (String resourceClassName : resourceClassNames) { + Class resourceModelClass = Class.forName(FHIROpenApiGenerator.RESOURCEPACKAGENAME + "." + resourceClassName); + if (DomainResource.class.isAssignableFrom(resourceModelClass) + && DomainResource.class != resourceModelClass + && filter.acceptResourceType(resourceModelClass)) { + addedContent = true; + + generateCompartmentPaths(compartmentModelClass, resourceModelClass, paths, filter); + JsonObjectBuilder tag = factory.createObjectBuilder(); + tag.add("name", resourceClassName); + tags.add(tag); + generateDefinition(resourceModelClass, definitions); + + // generate definition for all inner classes inside the top level resource. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + String parentClassName = innerClassName.split("\\$")[0]; + if (resourceClassName.equals(parentClassName) || + "DomainResource".equals(parentClassName) || + "Resource".equals(parentClassName) || + "Bundle".equals(parentClassName) || + "OperationOutcome".equals(parentClassName)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the applicable defined Types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (FHIROpenApiGenerator.isApplicableForClass(typeModelClass, resourceModelClass)) { + generateDefinition(typeModelClass, definitions); + } + } + } + } + + if (!addedContent) { + return; + } + + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", compartmentClassName + " compartment API"); + info.add("description", "A simplified version of the HL7 FHIR API for " + compartmentClassName + " compartment."); + info.add("version", "4.0.1"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + // generate Resource and DomainResource definitions + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + // for search response + generateDefinition(Bundle.class, definitions); + // for error response + generateDefinition(OperationOutcome.class, definitions); + + swagger.add("tags", tags); + swagger.add("paths", paths); + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateCompartmentSearchParameters(parameters, filter); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + swagger.add("parameters", parametersObject); + } + swagger.add("definitions", definitions); + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + compartmentClassName + "-compartment-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateMetadataSwagger() throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Capabilities"); + info.add("description", "The capabilities interaction retrieves the information about a server's capabilities - which portions of the HL7 FHIR specification it supports."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR capabilities interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateMetadataPathItem(path); + paths.add("/metadata", path); + swagger.add("paths", paths); + + + generateDefinition(CapabilityStatement.class, definitions); + generateDefinition(Resource.class, definitions); + generateDefinition(DomainResource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + Class parentClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName.split("\\$")[0]); + if (CapabilityStatement.class.equals(parentClass)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the applicable data types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (FHIROpenApiGenerator.isApplicableForClass(typeModelClass, CapabilityStatement.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + swagger.add("definitions", definitions); + + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "metadata-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateWholeSystemHistorySwagger() throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Whole System History"); + info.add("description", "The whole system history interaction can be used to obtain a list of changes (create, update, delete) to resources in the IBM FHIR Server."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR _history interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateWholeSystemHistoryPathItem(path, "Other", "Get the whole system history", false); + paths.add("/_history", path); + swagger.add("paths", paths); + + + generateDefinition(Bundle.class, definitions); + generateDefinition(Resource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + Class parentClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName.split("\\$")[0]); + if (Bundle.class.equals(parentClass)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the applicable data types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (FHIROpenApiGenerator.isApplicableForClass(typeModelClass, Bundle.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateWholeSystemHistoryParameters(parameters, true); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + swagger.add("parameters", parametersObject); + } + + swagger.add("definitions", definitions); + + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "wholeSystemHistory-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateBatchTransactionSwagger() throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Batch/Transaction"); + info.add("description", "The batch and transaction interactions submit a set of actions to perform on a server in a single HTTP request/response."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // FHIR batch/transaction interaction + JsonObjectBuilder path = factory.createObjectBuilder(); + generateBatchPathItem(path); + paths.add("/", path); + swagger.add("paths", paths); + + + generateDefinition(Bundle.class, definitions); + + // generate definition for all inner classes inside the top level resources. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + Class parentClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName.split("\\$")[0]); + if (Bundle.class.equals(parentClass)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the defined Types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + generateDefinition(typeModelClass, definitions); + } + + swagger.add("definitions", definitions); + + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "batch-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static void generateExportSwagger(Filter filter) throws Exception, ClassNotFoundException, Error { + JsonObjectBuilder swagger = factory.createObjectBuilder(); + swagger.add("swagger", "2.0"); + + JsonObjectBuilder info = factory.createObjectBuilder(); + info.add("title", "Bulk Data Export"); + info.add("description", "Export Data from the FHIR server."); + info.add("version", "4.0.0"); + swagger.add("info", info); + + swagger.add("basePath", "/fhir-server/api/v4"); + + // Set the hostname in APIConnectAdapter and uncomment this to add "x-ibm-configuration" + // with a default ExecuteInvoke Assembly + APIConnectAdapter.addApiConnectStuff(swagger); + + JsonObjectBuilder paths = factory.createObjectBuilder(); + JsonObjectBuilder definitions = factory.createObjectBuilder(); + + // System export + if (filter.acceptOperation(Resource.class, "export")) { + JsonObjectBuilder path = factory.createObjectBuilder(); + generateExportPathItem(path, "System", "Export data from the FHIR server", false, "get", false); + generateExportPathItem(path, "System", "Export data from the FHIR server", false, "post", false); + paths.add("/$export", path); + } + + // Patient and Group export + for (Class modelClass : FHIROpenApiGenerator.DATA_EXPORT_TYPES) { + if (filter.acceptOperation(modelClass, "export")) { + // generate definition for all inner classes inside the top level resources. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + Class parentClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName.split("\\$")[0]); + if (Parameters.class.equals(parentClass)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // Add Export + String typeName = modelClass.getSimpleName(); + JsonObjectBuilder path = factory.createObjectBuilder(); + generateExportPathItem(path, typeName, "Export " + typeName + " data from the FHIR server", true, "get", "Group".equals(typeName)); + generateExportPathItem(path, typeName, "Export " + typeName + " data from the FHIR server", true, "post", "Group".equals(typeName)); + JsonObject pathObject = path.build(); + StringBuilder pathStringBuilder = new StringBuilder(); + pathStringBuilder.append("/"); + pathStringBuilder.append(typeName); + if (modelClass.getSimpleName().equals("Group")) + pathStringBuilder.append("/{id}"); + pathStringBuilder.append("/$export"); + + paths.add(pathStringBuilder.toString(), pathObject); + } + } + + // Add bulkdata-status + JsonObjectBuilder path = factory.createObjectBuilder(); + generateBulkDataStatusPathItem(path); + paths.add("/$bulkdata-status", path); + + swagger.add("paths", paths); + + generateDefinition(Parameters.class, definitions); + generateDefinition(Resource.class, definitions); + + // generate definition for all inner classes inside the top level resources. + for (String innerClassName : FHIROpenApiGenerator.getAllResourceInnerClasses()) { + Class parentClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName.split("\\$")[0]); + if (Parameters.class.equals(parentClass)) { + Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); + generateDefinition(innerModelClass, definitions); + } + } + + // generate definition for all the applicable data types. + for (String typeClassName : FHIROpenApiGenerator.getAllTypesList()) { + Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); + if (FHIROpenApiGenerator.isApplicableForClass(typeModelClass, Parameters.class)) { + generateDefinition(typeModelClass, definitions); + } + } + + JsonObjectBuilder parameters = factory.createObjectBuilder(); + generateExportParameters(parameters, filter.acceptOperation(Group.class, "export")); + JsonObject parametersObject = parameters.build(); + if (!parametersObject.isEmpty()) { + swagger.add("parameters", parametersObject); + } + + swagger.add("definitions", definitions); + + + Map config = new HashMap(); + config.put(JsonGenerator.PRETTY_PRINTING, true); + JsonWriterFactory factory = Json.createWriterFactory(config); + + File outFile = new File(OUTDIR + File.separator + "export-swagger.json"); + try (JsonWriter writer = factory.createWriter(new FileOutputStream(outFile), StandardCharsets.UTF_8)) { + writer.writeObject(swagger.build()); + } catch (Exception e) { + throw new Error(e); + } + } + + private static Map, StructureDefinition> buildStructureDefinitionMap() { + Map, StructureDefinition> structureDefinitionMap = new HashMap, StructureDefinition>(); + try { + FHIROpenApiGenerator.populateStructureDefinitionMap(structureDefinitionMap, "profiles-resources.json"); + FHIROpenApiGenerator.populateStructureDefinitionMap(structureDefinitionMap, "profiles-types.json"); + } catch (Exception e) { + throw new Error(e); + } + return structureDefinitionMap; + } + + private static void generateParameters(JsonObjectBuilder parameters, Filter filter) throws Exception { + if (filter.acceptOperation("read") || filter.acceptOperation("vread") || filter.acceptOperation("update") + || filter.acceptOperation("delete") || filter.acceptOperation("history")) { + JsonObjectBuilder id = factory.createObjectBuilder(); + id.add("name", "id"); + id.add("description", "logical identifier"); + id.add("in", "path"); + id.add("required", true); + id.add("type", "string"); + parameters.add("idParam", id); + } + if (filter.acceptOperation("history")) { + generateWholeSystemHistoryParameters(parameters, false); + } + if (filter.acceptOperation("vread")) { + JsonObjectBuilder vid = factory.createObjectBuilder(); + vid.add("name", "vid"); + vid.add("description", "version identifier"); + vid.add("in", "path"); + vid.add("required", true); + vid.add("type", "string"); + parameters.add("vidParam", vid); + } + if (filter.acceptOperation("search")) { + for (Entry entry : searchHelper.getSearchParameters(Resource.class.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = entry.getKey(); + parameter.add("name", name); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "query"); + parameter.add("required", false); + parameter.add("type", "string"); + parameters.add(name + "Param", parameter); + } + } + } + + private static void generateCompartmentSearchParameters(JsonObjectBuilder parameters, Filter filter) throws Exception { + if (filter.acceptOperation("search")) { + JsonObjectBuilder id = factory.createObjectBuilder(); + id.add("name", "id"); + id.add("description", "logical identifier"); + id.add("in", "path"); + id.add("required", true); + id.add("type", "string"); + parameters.add("idParam", id); + for (Entry entry : searchHelper.getSearchParameters(Resource.class.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = searchParameter.getName().getValue(); + parameter.add("name", entry.getKey()); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "query"); + parameter.add("required", false); + parameter.add("type", "string"); + parameters.add(name + "Param", parameter); + } + } + } + + private static void generateWholeSystemHistoryParameters(JsonObjectBuilder parameters, boolean includeType) throws Exception { + // Add the _sort parameter (_sort=[-_lastUpdated, _lastUpdated, none]) + // curl -k -u ':' 'https://:/fhir-server/api/v4/_history?_since=2021-02-21T00:00:00Z&_sort=_lastUpdated' + + JsonObjectBuilder sort = factory.createObjectBuilder(); + sort.add("name", "_sort"); + sort.add("description", "Sort the returned data using one of three options: \r\n\r\n* -_lastUpdated: [Default] Decreasing time - most recent changes first\r\n* _lastUpdated: Increasing time - oldest changes first\r\n* none: Increasing id order - oldest changes first\r\n"); + sort.add("in", "query"); + sort.add("required", false); + + sort.add("type", "string"); + JsonArrayBuilder enums = factory.createArrayBuilder(); + enums.add("-_lastUpdated"); + enums.add("_lastUpdated"); + enums.add("none"); + sort.add("enum", enums); + + parameters.add("_sortParam", sort); + + // add _since= + JsonObjectBuilder since = factory.createObjectBuilder(); + since.add("name", "_since"); + since.add("description", "Return changes since a known timestamp"); + since.add("in", "query"); + since.add("required", false); + since.add("type", "string"); + + parameters.add("_sinceParam", since); + + // add _before= + JsonObjectBuilder before = factory.createObjectBuilder(); + before.add("name", "_before"); + before.add("description", "Return changes before a known timestamp"); + before.add("in", "query"); + before.add("required", false); + before.add("type", "string"); + + parameters.add("_beforeParam", before); + + // add _count + JsonObjectBuilder count = factory.createObjectBuilder(); + count.add("name", "_count"); + count.add("description", "Specifies a maximum number of results that are required"); + count.add("in", "query"); + count.add("required", false); + count.add("type", "integer"); + count.add("default", 100); + + parameters.add("_countParam", count); + + // add _type + if (includeType) { + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + type.add("type", "string"); + + parameters.add("_typeParam", type); + } + } + + private static void generateExportParameters(JsonObjectBuilder parameters, boolean includePathIdParam) throws Exception { + JsonObjectBuilder outputFormat = factory.createObjectBuilder(); + outputFormat.add("name", "_outputFormat"); + outputFormat.add("description", "The format for the requested bulk data files to be generated"); + outputFormat.add("in", "query"); + outputFormat.add("required", false); + outputFormat.add("type", "string"); + + parameters.add("_outputFormatParam", outputFormat); + + // add _since= + JsonObjectBuilder since = factory.createObjectBuilder(); + since.add("name", "_since"); + since.add("description", "Resources updated after this period will be included in the response."); + since.add("in", "query"); + since.add("required", false); + since.add("type", "string"); + + parameters.add("_sinceParam", since); + + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + type.add("type", "string"); + + parameters.add("_typeParam", type); + + if (includePathIdParam) { + JsonObjectBuilder id = factory.createObjectBuilder(); + id.add("name", "id"); + id.add("description", "logical identifier"); + id.add("in", "path"); + id.add("required", true); + id.add("type", "string"); + parameters.add("idParam", id); + } + } + + private static void generatePaths(Class modelClass, JsonObjectBuilder paths, Filter filter) throws Exception { + JsonObjectBuilder path = factory.createObjectBuilder(); + // FHIR create operation + if (filter.acceptOperation(modelClass, "create")) { + generateCreatePathItem(modelClass, path); + } + // FHIR search operation + if (filter.acceptOperation(modelClass, "search")) { + generateSearchPathItem(null, modelClass, path); + } + JsonObject pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName(), pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR search (via POST) operation + if (filter.acceptOperation(modelClass, "search")) { + generateSearchViaPostPathItem(null, modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/_search", pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR vread operation + if (filter.acceptOperation(modelClass, "vread")) { + generateVreadPathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}/_history/{vid}", pathObject); + } + + path = factory.createObjectBuilder(); + // FHIR read operation + if (filter.acceptOperation(modelClass, "read")) { + generateReadPathItem(modelClass, path); + } + // FHIR update operation + if (filter.acceptOperation(modelClass, "update")) { + generateUpdatePathItem(modelClass, path); + } + // FHIR delete operation + if (filter.acceptOperation(modelClass, "delete") && includeDeleteOperation) { + generateDeletePathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}", pathObject); + } + + // FHIR history operation + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "history")) { + generateHistoryPathItem(modelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/{id}/_history", pathObject); + } + + // Add Whole system History for type + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "history")) { + generateWholeSystemHistoryPathItem(path, modelClass.getSimpleName(), "Get the whole system history for " + modelClass.getSimpleName() + " resources", true); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + modelClass.getSimpleName() + "/_history", pathObject); + } + + // Add Patient Everything + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "everything") && "Patient".equals(modelClass.getSimpleName())) { + generatePatientEverythingPathItem(path, false); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/Patient/$everything", pathObject); + } + + path = factory.createObjectBuilder(); + if (filter.acceptOperation(modelClass, "everything") && "Patient".equals(modelClass.getSimpleName())) { + generatePatientEverythingPathItem(path, true); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/Patient/{id}/$everything", pathObject); + } + + // TODO: add patch + } + + private static void generateCompartmentPaths(Class compartmentModelClass, Class resourceModelClass, JsonObjectBuilder paths, Filter filter) throws Exception { + JsonObjectBuilder path = factory.createObjectBuilder(); + + // FHIR search operation + if (filter.acceptOperation(resourceModelClass, "search")) { + generateSearchPathItem(compartmentModelClass, resourceModelClass, path); + } + JsonObject pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + compartmentModelClass.getSimpleName() + "/{id}/" + resourceModelClass.getSimpleName(), pathObject); + } + + path = factory.createObjectBuilder(); + + // FHIR search (via POST) operation + if (filter.acceptOperation(resourceModelClass, "search")) { + generateSearchViaPostPathItem(compartmentModelClass, resourceModelClass, path); + } + pathObject = path.build(); + if (!pathObject.isEmpty()) { + paths.add("/" + compartmentModelClass.getSimpleName() + "/{id}/" + resourceModelClass.getSimpleName() + "/_search", pathObject); + } + } + + private static void generateCreatePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + post.add("tags", tags); + post.add("summary", "Create" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + post.add("operationId", "create" + modelClass.getSimpleName()); + + JsonArrayBuilder consumes = factory.createArrayBuilder(); + consumes.add(FHIRMediaType.APPLICATION_FHIR_JSON); + post.add("consumes", consumes); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder bodyParameter = factory.createObjectBuilder(); + + bodyParameter.add("name", "body"); + bodyParameter.add("in", "body"); + bodyParameter.add("required", true); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/" + modelClass.getSimpleName()); + bodyParameter.add("schema", schema); + + parameters.add(bodyParameter); + post.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Create " + modelClass.getSimpleName() + " operation successful"); + responses.add("201", response); + post.add("responses", responses); + + path.add("post", post); + } + + private static void generateReadPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", "Read" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "read" + modelClass.getSimpleName()); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Read " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/" + modelClass.getSimpleName()); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateVreadPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", + "Read specific version of" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "vread" + modelClass.getSimpleName()); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + + JsonObjectBuilder vidParamRef = factory.createObjectBuilder(); + vidParamRef.add("$ref", "#/parameters/vidParam"); + parameters.add(vidParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Versioned read " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/" + modelClass.getSimpleName()); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateUpdatePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder put = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + put.add("tags", tags); + put.add("summary", "Update an existing " + modelClass.getSimpleName() + " resource"); + put.add("operationId", "update" + modelClass.getSimpleName()); + + JsonArrayBuilder consumes = factory.createArrayBuilder(); + consumes.add(FHIRMediaType.APPLICATION_FHIR_JSON); + put.add("consumes", consumes); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + + JsonObjectBuilder bodyParameter = factory.createObjectBuilder(); + bodyParameter.add("name", "body"); + bodyParameter.add("in", "body"); + bodyParameter.add("required", true); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/" + modelClass.getSimpleName()); + bodyParameter.add("schema", schema); + + parameters.add(bodyParameter); + put.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response1 = factory.createObjectBuilder(); + response1.add("description", "Update " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder response2 = factory.createObjectBuilder(); + response2.add("description", "Create " + modelClass.getSimpleName() + + " operation successful (requires 'updateCreateEnabled' configuration option)"); + + responses.add("200", response1); + responses.add("201", response2); + + put.add("responses", responses); + + path.add("put", put); + } + + private static void generateDeletePathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder delete = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + delete.add("tags", tags); + delete.add("summary", "Delete" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + delete.add("operationId", "delete" + modelClass.getSimpleName()); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + + delete.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Delete " + modelClass.getSimpleName() + " operation successful"); + responses.add("204", response); + delete.add("responses", responses); + + path.add("delete", delete); + } + + private static void generateSearchPathItem(Class compartmentModelClass, Class modelClass, JsonObjectBuilder path) throws Exception { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + get.add("tags", tags); + + if (compartmentModelClass != null) { + get.add("summary", "Search for " + modelClass.getSimpleName() + " resources within " + compartmentModelClass.getSimpleName() + " compartment"); + get.add("operationId", "search" + compartmentModelClass.getSimpleName() + "Compartment"+ modelClass.getSimpleName()); + } else { + get.add("summary", "Search for " + modelClass.getSimpleName() + " resources"); + get.add("operationId", "search" + modelClass.getSimpleName()); + } + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // For compartment search, include parameter for {id} + if (compartmentModelClass != null) { + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + } + generateSearchParameters(modelClass, parameters); + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Search " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateSearchParameters(Class modelClass, JsonArrayBuilder parameters) throws Exception { + for (Entry entry : searchHelper.getSearchParameters(modelClass.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = entry.getKey(); + if (name.startsWith("_")) { + parameter.add("$ref", "#/parameters/" + name + "Param"); + } else { + parameter.add("name", name); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "query"); + parameter.add("type", "string"); + parameter.add("required", false); + } + parameters.add(parameter); + } + } + + private static void generateSearchViaPostPathItem(Class compartmentModelClass, Class modelClass, JsonObjectBuilder path) throws Exception { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + post.add("tags", tags); + + if (compartmentModelClass != null) { + post.add("summary", "Search for " + modelClass.getSimpleName() + " resources within " + compartmentModelClass.getSimpleName() + " compartment"); + post.add("operationId", "searchViaPost" + compartmentModelClass.getSimpleName() + "Compartment"+ modelClass.getSimpleName()); + } else { + post.add("summary", "Search for " + modelClass.getSimpleName() + " resources"); + post.add("operationId", "searchViaPost" + modelClass.getSimpleName()); + } + + JsonArrayBuilder consumes = factory.createArrayBuilder(); + consumes.add(APPLICATION_FORM); + post.add("consumes", consumes); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + post.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // For compartment search, include parameter for {id} + if (compartmentModelClass != null) { + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + } + generateSearchParameters(modelClass, parameters); + generateSearchFormParameters(modelClass, parameters); + post.add("parameters", parameters); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + JsonObjectBuilder responses = factory.createObjectBuilder(); + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Search " + modelClass.getSimpleName() + " operation successful"); + + schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + post.add("responses", responses); + + path.add("post", post); + } + + private static void generateSearchFormParameters(Class modelClass, JsonArrayBuilder parameters) throws Exception { + for (Entry entry : searchHelper.getSearchParameters(modelClass.getSimpleName()).entrySet()) { + SearchParameter searchParameter = entry.getValue(); + + JsonObjectBuilder parameter = factory.createObjectBuilder(); + String name = entry.getKey(); + parameter.add("name", name); + parameter.add("description", searchParameter.getDescription().getValue()); + parameter.add("in", "formData"); + parameter.add("type", "string"); + parameters.add(parameter); + } + } + + private static void generateHistoryPathItem(Class modelClass, JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(modelClass.getSimpleName()); + + get.add("tags", tags); + get.add("summary", "Return the history of" + getArticle(modelClass) + modelClass.getSimpleName() + " resource"); + get.add("operationId", "history" + modelClass.getSimpleName()); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder idParamRef = factory.createObjectBuilder(); + idParamRef.add("$ref", "#/parameters/idParam"); + parameters.add(idParamRef); + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "History " + modelClass.getSimpleName() + " operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateMetadataPathItem(JsonObjectBuilder path) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Other"); + + get.add("tags", tags); + get.add("summary", "Get the FHIR Capability statement for this endpoint"); + get.add("operationId", "metadata"); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "metadata operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/CapabilityStatement"); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + /** + * This method is used by both the resource specific whole system history path generation and the general whole system history path generation. + * If it is resource specific, the type parameter will not be generated and the resource type will be included in the operationId. + * @param path + * @param tag + * @param summary + * @param resourceSpecific + */ + private static void generateWholeSystemHistoryPathItem(JsonObjectBuilder path, String tag, String summary, boolean resourceSpecific) { + JsonObjectBuilder get = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add(tag); + + get.add("tags", tags); + get.add("summary", summary); + + String operationId = "wholeSystemHistory"; + if (resourceSpecific) { + operationId = operationId + tag; + } + get.add("operationId", operationId); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + get.add("produces", produces); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + // Add parameters to api call + JsonObjectBuilder sortParameter = factory.createObjectBuilder(); + sortParameter.add("$ref", "#/parameters/_sortParam"); + parameters.add(sortParameter); + + JsonObjectBuilder sinceParameter = factory.createObjectBuilder(); + sinceParameter.add("$ref", "#/parameters/_sinceParam"); + parameters.add(sinceParameter); + + JsonObjectBuilder beforeParameter = factory.createObjectBuilder(); + beforeParameter.add("$ref", "#/parameters/_beforeParam"); + parameters.add(beforeParameter); + + JsonObjectBuilder countParameter = factory.createObjectBuilder(); + countParameter.add("$ref", "#/parameters/_countParam"); + parameters.add(countParameter); + + if (!resourceSpecific) { + JsonObjectBuilder typeParameter = factory.createObjectBuilder(); + typeParameter.add("$ref", "#/parameters/_typeParam"); + parameters.add(typeParameter); + } + + get.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "whole system history operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + get.add("responses", responses); + + path.add("get", get); + } + + private static void generateBatchPathItem(JsonObjectBuilder path) { + JsonObjectBuilder post = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Other"); + + post.add("tags", tags); + post.add("summary", "Perform a batch operation"); + post.add("operationId", "batch"); + + JsonArrayBuilder consumes = factory.createArrayBuilder(); + consumes.add(FHIRMediaType.APPLICATION_FHIR_JSON); + post.add("consumes", consumes); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + JsonObjectBuilder bodyParameter = factory.createObjectBuilder(); + + bodyParameter.add("name", "body"); + bodyParameter.add("in", "body"); + bodyParameter.add("required", true); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + bodyParameter.add("schema", schema); + + parameters.add(bodyParameter); + post.add("parameters", parameters); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + post.add("produces", produces); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "batch operation successful"); + + schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + post.add("responses", responses); + + path.add("post", post); + } + + /** + * Common method used for both the general export function and the Patient or Group specific function. + * Expected tag parameter is set to "other" for non resource specific generation. Tags "Patient" or "Group" for resource specific generation. + * HttpMethod parameter is "get" or "post". + * + * @param path + * @param type + * @param summary + * @param resourceSpecific + * @param httpMethod + */ + private static void generateExportPathItem(JsonObjectBuilder path, String type, String summary, boolean resourceSpecific, String httpMethod, boolean addIdParam) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Bulk Data"); + + httpMethodBuilder.add("tags", tags); + httpMethodBuilder.add("summary", summary); + + String operationId = httpMethod + "Export"; + if (resourceSpecific) { + operationId = operationId + type; + } + httpMethodBuilder.add("operationId", operationId); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + if (addIdParam) { + JsonObjectBuilder idParameter = factory.createObjectBuilder(); + idParameter.add("$ref", "#/parameters/idParam"); + parameters.add(idParameter); + } + + if ("post".equalsIgnoreCase(httpMethod)) { + JsonArrayBuilder consumes = factory.createArrayBuilder(); + consumes.add(FHIRMediaType.APPLICATION_FHIR_JSON); + httpMethodBuilder.add("consumes", consumes); + + // Generate POST Parameters + JsonObjectBuilder body = factory.createObjectBuilder(); + body.add("name", "body"); + body.add("description", "The Parameters for the bulk data export request."); + body.add("in", "body"); + body.add("required", true); + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Parameters"); + body.add("schema", schema); + + parameters.add(body); + } else { + // Generate GET parameters + JsonObjectBuilder outputFormatParameter = factory.createObjectBuilder(); + outputFormatParameter.add("$ref", "#/parameters/_outputFormatParam"); + parameters.add(outputFormatParameter); + + JsonObjectBuilder sinceParameter = factory.createObjectBuilder(); + sinceParameter.add("$ref", "#/parameters/_sinceParam"); + parameters.add(sinceParameter); + + JsonObjectBuilder typeParameter = factory.createObjectBuilder(); + typeParameter.add("$ref", "#/parameters/_typeParam"); + parameters.add(typeParameter); + } + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_NDJSON); + httpMethodBuilder.add("produces", produces); + + httpMethodBuilder.add("parameters", parameters); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Export job is initiated"); + + // Add Response headers + JsonObjectBuilder headers = factory.createObjectBuilder(); + JsonObjectBuilder contentLocation = factory.createObjectBuilder(); + contentLocation.add("description", "The polling location"); + contentLocation.add("type", "string"); + + headers.add("Content-Location", contentLocation); + response.add("headers", headers); + + responses.add("202", response); + httpMethodBuilder.add("responses", responses); + + path.add(httpMethod, httpMethodBuilder); + } + + /** + * + * @param path + */ + private static void generateBulkDataStatusPathItem(JsonObjectBuilder path) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Bulk Data"); + + httpMethodBuilder.add("tags", tags); + httpMethodBuilder.add("summary", "Retrieves the status of the buld data request"); + httpMethodBuilder.add("operationId", "bulkDataStatus"); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + // Generate GET parameters + JsonObjectBuilder jobParameter = factory.createObjectBuilder(); + jobParameter.add("name", "job"); + jobParameter.add("description", "Job id from the Content-Location header response value after the export request"); + jobParameter.add("in", "query"); + jobParameter.add("required", true); + jobParameter.add("type", "string"); + parameters.add(jobParameter); + + httpMethodBuilder.add("parameters", parameters); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add("application/json"); + httpMethodBuilder.add("produces", produces); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + // 202 not completed + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "Export job is queued and not yet complete"); + responses.add("202", response); + + // Add 200 completed + response = factory.createObjectBuilder(); + response.add("description", "Export job completed"); + + // add response body + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("type", "object"); + JsonObjectBuilder properties = factory.createObjectBuilder(); + JsonObjectBuilder transactionTime = factory.createObjectBuilder(); + transactionTime.add("type", "string"); + properties.add("transactionTime", transactionTime); + + JsonObjectBuilder request = factory.createObjectBuilder(); + request.add("type", "string"); + properties.add("request", request); + + JsonObjectBuilder requiresAccessToken = factory.createObjectBuilder(); + requiresAccessToken.add("type", "boolean"); + properties.add("requiresAccessToken", requiresAccessToken); + + JsonObjectBuilder output = factory.createObjectBuilder(); + output.add("type", "array"); + JsonObjectBuilder items = factory.createObjectBuilder(); + items.add("type", "object"); + JsonObjectBuilder itemProperties = factory.createObjectBuilder(); + + JsonObjectBuilder outputArrayEntryTypeProperty = factory.createObjectBuilder(); + outputArrayEntryTypeProperty.add("type", "string"); + itemProperties.add("type", outputArrayEntryTypeProperty); + + JsonObjectBuilder outputArrayEntryUrlProperty = factory.createObjectBuilder(); + outputArrayEntryUrlProperty.add("type", "string"); + itemProperties.add("url", outputArrayEntryUrlProperty); + + JsonObjectBuilder outputArrayEntryCountProperty = factory.createObjectBuilder(); + outputArrayEntryCountProperty.add("type", "integer"); + itemProperties.add("count", outputArrayEntryCountProperty); + + items.add("properties", itemProperties); + output.add("items", items); + properties.add("output", output); + + schema.add("properties", properties); + response.add("schema", schema); + responses.add("200", response); + httpMethodBuilder.add("responses", responses); + + path.add("get", httpMethodBuilder); + } + + /** + * Generates the $everything path for a Patient resource + * + * @param path + * @param addIdParam + */ + private static void generatePatientEverythingPathItem(JsonObjectBuilder path, boolean addIdParam) { + JsonObjectBuilder httpMethodBuilder = factory.createObjectBuilder(); + + JsonArrayBuilder tags = factory.createArrayBuilder(); + tags.add("Patient"); + httpMethodBuilder.add("tags", tags); + + JsonArrayBuilder parameters = factory.createArrayBuilder(); + + if (addIdParam) { + httpMethodBuilder.add("summary", "Return all the information related to a given patient"); + httpMethodBuilder.add("operationId", "everythingPatient"); + + JsonObjectBuilder idParameter = factory.createObjectBuilder(); + idParameter.add("$ref", "#/parameters/idParam"); + parameters.add(idParameter); + } else { + httpMethodBuilder.add("summary", "Return all the information related to one or more patients"); + httpMethodBuilder.add("operationId", "everythingAllPatients"); + } + + JsonObjectBuilder startParam = factory.createObjectBuilder(); + startParam.add("name", "start"); + startParam.add("description", "All records relating to care provided in a certain date range"); + startParam.add("in", "query"); + startParam.add("required", false); + startParam.add("type", "string"); + parameters.add(startParam); + + JsonObjectBuilder endParam = factory.createObjectBuilder(); + endParam.add("name", "end"); + endParam.add("description", "All records relating to care provided in a certain date range"); + endParam.add("in", "query"); + endParam.add("required", false); + endParam.add("type", "string"); + parameters.add(endParam); + + JsonObjectBuilder type = factory.createObjectBuilder(); + type.add("name", "_type"); + type.add("description", "Limit which resource types are returned"); + type.add("in", "query"); + type.add("required", false); + type.add("type", "string"); + + parameters.add(type); + + httpMethodBuilder.add("parameters", parameters); + + JsonArrayBuilder produces = factory.createArrayBuilder(); + produces.add(FHIRMediaType.APPLICATION_FHIR_JSON); + httpMethodBuilder.add("produces", produces); + + JsonObjectBuilder responses = factory.createObjectBuilder(); + + JsonObjectBuilder response = factory.createObjectBuilder(); + response.add("description", "everything operation successful"); + + JsonObjectBuilder schema = factory.createObjectBuilder(); + schema.add("$ref", "#/definitions/Bundle"); + + response.add("schema", schema); + responses.add("200", response); + httpMethodBuilder.add("responses", responses); + + path.add("get", httpMethodBuilder); + } + + private static void generateDefinition(Class modelClass, JsonObjectBuilder definitions) throws Exception { + if (!ModelSupport.isPrimitiveType(modelClass)) { + JsonObjectBuilder definition = factory.createObjectBuilder(); + JsonObjectBuilder properties = factory.createObjectBuilder(); + JsonArrayBuilder required = factory.createArrayBuilder(); + + StructureDefinition structureDefinition = getStructureDefinition(modelClass); + + if (structureDefinition == null) { + System.err.println("Failed generateDefinition for: " + modelClass.getName()); + return; + } + + if (Resource.class.isAssignableFrom(modelClass)) { + // add the 'resourceType' property + JsonObjectBuilder property = factory.createObjectBuilder(); + + // Convert all the primitive types to json types. + property.add("type", "string"); + if (Resource.class == modelClass) { + // TODO: when a filter was passed, limit this to just the resource types included in the filter + List typeNames = FHIROpenApiGenerator.getClassNames(); + JsonArrayBuilder enumValues = factory.createArrayBuilder(typeNames); + property.add("enum", enumValues); + properties.add("resourceType", property.build()); + required.add("resourceType"); + } else { + // TODO how to "overwrite" the Resource definition and say that the value is fixed? + // https://github.com/OAI/OpenAPI-Specification/issues/1313 +// property.add("enum", modelClass.getSimpleName()); + } + } + + for (Field field : modelClass.getDeclaredFields()) { + if (!Modifier.isStatic(field.getModifiers()) && !Modifier.isVolatile(field.getModifiers())) { + if (!ModelSupport.isChoiceElement(modelClass, ModelSupport.getElementName(field)) && field.isAnnotationPresent(Required.class)) { + required.add(ModelSupport.getElementName(field)); + } + generateProperties(structureDefinition, modelClass, field, properties); + } + } + + JsonArray requiredArray = required.build(); + + Class superClass = modelClass.getSuperclass(); + if (superClass != null + && superClass.getPackage().getName().startsWith("org.linuxforhealth.fhir.model") + && !superClass.equals(AbstractVisitable.class)) { + JsonArrayBuilder allOf = factory.createArrayBuilder(); + + JsonObjectBuilder ref = factory.createObjectBuilder(); + ref.add("$ref", "#/definitions/" + superClass.getSimpleName()); + allOf.add(ref); + + JsonObjectBuilder wrapper = factory.createObjectBuilder(); + wrapper.add("type", "object"); + wrapper.add("properties", properties); + if (!requiredArray.isEmpty()) { + wrapper.add("required", requiredArray); + } + allOf.add(wrapper); + + definition.add("allOf", allOf); + } else { + definition.add("type", "object"); + if (Resource.class.equals(modelClass)) { + definition.add("discriminator", "resourceType"); + } + definition.add("properties", properties); + if (!requiredArray.isEmpty()) { + definition.add("required", requiredArray); + } + } + + if (Resource.class.isAssignableFrom(modelClass)) { + FHIROpenApiGenerator.addExamples(modelClass, definition); + } + + definitions.add(getSimpleNameWithEnclosingNames(modelClass), definition); + } + } + + private static String getSimpleNameWithEnclosingNames(Class modelClass) { + StringBuilder fullName = new StringBuilder(modelClass.getSimpleName()); + while (modelClass.isMemberClass()) { + modelClass = modelClass.getEnclosingClass(); + fullName.insert(0, modelClass.getSimpleName() + "_"); + } + return fullName.toString(); + } + + private static StructureDefinition getStructureDefinition(Class modelClass) { + StructureDefinition structureDefinition = null; + // Use Code definition for all its sub-classes for now + if (Code.class.isAssignableFrom(modelClass)) { + try { + structureDefinition = structureDefinitionMap.get(Class.forName(FHIROpenApiGenerator.TYPEPACKAGENAME + "." + "Code")); + } catch (ClassNotFoundException e) { + structureDefinition = null; + } + } else { + structureDefinition = structureDefinitionMap.get(modelClass); + structureDefinition = (structureDefinition != null) + ? structureDefinition : getEnclosingStructureDefinition(modelClass); + } + + return structureDefinition; + } + + private static StructureDefinition getEnclosingStructureDefinition(Class modelClass) { + StructureDefinition structureDefinition = null; + int nameLength = 0; + for (Class key : structureDefinitionMap.keySet()) { + String modelClassName; + if (modelClass.getName().contains(FHIROpenApiGenerator.TYPEPACKAGENAME)) { + modelClassName = modelClass.getName().substring(FHIROpenApiGenerator.TYPEPACKAGENAME.length()+1); + } else { + modelClassName = modelClass.getName().substring(FHIROpenApiGenerator.RESOURCEPACKAGENAME.length()+1); + } + + + if (modelClassName.startsWith(key.getSimpleName()) + && key.getSimpleName().length() > nameLength) { + structureDefinition = structureDefinitionMap.get(key); + nameLength = key.getSimpleName().length(); + } + } + return structureDefinition; + } + + private static void generateProperties(StructureDefinition structureDefinition, Class modelClass, Field field, + JsonObjectBuilder properties) throws Exception { + + boolean many = false; + + Type fieldType = field.getType(); + Type genericType = field.getGenericType(); + if (genericType instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) genericType; + fieldType = parameterizedType.getActualTypeArguments()[0]; + many = true; + } + + String elementName = ModelSupport.getElementName(field); + + if (ModelSupport.isChoiceElement(modelClass, elementName)) { + Set> choiceElementTypes = ModelSupport.getChoiceElementTypes(modelClass, elementName); + ElementDefinition elementDefinition = getElementDefinition(structureDefinition, modelClass, elementName + "[x]"); + String description = elementDefinition.getDefinition().getValue(); + Integer min = elementDefinition.getMin() != null ? elementDefinition.getMin().getValue() : null; + for (Class choiceType : choiceElementTypes) { + if (FHIROpenApiGenerator.isApplicableForClass(choiceType, modelClass)) { + String choiceElementName = ModelSupport.getChoiceElementName(elementName, choiceType); + generateProperty(structureDefinition, modelClass, field, properties, choiceElementName, choiceType, many, description, min); + } + } + } else { + ElementDefinition elementDefinition = getElementDefinition(structureDefinition, modelClass, elementName); + String description = elementDefinition.getDefinition().getValue(); + Integer min = elementDefinition.getMin() != null ? elementDefinition.getMin().getValue() : null; + generateProperty(structureDefinition, modelClass, field, properties, elementName, (Class)fieldType, many, description, min); + } + } + + private static void generateProperty(StructureDefinition structureDefinition, Class modelClass, Field field, + JsonObjectBuilder properties, String elementName, Class fieldClass, boolean many, String description, Integer min) + throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException { + + JsonObjectBuilder property = factory.createObjectBuilder(); + + // Convert all the primitive types to json types. + if (isEnumerationWrapperClass(fieldClass)) { + property.add("type", "string"); + JsonArrayBuilder constants = factory.createArrayBuilder(); + Class enumClass = Class.forName(fieldClass.getName() + "$ValueSet"); + for (Object constant : enumClass.getEnumConstants()) { + Method method = constant.getClass().getMethod("value"); + String value = (String) method.invoke(constant); + constants.add(value); + } + property.add("enum", constants); + } else if (org.linuxforhealth.fhir.model.type.String.class.isAssignableFrom(fieldClass)) { + property.add("type", "string"); + if (org.linuxforhealth.fhir.model.type.Code.class.isAssignableFrom(fieldClass)) { + property.add("pattern", "[^\\s]+(\\s[^\\s]+)*"); + } else if (org.linuxforhealth.fhir.model.type.Id.class.equals(fieldClass)) { + property.add("pattern", "[A-Za-z0-9\\-\\.]{1,64}"); + } else { + property.add("pattern", "[ \\r\\n\\t\\S]+"); + } + } else if (org.linuxforhealth.fhir.model.type.Uri.class.isAssignableFrom(fieldClass)) { + property.add("type", "string"); + if (Oid.class.equals(fieldClass)) { + property.add("pattern", "urn:oid:[0-2](\\.(0|[1-9][0-9]*))+"); + } else if (Uuid.class.equals(fieldClass)) { + property.add("pattern", "urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } else { + property.add("pattern", "\\S*"); + } + } else if (org.linuxforhealth.fhir.model.type.Boolean.class.equals(fieldClass)) { + property.add("type", "boolean"); + } else if (org.linuxforhealth.fhir.model.type.Instant.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern", "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)-(0[1-9]" + + "|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]" + + "|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00))"); + } else if (org.linuxforhealth.fhir.model.type.Time.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?"); + } else if (org.linuxforhealth.fhir.model.type.Date.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)" + + "|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1]))?)?"); + } else if (org.linuxforhealth.fhir.model.type.DateTime.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]" + + "|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]" + + "|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?"); + } else if (org.linuxforhealth.fhir.model.type.Decimal.class.equals(fieldClass)) { + property.add("type", "number"); + } else if (org.linuxforhealth.fhir.model.type.Integer.class.isAssignableFrom(fieldClass)) { + property.add("type", "integer"); + property.add("format", "int32"); + } else if (org.linuxforhealth.fhir.model.type.Base64Binary.class.equals(fieldClass)) { + property.add("type", "string"); + property.add("pattern","(\\s*([0-9a-zA-Z\\+/=]){4}\\s*)+"); + } else if (String.class.equals(fieldClass)) { + property.add("type", "string"); + if ("id".equals(elementName)) { + property.add("pattern", "[A-Za-z0-9\\-\\.]{1,64}"); + } else if ("url".equals(elementName)) { + property.add("pattern", "\\S*"); + } + } else if (org.linuxforhealth.fhir.model.type.Xhtml.class.equals(fieldClass)) { + property.add("type", "string"); + } else { + // For non-primitives, point to the corresponding definition + property.add("$ref", "#/definitions/" + getSimpleNameWithEnclosingNames(fieldClass)); + } + + if (description != null) { + property.add("description", description); + } + + // Include select examples to help tools avoid bumping into infinite recursion (if they try generate examples) + JsonStructure example = null; + if (Element.class.equals(modelClass) && Extension.class.equals(fieldClass)) { + // "example": [{"url":"http://example.com","valueString":"textValue"}] + JsonObject obj = factory.createObjectBuilder() + .add("url", "http://example.com") + .add("valueString", "text value") + .build(); + example = factory.createArrayBuilder().add(obj).build(); + } else if (Identifier.class.equals(modelClass) && Reference.class.equals(fieldClass)) { + // "example": {"reference":"Organization/123","type":"Organization","display":"The Assigning Organization"} + example = factory.createObjectBuilder() + .add("reference", "Organization/123") + .add("type", "Organization") + .add("display", "The Assigning Organization") + // skip assigner to break the recursion + .build(); + } + + if (many) { + JsonObjectBuilder wrapper = factory.createObjectBuilder(); + wrapper.add("type", "array"); + wrapper.add("items", property); + if (min != null && min > 0) { + wrapper.add("minItems", min.intValue()); + } + if (example != null) { + wrapper.add("example", example); + } + properties.add(elementName, wrapper); + } else { + if (example != null) { + property.add("example", example); + } + properties.add(elementName, property); + } + } + + /** + * Returns the ElementDefinition for the given elementName in the Type represented by modelClass + */ + private static ElementDefinition getElementDefinition(StructureDefinition structureDefinition, Class modelClass, String elementName) { + String structureDefinitionName = structureDefinition.getName().getValue(); + String path = structureDefinitionName; + + String pathEnding = elementName; + if (BackboneElement.class.isAssignableFrom(modelClass) && !BackboneElement.class.equals(modelClass) && modelClass.isMemberClass()) { + String modelClassName = modelClass.getSimpleName(); + modelClassName = modelClassName.substring(0, 1).toLowerCase() + modelClassName.substring(1); + + if (Character.isDigit(modelClassName.charAt(modelClassName.length() - 1))) { + modelClassName = modelClassName.substring(0, modelClassName.length() - 1); + } + + path += "." + modelClassName; + pathEnding = modelClassName + "." + elementName; + } + + path += "." + elementName; + + for (ElementDefinition elementDefinition : structureDefinition.getDifferential().getElement()) { + String elementDefinitionPath = elementDefinition.getPath().getValue(); + if (elementDefinitionPath.equals(path) || (elementDefinitionPath.startsWith(structureDefinitionName) + && elementDefinitionPath.endsWith(pathEnding))) { + return elementDefinition; + } + } + + throw new RuntimeException("Unable to retrieve element definition for " + elementName + " in " + modelClass.getName()); + } + + private static Set getCompartmentClassNames(String compartment) { + try { + return compartmentHelper.getCompartmentResourceTypes(compartment); + } catch (FHIRSearchException e) { + return Collections.emptySet(); + } + } + + private static boolean isEnumerationWrapperClass(Class type) { + try { + Class.forName(type.getName() + "$ValueSet"); + return true; + } catch (Exception e) { + // do nothing + } + return false; + } + + private static class Filter { + private Map> filterMap = null; + + public Filter(Map> filterMap) { + this.filterMap = filterMap; + } + + /** + * @return true if the operation is enables for the specified resourceType, otherwise false + */ + public boolean acceptResourceType(Class resourceType) { + return filterMap.containsKey(resourceType.getSimpleName()); + } + + /** + * @return true if one or more of the resources in the filterMap includes the passed operation, otherwise false + */ + public boolean acceptOperation(Class resourceType, String operation) { + List interactions = filterMap.get(resourceType.getSimpleName()); + return interactions != null && interactions.contains(operation); + } + + public boolean acceptOperation(String operation) { + for (String resourceType : filterMap.keySet()) { + List operationList = filterMap.get(resourceType); + if (operationList.contains(operation)) { + return true; + } + } + return false; + } + } + + private static Filter createFilter(String filterString) { + return new Filter(parseFilterString(filterString)); + } + + private static Map> parseFilterString(String filterString) { + Map> filterMap = new HashMap>(); + for (String component : filterString.split(";")) { + String resourceType = component.substring(0, component.indexOf("(")); + String operations = component.substring(component.indexOf("(") + 1, component.indexOf(")")); + List operationList = new ArrayList(); + for (String operation : operations.split(",")) { + operationList.add(operation); + } + filterMap.put(resourceType, operationList); + } + return filterMap; + } + + private static Filter createAcceptAllFilter() throws Exception { + return new Filter(buildAcceptAllFilterMap()); + } + + // build filter map for all domain resources and operations + private static Map> buildAcceptAllFilterMap() throws Exception { + Map> filterMap = new HashMap>(); + for (String className : FHIROpenApiGenerator.getClassNames()) { + Class modelClass = Class.forName(RESOURCEPACKAGENAME + "." + className); + if (Resource.class == modelClass) { + filterMap.put(className, List.of("search", "history", "batch", "transaction", "export")); + } else if (DomainResource.class.isAssignableFrom(modelClass)) { + String resourceType = className; + // TODO: add patch + List operationList = Arrays.asList("create", "read", "vread", "update", "delete", "search", + "history", "export", "everything"); + filterMap.put(resourceType, operationList); + } + } + return filterMap; + } + + private static String getArticle(Class modelClass) { + List prefixes = Arrays.asList("A", "E", "I", "O", "Un"); + for (String prefix : prefixes) { + if (modelClass.getSimpleName().startsWith(prefix)) { + return " an "; + } + } + return " a "; + } +}