All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.openapi.generator.FHIROpenApiGenerator Maven / Gradle / Ivy

/*
 * (C) Copyright IBM Corp. 2018, 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.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 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;

import com.ibm.fhir.core.FHIRMediaType;
import com.ibm.fhir.examples.ExamplesUtil;
import com.ibm.fhir.model.annotation.Required;
import com.ibm.fhir.model.format.Format;
import com.ibm.fhir.model.parser.FHIRParser;
import com.ibm.fhir.model.resource.ActivityDefinition;
import com.ibm.fhir.model.resource.Bundle;
import com.ibm.fhir.model.resource.Bundle.Entry;
import com.ibm.fhir.model.resource.CapabilityStatement;
import com.ibm.fhir.model.resource.DeviceDefinition;
import com.ibm.fhir.model.resource.DomainResource;
import com.ibm.fhir.model.resource.EventDefinition;
import com.ibm.fhir.model.resource.EvidenceVariable;
import com.ibm.fhir.model.resource.GuidanceResponse;
import com.ibm.fhir.model.resource.Library;
import com.ibm.fhir.model.resource.Measure;
import com.ibm.fhir.model.resource.MedicationDispense;
import com.ibm.fhir.model.resource.MedicationKnowledge;
import com.ibm.fhir.model.resource.MedicationRequest;
import com.ibm.fhir.model.resource.MedicationStatement;
import com.ibm.fhir.model.resource.MedicinalProductContraindication;
import com.ibm.fhir.model.resource.MedicinalProductIndication;
import com.ibm.fhir.model.resource.MedicinalProductManufactured;
import com.ibm.fhir.model.resource.MedicinalProductPackaged;
import com.ibm.fhir.model.resource.MedicinalProductUndesirableEffect;
import com.ibm.fhir.model.resource.OperationOutcome;
import com.ibm.fhir.model.resource.PlanDefinition;
import com.ibm.fhir.model.resource.RequestGroup;
import com.ibm.fhir.model.resource.ResearchElementDefinition;
import com.ibm.fhir.model.resource.Resource;
import com.ibm.fhir.model.resource.SearchParameter;
import com.ibm.fhir.model.resource.StructureDefinition;
import com.ibm.fhir.model.resource.StructureMap;
import com.ibm.fhir.model.resource.SubstancePolymer;
import com.ibm.fhir.model.resource.Task;
import com.ibm.fhir.model.type.BackboneElement;
import com.ibm.fhir.model.type.Code;
import com.ibm.fhir.model.type.ContactDetail;
import com.ibm.fhir.model.type.Contributor;
import com.ibm.fhir.model.type.DataRequirement;
import com.ibm.fhir.model.type.Dosage;
import com.ibm.fhir.model.type.Element;
import com.ibm.fhir.model.type.ElementDefinition;
import com.ibm.fhir.model.type.Expression;
import com.ibm.fhir.model.type.Extension;
import com.ibm.fhir.model.type.Identifier;
import com.ibm.fhir.model.type.MarketingStatus;
import com.ibm.fhir.model.type.Oid;
import com.ibm.fhir.model.type.ParameterDefinition;
import com.ibm.fhir.model.type.Population;
import com.ibm.fhir.model.type.ProdCharacteristic;
import com.ibm.fhir.model.type.ProductShelfLife;
import com.ibm.fhir.model.type.Reference;
import com.ibm.fhir.model.type.RelatedArtifact;
import com.ibm.fhir.model.type.SubstanceAmount;
import com.ibm.fhir.model.type.TriggerDefinition;
import com.ibm.fhir.model.type.UsageContext;
import com.ibm.fhir.model.type.Uuid;
import com.ibm.fhir.model.util.ModelSupport;
import com.ibm.fhir.model.visitor.AbstractVisitable;
import com.ibm.fhir.search.compartment.CompartmentUtil;
import com.ibm.fhir.search.exception.FHIRSearchException;
import com.ibm.fhir.search.util.SearchUtil;
import com.ibm.fhir.swagger.generator.APIConnectAdapter;

/**
 * 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 String TYPEPACKAGENAME = "com.ibm.fhir.model.type"; public static final String RESOURCEPACKAGENAME = "com.ibm.fhir.model.resource"; public static final String APPLICATION_FORM = "application/x-www-form-urlencoded"; 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); } 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); JsonArrayBuilder tags = factory.createArrayBuilder(); JsonObjectBuilder paths = factory.createObjectBuilder(); JsonObjectBuilder requestBodies = factory.createObjectBuilder(); JsonObjectBuilder definitions = factory.createObjectBuilder(); List classNames = getClassNames(); for (String resourceClassName : classNames) { 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. for (String innerClassName : getAllResourceInnerClasses()) { String parentClassName = innerClassName.split("\\$")[0]; if (resourceClassName.equals(parentClassName)) { Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); definitionsToAdd.add(innerModelClass); } } // 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(FHIROpenApiGenerator.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. for (String innerClassName : getAllResourceInnerClasses()) { String parentClassName = innerClassName.split("\\$")[0]; if (withinCompartmentResourceClassName.equals(parentClassName)) { Class innerModelClass = Class.forName(RESOURCEPACKAGENAME + "." + innerClassName); definitionsToAdd.add(innerModelClass); } } // 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); 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); 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. for (String innerClassName : 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 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); 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) { List 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(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", resourceModelClass.getSimpleName()); tags.add(tag); generateRequestBody(resourceModelClass, requestBodies); generateDefinition(resourceModelClass, definitions); // generate definition for all inner classes inside the top level resources. for (String innerClassName : 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 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); 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("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", CONTEXT_ROOT); 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 : 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 defined Types. for (String typeClassName : getAllTypesList()) { Class typeModelClass = Class.forName(TYPEPACKAGENAME + "." + typeClassName); if (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-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("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", CONTEXT_ROOT); 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 : 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 : 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-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 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 (SearchParameter searchParameter : SearchUtil.getApplicableSearchParameters(Resource.class.getSimpleName())) { JsonObjectBuilder parameter = factory.createObjectBuilder(); String name = searchParameter.getName().getValue(); 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 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); } // 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 { List searchParameters = new ArrayList( SearchUtil.getApplicableSearchParameters(modelClass.getSimpleName())); for (SearchParameter searchParameter : searchParameters) { JsonObjectBuilder parameter = factory.createObjectBuilder(); String name = searchParameter.getName().getValue(); 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); } 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); } 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("com.ibm.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/ibm/minimal/" + 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 (com.ibm.fhir.model.type.String.class.isAssignableFrom(fieldClass)) { property.add("type", "string"); if (com.ibm.fhir.model.type.Code.class.isAssignableFrom(fieldClass)) { property.add("pattern", "[^\\s]+(\\s[^\\s]+)*"); } else if (com.ibm.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 (com.ibm.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 (com.ibm.fhir.model.type.Boolean.class.equals(fieldClass)) { property.add("type", "boolean"); } else if (com.ibm.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 (com.ibm.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 (com.ibm.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 (com.ibm.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 (com.ibm.fhir.model.type.Decimal.class.equals(fieldClass)) { property.add("type", "number"); } else if (com.ibm.fhir.model.type.Integer.class.isAssignableFrom(fieldClass)) { property.add("type", "integer"); property.add("format", "int32"); } else if (com.ibm.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 (com.ibm.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() { return ModelSupport.getResourceTypes().stream() .map(r -> ModelSupport.getTypeName(r)) .collect(Collectors.toList()); } private static List getCompartmentClassNames(String compartment) { try { return CompartmentUtil.getCompartmentResourceTypes(compartment); } catch (FHIRSearchException e) { return Collections.emptyList(); } } 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) { return filterMap.get(resourceType.getSimpleName()).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 (DomainResource.class.isAssignableFrom(modelClass)) { String resourceType = className; // TODO: add patch List operationList = Arrays.asList("create", "read", "vread", "update", "delete", "search", "history", "batch", "transaction"); 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 (SubstanceAmount.class == typeModelClass || SubstanceAmount.class == typeModelClass.getEnclosingClass()) { if (SubstancePolymer.class != outerModelClass) { isApplicable = false; } } else if (ProdCharacteristic.class == typeModelClass || ProdCharacteristic.class == typeModelClass.getEnclosingClass()) { if (DeviceDefinition.class != outerModelClass && MedicinalProductManufactured.class != outerModelClass && MedicinalProductPackaged.class != outerModelClass) { isApplicable = false; } } else if (ProductShelfLife.class == typeModelClass || ProductShelfLife.class == typeModelClass.getEnclosingClass()) { if (DeviceDefinition.class != outerModelClass && MedicinalProductPackaged.class != outerModelClass) { isApplicable = false; } } else if (MarketingStatus.class == typeModelClass || MarketingStatus.class == typeModelClass.getEnclosingClass()) { if (DeviceDefinition.class != outerModelClass && MedicinalProductPackaged.class != outerModelClass) { isApplicable = false; } } else if (Population.class == typeModelClass || Population.class == typeModelClass.getEnclosingClass()) { if (MedicinalProductContraindication.class != outerModelClass && MedicinalProductIndication.class != outerModelClass && MedicinalProductUndesirableEffect.class != outerModelClass) { isApplicable = false; } } return isApplicable; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy