001/*- 002 * #%L 003 * hapi-fhir-server-openapi 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.rest.openapi; 021 022import ca.uhn.fhir.context.ConfigurationException; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.api.Hook; 027import ca.uhn.fhir.interceptor.api.Pointcut; 028import ca.uhn.fhir.rest.api.Constants; 029import ca.uhn.fhir.rest.server.IServerAddressStrategy; 030import ca.uhn.fhir.rest.server.IServerConformanceProvider; 031import ca.uhn.fhir.rest.server.RestfulServer; 032import ca.uhn.fhir.rest.server.RestfulServerUtils; 033import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 034import ca.uhn.fhir.util.ClasspathUtil; 035import ca.uhn.fhir.util.ExtensionConstants; 036import ca.uhn.fhir.util.HapiExtensions; 037import ca.uhn.fhir.util.UrlUtil; 038import com.google.common.collect.ImmutableList; 039import com.vladsch.flexmark.html.HtmlRenderer; 040import com.vladsch.flexmark.parser.Parser; 041import io.swagger.v3.core.util.Yaml; 042import io.swagger.v3.oas.models.Components; 043import io.swagger.v3.oas.models.OpenAPI; 044import io.swagger.v3.oas.models.Operation; 045import io.swagger.v3.oas.models.PathItem; 046import io.swagger.v3.oas.models.Paths; 047import io.swagger.v3.oas.models.examples.Example; 048import io.swagger.v3.oas.models.info.Contact; 049import io.swagger.v3.oas.models.info.Info; 050import io.swagger.v3.oas.models.media.Content; 051import io.swagger.v3.oas.models.media.DateSchema; 052import io.swagger.v3.oas.models.media.DateTimeSchema; 053import io.swagger.v3.oas.models.media.MediaType; 054import io.swagger.v3.oas.models.media.NumberSchema; 055import io.swagger.v3.oas.models.media.ObjectSchema; 056import io.swagger.v3.oas.models.media.Schema; 057import io.swagger.v3.oas.models.media.StringSchema; 058import io.swagger.v3.oas.models.parameters.Parameter; 059import io.swagger.v3.oas.models.parameters.RequestBody; 060import io.swagger.v3.oas.models.responses.ApiResponse; 061import io.swagger.v3.oas.models.responses.ApiResponses; 062import io.swagger.v3.oas.models.servers.Server; 063import io.swagger.v3.oas.models.tags.Tag; 064import jakarta.annotation.Nonnull; 065import jakarta.servlet.ServletContext; 066import jakarta.servlet.http.HttpServletRequest; 067import jakarta.servlet.http.HttpServletResponse; 068import org.apache.commons.io.IOUtils; 069import org.apache.commons.lang3.StringUtils; 070import org.hl7.fhir.convertors.factory.VersionConvertorFactory_30_40; 071import org.hl7.fhir.convertors.factory.VersionConvertorFactory_40_50; 072import org.hl7.fhir.convertors.factory.VersionConvertorFactory_43_50; 073import org.hl7.fhir.instance.model.api.IBaseConformance; 074import org.hl7.fhir.instance.model.api.IBaseResource; 075import org.hl7.fhir.instance.model.api.IPrimitiveType; 076import org.hl7.fhir.r4.model.CanonicalType; 077import org.hl7.fhir.r4.model.CapabilityStatement; 078import org.hl7.fhir.r4.model.CodeableConcept; 079import org.hl7.fhir.r4.model.Coding; 080import org.hl7.fhir.r4.model.DateType; 081import org.hl7.fhir.r4.model.Enumerations; 082import org.hl7.fhir.r4.model.Extension; 083import org.hl7.fhir.r4.model.IdType; 084import org.hl7.fhir.r4.model.OperationDefinition; 085import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent; 086import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; 087import org.hl7.fhir.r4.model.Parameters; 088import org.hl7.fhir.r4.model.Reference; 089import org.hl7.fhir.r4.model.Resource; 090import org.hl7.fhir.r4.model.StringType; 091import org.hl7.fhir.r4.model.Type; 092import org.hl7.fhir.r4.model.codesystems.DataTypes; 093import org.thymeleaf.IEngineConfiguration; 094import org.thymeleaf.TemplateEngine; 095import org.thymeleaf.cache.AlwaysValidCacheEntryValidity; 096import org.thymeleaf.cache.ICacheEntryValidity; 097import org.thymeleaf.context.IExpressionContext; 098import org.thymeleaf.context.WebContext; 099import org.thymeleaf.linkbuilder.AbstractLinkBuilder; 100import org.thymeleaf.standard.StandardDialect; 101import org.thymeleaf.templatemode.TemplateMode; 102import org.thymeleaf.templateresolver.ITemplateResolver; 103import org.thymeleaf.templateresolver.TemplateResolution; 104import org.thymeleaf.templateresource.ClassLoaderTemplateResource; 105import org.thymeleaf.web.servlet.IServletWebExchange; 106import org.thymeleaf.web.servlet.JakartaServletWebApplication; 107 108import java.io.IOException; 109import java.io.InputStream; 110import java.math.BigDecimal; 111import java.nio.charset.StandardCharsets; 112import java.util.ArrayList; 113import java.util.HashMap; 114import java.util.Iterator; 115import java.util.LinkedHashMap; 116import java.util.List; 117import java.util.Map; 118import java.util.Optional; 119import java.util.Properties; 120import java.util.Set; 121import java.util.function.Supplier; 122import java.util.stream.Collectors; 123 124import static ca.uhn.fhir.rest.server.util.NarrativeUtil.sanitizeHtmlFragment; 125import static org.apache.commons.lang3.ObjectUtils.defaultIfNull; 126import static org.apache.commons.lang3.StringUtils.defaultString; 127import static org.apache.commons.lang3.StringUtils.isBlank; 128import static org.apache.commons.lang3.StringUtils.isNotBlank; 129 130public class OpenApiInterceptor { 131 132 public static final String FHIR_JSON_RESOURCE = "FHIR-JSON-RESOURCE"; 133 public static final String FHIR_XML_RESOURCE = "FHIR-XML-RESOURCE"; 134 public static final String PAGE_SYSTEM = "System Level Operations"; 135 public static final String PAGE_ALL = "All"; 136 public static final FhirContext FHIR_CONTEXT_CANONICAL = FhirContext.forR4(); 137 public static final String REQUEST_DETAILS = "REQUEST_DETAILS"; 138 public static final String RACCOON_PNG = "raccoon.png"; 139 private final String mySwaggerUiVersion; 140 private final TemplateEngine myTemplateEngine; 141 private final Parser myFlexmarkParser; 142 private final HtmlRenderer myFlexmarkRenderer; 143 private final Map<String, String> myResourcePathToClasspath = new HashMap<>(); 144 private final Map<String, String> myExtensionToContentType = new HashMap<>(); 145 private String myBannerImage; 146 private String myCssText; 147 private boolean myUseResourcePages; 148 149 /** 150 * Constructor 151 */ 152 public OpenApiInterceptor() { 153 mySwaggerUiVersion = initSwaggerUiWebJar(); 154 155 myTemplateEngine = new TemplateEngine(); 156 ITemplateResolver resolver = new SwaggerUiTemplateResolver(); 157 myTemplateEngine.setTemplateResolver(resolver); 158 StandardDialect dialect = new StandardDialect(); 159 myTemplateEngine.setDialect(dialect); 160 161 myTemplateEngine.setLinkBuilder(new TemplateLinkBuilder()); 162 163 myFlexmarkParser = Parser.builder().build(); 164 myFlexmarkRenderer = HtmlRenderer.builder().build(); 165 166 initResources(); 167 } 168 169 private void initResources() { 170 setBannerImage(RACCOON_PNG); 171 setUseResourcePages(true); 172 173 addResourcePathToClasspath("/swagger-ui/index.html", "/ca/uhn/fhir/rest/openapi/index.html"); 174 addResourcePathToClasspath("/swagger-ui/" + RACCOON_PNG, "/ca/uhn/fhir/rest/openapi/raccoon.png"); 175 addResourcePathToClasspath("/swagger-ui/index.css", "/ca/uhn/fhir/rest/openapi/index.css"); 176 177 myExtensionToContentType.put(".png", "image/png"); 178 myExtensionToContentType.put(".css", "text/css; charset=UTF-8"); 179 } 180 181 protected void addResourcePathToClasspath(String thePath, String theClasspath) { 182 myResourcePathToClasspath.put(thePath, theClasspath); 183 } 184 185 private String initSwaggerUiWebJar() { 186 final String mySwaggerUiVersion; 187 Properties props = new Properties(); 188 String resourceName = "/META-INF/maven/org.webjars/swagger-ui/pom.properties"; 189 try { 190 InputStream resourceAsStream = ClasspathUtil.loadResourceAsStream(resourceName); 191 props.load(resourceAsStream); 192 } catch (IOException e) { 193 throw new ConfigurationException(Msg.code(239) + "Failed to load resource: " + resourceName); 194 } 195 mySwaggerUiVersion = props.getProperty("version"); 196 return mySwaggerUiVersion; 197 } 198 199 @Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLER_SELECTED) 200 public boolean serveSwaggerUi( 201 HttpServletRequest theRequest, HttpServletResponse theResponse, ServletRequestDetails theRequestDetails) 202 throws IOException { 203 String requestPath = theRequest.getPathInfo(); 204 String queryString = theRequest.getQueryString(); 205 206 if (isBlank(requestPath) || requestPath.equals("/")) { 207 if (isBlank(queryString)) { 208 Set<String> highestRankedAcceptValues = 209 RestfulServerUtils.parseAcceptHeaderAndReturnHighestRankedOptions(theRequest); 210 if (highestRankedAcceptValues.contains(Constants.CT_HTML)) { 211 212 String serverBase = "."; 213 if (theRequestDetails.getServletRequest() != null) { 214 IServerAddressStrategy addressStrategy = 215 theRequestDetails.getServer().getServerAddressStrategy(); 216 serverBase = addressStrategy.determineServerBase(theRequest.getServletContext(), theRequest); 217 } 218 String redirectUrl = theResponse.encodeRedirectURL(serverBase + "/swagger-ui/"); 219 theResponse.sendRedirect(redirectUrl); 220 theResponse.getWriter().close(); 221 return false; 222 } 223 } 224 225 return true; 226 } 227 228 if (requestPath.startsWith("/swagger-ui/")) { 229 230 return !handleResourceRequest(theResponse, theRequestDetails, requestPath); 231 232 } else if (requestPath.equals("/api-docs")) { 233 234 OpenAPI openApi = generateOpenApi(theRequestDetails); 235 String response = Yaml.pretty(openApi); 236 237 theResponse.setContentType("text/yaml"); 238 theResponse.setStatus(200); 239 theResponse.getWriter().write(response); 240 theResponse.getWriter().close(); 241 return false; 242 } 243 244 return true; 245 } 246 247 protected boolean handleResourceRequest( 248 HttpServletResponse theResponse, ServletRequestDetails theRequestDetails, String requestPath) 249 throws IOException { 250 if (requestPath.equals("/swagger-ui/") || requestPath.equals("/swagger-ui/index.html")) { 251 serveSwaggerUiHtml(theRequestDetails, theResponse); 252 return true; 253 } 254 255 String resourceClasspath = myResourcePathToClasspath.get(requestPath); 256 if (resourceClasspath != null) { 257 theResponse.setStatus(200); 258 259 String extension = requestPath.substring(requestPath.lastIndexOf('.')); 260 String contentType = myExtensionToContentType.get(extension); 261 assert contentType != null; 262 theResponse.setContentType(contentType); 263 try (InputStream resource = ClasspathUtil.loadResourceAsStream(resourceClasspath)) { 264 IOUtils.copy(resource, theResponse.getOutputStream()); 265 theResponse.getOutputStream().close(); 266 } 267 return true; 268 } 269 270 String resourcePath = requestPath.substring("/swagger-ui/".length()); 271 272 if (resourcePath.equals("swagger-ui-custom.css") && isNotBlank(myCssText)) { 273 theResponse.setContentType("text/css"); 274 theResponse.setStatus(200); 275 theResponse.getWriter().println(myCssText); 276 theResponse.getWriter().close(); 277 return true; 278 } 279 280 try (InputStream resource = ClasspathUtil.loadResourceAsStream( 281 "/META-INF/resources/webjars/swagger-ui/" + mySwaggerUiVersion + "/" + resourcePath)) { 282 283 if (resourcePath.endsWith(".js") || resourcePath.endsWith(".map")) { 284 theResponse.setContentType("application/javascript"); 285 theResponse.setStatus(200); 286 IOUtils.copy(resource, theResponse.getOutputStream()); 287 theResponse.getOutputStream().close(); 288 return true; 289 } 290 291 if (resourcePath.endsWith(".css")) { 292 theResponse.setContentType("text/css"); 293 theResponse.setStatus(200); 294 IOUtils.copy(resource, theResponse.getOutputStream()); 295 theResponse.getOutputStream().close(); 296 return true; 297 } 298 299 if (resourcePath.endsWith(".html")) { 300 theResponse.setContentType(Constants.CT_HTML); 301 theResponse.setStatus(200); 302 IOUtils.copy(resource, theResponse.getOutputStream()); 303 theResponse.getOutputStream().close(); 304 return true; 305 } 306 } 307 return false; 308 } 309 310 public String removeTrailingSlash(String theUrl) { 311 while (theUrl != null && theUrl.endsWith("/")) { 312 theUrl = theUrl.substring(0, theUrl.length() - 1); 313 } 314 return theUrl; 315 } 316 317 /** 318 * If supplied, this field can be used to provide additional CSS text that should 319 * be loaded by the swagger-ui page. The contents should be raw CSS text, e.g. 320 * <code> 321 * BODY { font-size: 1.1em; } 322 * </code> 323 */ 324 public String getCssText() { 325 return myCssText; 326 } 327 328 /** 329 * If supplied, this field can be used to provide additional CSS text that should 330 * be loaded by the swagger-ui page. The contents should be raw CSS text, e.g. 331 * <code> 332 * BODY { font-size: 1.1em; } 333 * </code> 334 */ 335 public void setCssText(String theCssText) { 336 myCssText = theCssText; 337 } 338 339 @SuppressWarnings("unchecked") 340 private void serveSwaggerUiHtml(ServletRequestDetails theRequestDetails, HttpServletResponse theResponse) 341 throws IOException { 342 CapabilityStatement cs = getCapabilityStatement(theRequestDetails); 343 String baseUrl = removeTrailingSlash(cs.getImplementation().getUrl()); 344 theResponse.setStatus(200); 345 theResponse.setContentType(Constants.CT_HTML); 346 347 HttpServletRequest servletRequest = theRequestDetails.getServletRequest(); 348 ServletContext servletContext = servletRequest.getServletContext(); 349 350 JakartaServletWebApplication application = JakartaServletWebApplication.buildApplication(servletContext); 351 IServletWebExchange exchange = application.buildExchange(servletRequest, theResponse); 352 WebContext context = new WebContext(exchange); 353 context.setVariable(REQUEST_DETAILS, theRequestDetails); 354 context.setVariable("DESCRIPTION", cs.getImplementation().getDescription()); 355 context.setVariable("SERVER_NAME", cs.getSoftware().getName()); 356 context.setVariable("SERVER_VERSION", cs.getSoftware().getVersion()); 357 context.setVariable("BASE_URL", cs.getImplementation().getUrl()); 358 context.setVariable("BANNER_IMAGE_URL", getBannerImage()); 359 context.setVariable("OPENAPI_DOCS", baseUrl + "/api-docs"); 360 context.setVariable("FHIR_VERSION", cs.getFhirVersion().toCode()); 361 context.setVariable("ADDITIONAL_CSS_TEXT", getCssText()); 362 context.setVariable("USE_RESOURCE_PAGES", isUseResourcePages()); 363 context.setVariable( 364 "FHIR_VERSION_CODENAME", 365 FhirVersionEnum.forVersionString(cs.getFhirVersion().toCode()).name()); 366 367 String copyright = cs.getCopyright(); 368 if (isNotBlank(copyright)) { 369 copyright = renderMarkdown(copyright); 370 context.setVariable("COPYRIGHT_HTML", copyright); 371 } 372 373 List<String> pageNames = new ArrayList<>(); 374 Map<String, Integer> resourceToCount = new HashMap<>(); 375 cs.getRestFirstRep().getResource().stream().forEach(t -> { 376 String type = t.getType(); 377 pageNames.add(type); 378 Extension countExtension = t.getExtensionByUrl(ExtensionConstants.CONF_RESOURCE_COUNT); 379 if (countExtension != null) { 380 IPrimitiveType<? extends Number> countExtensionValue = 381 (IPrimitiveType<? extends Number>) countExtension.getValueAsPrimitive(); 382 if (countExtensionValue != null && countExtensionValue.hasValue()) { 383 resourceToCount.put(type, countExtensionValue.getValue().intValue()); 384 } 385 } 386 }); 387 pageNames.sort((o1, o2) -> { 388 Integer count1 = resourceToCount.get(o1); 389 Integer count2 = resourceToCount.get(o2); 390 if (count1 != null && count2 != null) { 391 return count2 - count1; 392 } 393 if (count1 != null) { 394 return -1; 395 } 396 if (count2 != null) { 397 return 1; 398 } 399 return o1.compareTo(o2); 400 }); 401 402 pageNames.add(0, PAGE_ALL); 403 pageNames.add(1, PAGE_SYSTEM); 404 405 context.setVariable("PAGE_NAMES", pageNames); 406 context.setVariable("PAGE_NAME_TO_COUNT", resourceToCount); 407 408 String page; 409 if (isUseResourcePages()) { 410 page = extractPageName(theRequestDetails, PAGE_SYSTEM); 411 } else { 412 page = PAGE_ALL; 413 } 414 context.setVariable("PAGE", page); 415 416 populateOIDCVariables(theRequestDetails, context); 417 418 String outcome = myTemplateEngine.process("index.html", context); 419 420 theResponse.getWriter().write(outcome); 421 theResponse.getWriter().close(); 422 } 423 424 @Nonnull 425 private String renderMarkdown(String copyright) { 426 return myFlexmarkRenderer.render(myFlexmarkParser.parse(copyright)); 427 } 428 429 protected void populateOIDCVariables(ServletRequestDetails theRequestDetails, WebContext theContext) { 430 theContext.setVariable("OAUTH2_REDIRECT_URL_PROPERTY", ""); 431 } 432 433 private String extractPageName(ServletRequestDetails theRequestDetails, String theDefault) { 434 String[] pageValues = theRequestDetails.getParameters().get("page"); 435 String page = null; 436 if (pageValues != null && pageValues.length > 0) { 437 page = pageValues[0]; 438 } 439 if (isBlank(page)) { 440 page = theDefault; 441 } 442 return page; 443 } 444 445 protected OpenAPI generateOpenApi(ServletRequestDetails theRequestDetails) { 446 String page = extractPageName(theRequestDetails, null); 447 448 CapabilityStatement cs = getCapabilityStatement(theRequestDetails); 449 FhirContext ctx = theRequestDetails.getFhirContext(); 450 451 IServerConformanceProvider<?> capabilitiesProvider = null; 452 RestfulServer restfulServer = theRequestDetails.getServer(); 453 if (restfulServer.getServerConformanceProvider() instanceof IServerConformanceProvider) { 454 capabilitiesProvider = (IServerConformanceProvider<?>) restfulServer.getServerConformanceProvider(); 455 } 456 457 OpenAPI openApi = new OpenAPI(); 458 459 openApi.setInfo(new Info()); 460 openApi.getInfo().setDescription(cs.getDescription()); 461 openApi.getInfo().setTitle(cs.getSoftware().getName()); 462 openApi.getInfo().setVersion(cs.getSoftware().getVersion()); 463 openApi.getInfo().setContact(new Contact()); 464 openApi.getInfo().getContact().setName(cs.getContactFirstRep().getName()); 465 openApi.getInfo() 466 .getContact() 467 .setEmail(cs.getContactFirstRep().getTelecomFirstRep().getValue()); 468 469 Server server = new Server(); 470 openApi.addServersItem(server); 471 server.setUrl(cs.getImplementation().getUrl()); 472 server.setDescription(cs.getSoftware().getName()); 473 474 Paths paths = new Paths(); 475 openApi.setPaths(paths); 476 477 if (page == null || page.equals(PAGE_SYSTEM) || page.equals(PAGE_ALL)) { 478 Tag serverTag = new Tag(); 479 serverTag.setName(PAGE_SYSTEM); 480 serverTag.setDescription("Server-level operations"); 481 openApi.addTagsItem(serverTag); 482 483 Operation capabilitiesOperation = getPathItem(paths, "/metadata", PathItem.HttpMethod.GET); 484 capabilitiesOperation.addTagsItem(PAGE_SYSTEM); 485 capabilitiesOperation.setSummary("server-capabilities: Fetch the server FHIR CapabilityStatement"); 486 addFhirResourceResponse(ctx, openApi, capabilitiesOperation, "CapabilityStatement"); 487 488 Set<CapabilityStatement.SystemRestfulInteraction> systemInteractions = 489 cs.getRestFirstRep().getInteraction().stream() 490 .map(t -> t.getCode()) 491 .collect(Collectors.toSet()); 492 // Transaction Operation 493 if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.TRANSACTION) 494 || systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.BATCH)) { 495 Operation transaction = getPathItem(paths, "/", PathItem.HttpMethod.POST); 496 transaction.addTagsItem(PAGE_SYSTEM); 497 transaction.setSummary("server-transaction: Execute a FHIR Transaction (or FHIR Batch) Bundle"); 498 addFhirResourceResponse(ctx, openApi, transaction, null); 499 addFhirResourceRequestBody(openApi, transaction, ctx, null); 500 } 501 502 // System History Operation 503 if (systemInteractions.contains(CapabilityStatement.SystemRestfulInteraction.HISTORYSYSTEM)) { 504 Operation systemHistory = getPathItem(paths, "/_history", PathItem.HttpMethod.GET); 505 systemHistory.addTagsItem(PAGE_SYSTEM); 506 systemHistory.setSummary( 507 "server-history: Fetch the resource change history across all resource types on the server"); 508 addFhirResourceResponse(ctx, openApi, systemHistory, null); 509 } 510 511 // System-level Operations 512 for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : 513 cs.getRestFirstRep().getOperation()) { 514 addFhirOperation(ctx, openApi, theRequestDetails, capabilitiesProvider, paths, null, nextOperation); 515 } 516 } 517 518 for (CapabilityStatement.CapabilityStatementRestResourceComponent nextResource : 519 cs.getRestFirstRep().getResource()) { 520 String resourceType = nextResource.getType(); 521 522 if (page != null && !page.equals(resourceType) && !page.equals(PAGE_ALL)) { 523 continue; 524 } 525 526 Set<CapabilityStatement.TypeRestfulInteraction> typeRestfulInteractions = 527 nextResource.getInteraction().stream() 528 .map(t -> t.getCodeElement().getValue()) 529 .collect(Collectors.toSet()); 530 531 Tag resourceTag = new Tag(); 532 resourceTag.setName(resourceType); 533 resourceTag.setDescription(createResourceDescription(nextResource)); 534 openApi.addTagsItem(resourceTag); 535 536 // Instance Read 537 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.READ)) { 538 Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.GET); 539 operation.addTagsItem(resourceType); 540 operation.setSummary("read-instance: Read " + resourceType + " instance"); 541 addResourceIdParameter(operation); 542 addFhirResourceResponse(ctx, openApi, operation, null); 543 } 544 545 // Instance VRead 546 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.VREAD)) { 547 Operation operation = 548 getPathItem(paths, "/" + resourceType + "/{id}/_history/{version_id}", PathItem.HttpMethod.GET); 549 operation.addTagsItem(resourceType); 550 operation.setSummary("vread-instance: Read " + resourceType + " instance with specific version"); 551 addResourceIdParameter(operation); 552 addResourceVersionIdParameter(operation); 553 addFhirResourceResponse(ctx, openApi, operation, null); 554 } 555 556 // Type Create 557 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.CREATE)) { 558 Operation operation = getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.POST); 559 operation.addTagsItem(resourceType); 560 operation.setSummary("create-type: Create a new " + resourceType + " instance"); 561 addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType)); 562 addFhirResourceResponse(ctx, openApi, operation, null); 563 } 564 565 // Instance Update 566 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.UPDATE)) { 567 Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PUT); 568 operation.addTagsItem(resourceType); 569 operation.setSummary("update-instance: Update an existing " + resourceType 570 + " instance, or create using a client-assigned ID"); 571 addResourceIdParameter(operation); 572 addFhirResourceRequestBody(openApi, operation, ctx, genericExampleSupplier(ctx, resourceType)); 573 addFhirResourceResponse(ctx, openApi, operation, null); 574 } 575 576 // Type history 577 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) { 578 Operation operation = getPathItem(paths, "/" + resourceType + "/_history", PathItem.HttpMethod.GET); 579 operation.addTagsItem(resourceType); 580 operation.setSummary( 581 "type-history: Fetch the resource change history for all resources of type " + resourceType); 582 addFhirResourceResponse(ctx, openApi, operation, null); 583 } 584 585 // Instance history 586 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.HISTORYTYPE)) { 587 Operation operation = 588 getPathItem(paths, "/" + resourceType + "/{id}/_history", PathItem.HttpMethod.GET); 589 operation.addTagsItem(resourceType); 590 operation.setSummary("instance-history: Fetch the resource change history for all resources of type " 591 + resourceType); 592 addResourceIdParameter(operation); 593 addFhirResourceResponse(ctx, openApi, operation, null); 594 } 595 596 // Instance Patch 597 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.PATCH)) { 598 Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.PATCH); 599 operation.addTagsItem(resourceType); 600 operation.setSummary("instance-patch: Patch a resource instance of type " + resourceType + " by ID"); 601 addResourceIdParameter(operation); 602 addFhirResourceRequestBody(openApi, operation, FHIR_CONTEXT_CANONICAL, patchExampleSupplier()); 603 addFhirResourceResponse(ctx, openApi, operation, null); 604 } 605 606 // Instance Delete 607 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.DELETE)) { 608 Operation operation = getPathItem(paths, "/" + resourceType + "/{id}", PathItem.HttpMethod.DELETE); 609 operation.addTagsItem(resourceType); 610 operation.setSummary("instance-delete: Perform a logical delete on a resource instance"); 611 addResourceIdParameter(operation); 612 addFhirResourceResponse(ctx, openApi, operation, null); 613 } 614 615 // Search 616 if (typeRestfulInteractions.contains(CapabilityStatement.TypeRestfulInteraction.SEARCHTYPE)) { 617 addSearchOperation( 618 openApi, 619 getPathItem(paths, "/" + resourceType, PathItem.HttpMethod.GET), 620 ctx, 621 resourceType, 622 nextResource); 623 addSearchOperation( 624 openApi, 625 getPathItem(paths, "/" + resourceType + "/_search", PathItem.HttpMethod.GET), 626 ctx, 627 resourceType, 628 nextResource); 629 } 630 631 // Resource-level Operations 632 for (CapabilityStatement.CapabilityStatementRestResourceOperationComponent nextOperation : 633 nextResource.getOperation()) { 634 addFhirOperation( 635 ctx, openApi, theRequestDetails, capabilitiesProvider, paths, resourceType, nextOperation); 636 } 637 } 638 639 return openApi; 640 } 641 642 @Nonnull 643 protected String createResourceDescription( 644 CapabilityStatement.CapabilityStatementRestResourceComponent theResource) { 645 StringBuilder b = new StringBuilder(); 646 b.append("The ").append(theResource.getType()).append(" FHIR resource type"); 647 648 String documentation = theResource.getDocumentation(); 649 if (isNotBlank(documentation)) { 650 b.append("<br/>"); 651 b.append(sanitizeHtmlFragment(renderMarkdown(documentation))); 652 } 653 654 if (isNotBlank(theResource.getProfile())) { 655 b.append("<br/>"); 656 b.append("Base profile: "); 657 b.append(sanitizeHtmlFragment(theResource.getProfile())); 658 } 659 660 for (CanonicalType next : theResource.getSupportedProfile()) { 661 String nextSupportedProfile = next.getValueAsString(); 662 if (isNotBlank(nextSupportedProfile)) { 663 b.append("<br/>"); 664 b.append("Supported profile: "); 665 b.append(sanitizeHtmlFragment(nextSupportedProfile)); 666 } 667 } 668 669 return b.toString(); 670 } 671 672 protected void addSearchOperation( 673 final OpenAPI openApi, 674 final Operation operation, 675 final FhirContext ctx, 676 final String resourceType, 677 final CapabilityStatement.CapabilityStatementRestResourceComponent nextResource) { 678 operation.addTagsItem(resourceType); 679 operation.setDescription("This is a search type"); 680 operation.setSummary("search-type: Search for " + resourceType + " instances"); 681 addFhirResourceResponse(ctx, openApi, operation, null); 682 683 for (final CapabilityStatement.CapabilityStatementRestResourceSearchParamComponent nextSearchParam : 684 nextResource.getSearchParam()) { 685 final Parameter parametersItem = new Parameter(); 686 operation.addParametersItem(parametersItem); 687 688 parametersItem.setName(nextSearchParam.getName()); 689 parametersItem.setRequired(false); 690 parametersItem.setIn("query"); 691 parametersItem.setDescription(nextSearchParam.getDocumentation()); 692 parametersItem.setSchema(toSchema(nextSearchParam.getType())); 693 } 694 } 695 696 private Supplier<IBaseResource> patchExampleSupplier() { 697 return () -> { 698 Parameters example = new Parameters(); 699 Parameters.ParametersParameterComponent operation = 700 example.addParameter().setName("operation"); 701 operation.addPart().setName("type").setValue(new StringType("add")); 702 operation.addPart().setName("path").setValue(new StringType("Patient")); 703 operation.addPart().setName("name").setValue(new StringType("birthDate")); 704 operation.addPart().setName("value").setValue(new DateType("1930-01-01")); 705 return example; 706 }; 707 } 708 709 private void addSchemaFhirResource(OpenAPI theOpenApi) { 710 ensureComponentsSchemasPopulated(theOpenApi); 711 712 if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_JSON_RESOURCE)) { 713 ObjectSchema fhirJsonSchema = new ObjectSchema(); 714 fhirJsonSchema.setDescription("A FHIR resource"); 715 theOpenApi.getComponents().addSchemas(FHIR_JSON_RESOURCE, fhirJsonSchema); 716 } 717 718 if (!theOpenApi.getComponents().getSchemas().containsKey(FHIR_XML_RESOURCE)) { 719 ObjectSchema fhirXmlSchema = new ObjectSchema(); 720 fhirXmlSchema.setDescription("A FHIR resource"); 721 theOpenApi.getComponents().addSchemas(FHIR_XML_RESOURCE, fhirXmlSchema); 722 } 723 } 724 725 private void ensureComponentsSchemasPopulated(OpenAPI theOpenApi) { 726 if (theOpenApi.getComponents() == null) { 727 theOpenApi.setComponents(new Components()); 728 } 729 if (theOpenApi.getComponents().getSchemas() == null) { 730 theOpenApi.getComponents().setSchemas(new LinkedHashMap<>()); 731 } 732 } 733 734 private CapabilityStatement getCapabilityStatement(ServletRequestDetails theRequestDetails) { 735 RestfulServer restfulServer = theRequestDetails.getServer(); 736 IBaseConformance versionIndependentCapabilityStatement = 737 restfulServer.getCapabilityStatement(theRequestDetails); 738 return toCanonicalVersion(versionIndependentCapabilityStatement); 739 } 740 741 private void addFhirOperation( 742 FhirContext theFhirContext, 743 OpenAPI theOpenApi, 744 ServletRequestDetails theRequestDetails, 745 IServerConformanceProvider<?> theCapabilitiesProvider, 746 Paths thePaths, 747 String theResourceType, 748 CapabilityStatement.CapabilityStatementRestResourceOperationComponent theOperation) { 749 if (theCapabilitiesProvider != null) { 750 IdType definitionId = new IdType(theOperation.getDefinition()); 751 IBaseResource operationDefinitionNonCanonical = 752 theCapabilitiesProvider.readOperationDefinition(definitionId, theRequestDetails); 753 if (operationDefinitionNonCanonical == null) { 754 return; 755 } 756 757 OperationDefinition operationDefinition = toCanonicalVersion(operationDefinitionNonCanonical); 758 final boolean postOnly = operationDefinition.getAffectsState() 759 || operationDefinition.getParameter().stream() 760 .filter(p -> p.getUse().equals(OperationParameterUse.IN)) 761 .anyMatch(p -> { 762 final boolean required = p.getMin() > 0; 763 return required && !isPrimitive(p); 764 }); 765 766 if (!postOnly) { 767 768 // GET form for non-state-affecting operations 769 if (theResourceType != null) { 770 if (operationDefinition.getType()) { 771 Operation operation = getPathItem( 772 thePaths, 773 "/" + theResourceType + "/$" + operationDefinition.getCode(), 774 PathItem.HttpMethod.GET); 775 populateOperation( 776 theFhirContext, 777 theOpenApi, 778 theResourceType, 779 operationDefinition, 780 operation, 781 "/" + theResourceType + "/$" + operationDefinition.getCode(), 782 PathItem.HttpMethod.GET); 783 } 784 if (operationDefinition.getInstance()) { 785 Operation operation = getPathItem( 786 thePaths, 787 "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), 788 PathItem.HttpMethod.GET); 789 addResourceIdParameter(operation); 790 populateOperation( 791 theFhirContext, 792 theOpenApi, 793 theResourceType, 794 operationDefinition, 795 operation, 796 "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), 797 PathItem.HttpMethod.GET); 798 } 799 } else { 800 if (operationDefinition.getSystem()) { 801 Operation operation = 802 getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.GET); 803 populateOperation( 804 theFhirContext, 805 theOpenApi, 806 null, 807 operationDefinition, 808 operation, 809 "/$" + operationDefinition.getCode(), 810 PathItem.HttpMethod.GET); 811 } 812 } 813 } 814 815 // POST form for all operations 816 if (theResourceType != null) { 817 if (operationDefinition.getType()) { 818 Operation operation = getPathItem( 819 thePaths, 820 "/" + theResourceType + "/$" + operationDefinition.getCode(), 821 PathItem.HttpMethod.POST); 822 populateOperation( 823 theFhirContext, 824 theOpenApi, 825 theResourceType, 826 operationDefinition, 827 operation, 828 "/" + theResourceType + "/$" + operationDefinition.getCode(), 829 PathItem.HttpMethod.POST); 830 } 831 if (operationDefinition.getInstance()) { 832 Operation operation = getPathItem( 833 thePaths, 834 "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), 835 PathItem.HttpMethod.POST); 836 addResourceIdParameter(operation); 837 populateOperation( 838 theFhirContext, 839 theOpenApi, 840 theResourceType, 841 operationDefinition, 842 operation, 843 "/" + theResourceType + "/{id}/$" + operationDefinition.getCode(), 844 PathItem.HttpMethod.POST); 845 } 846 } else { 847 if (operationDefinition.getSystem()) { 848 Operation operation = 849 getPathItem(thePaths, "/$" + operationDefinition.getCode(), PathItem.HttpMethod.POST); 850 populateOperation( 851 theFhirContext, 852 theOpenApi, 853 null, 854 operationDefinition, 855 operation, 856 "/$" + operationDefinition.getCode(), 857 PathItem.HttpMethod.POST); 858 } 859 } 860 } 861 } 862 863 private static final List<String> primitiveTypes = List.of( 864 DataTypes.BOOLEAN.toCode(), 865 DataTypes.INTEGER.toCode(), 866 DataTypes.STRING.toCode(), 867 DataTypes.DECIMAL.toCode(), 868 DataTypes.URI.toCode(), 869 DataTypes.URL.toCode(), 870 DataTypes.CANONICAL.toCode(), 871 DataTypes.REFERENCE.toCode(), 872 DataTypes.BASE64BINARY.toCode(), 873 DataTypes.INSTANT.toCode(), 874 DataTypes.DATE.toCode(), 875 DataTypes.DATETIME.toCode(), 876 DataTypes.TIME.toCode(), 877 DataTypes.CODE.toCode(), 878 DataTypes.CODING.toCode(), 879 DataTypes.OID.toCode(), 880 DataTypes.ID.toCode(), 881 DataTypes.MARKDOWN.toCode(), 882 DataTypes.UNSIGNEDINT.toCode(), 883 DataTypes.POSITIVEINT.toCode(), 884 DataTypes.UUID.toCode()); 885 886 private static boolean isPrimitive(OperationDefinitionParameterComponent parameter) { 887 return parameter.getType() != null && primitiveTypes.contains(parameter.getType()); 888 } 889 890 private void populateOperation( 891 FhirContext theFhirContext, 892 OpenAPI theOpenApi, 893 String theResourceType, 894 OperationDefinition theOperationDefinition, 895 Operation theOperation, 896 String thePath, 897 PathItem.HttpMethod httpMethod) { 898 if (theResourceType == null) { 899 theOperation.addTagsItem(PAGE_SYSTEM); 900 } else { 901 theOperation.addTagsItem(theResourceType); 902 } 903 theOperation.setSummary(Optional.ofNullable(theOperationDefinition.getTitle()) 904 .orElse(String.format("%s: %s", httpMethod.name(), thePath))); 905 theOperation.setDescription(theOperationDefinition.getDescription()); 906 addFhirResourceResponse(theFhirContext, theOpenApi, theOperation, null); 907 if (httpMethod == PathItem.HttpMethod.GET) { 908 909 for (OperationDefinition.OperationDefinitionParameterComponent nextParameter : 910 theOperationDefinition.getParameter()) { 911 if ("0".equals(nextParameter.getMax()) 912 || !nextParameter.getUse().equals(OperationParameterUse.IN)) { 913 continue; 914 } 915 if (!isPrimitive(nextParameter) && nextParameter.getMin() == 0) { 916 continue; 917 } 918 Parameter parametersItem = new Parameter(); 919 theOperation.addParametersItem(parametersItem); 920 921 parametersItem.setName(nextParameter.getName()); 922 parametersItem.setIn("query"); 923 parametersItem.setDescription(nextParameter.getDocumentation()); 924 parametersItem.setRequired(nextParameter.getMin() > 0); 925 parametersItem.setSchema(toSchema(nextParameter.getSearchType())); 926 927 List<Extension> exampleExtensions = 928 nextParameter.getExtensionsByUrl(HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); 929 if (exampleExtensions.size() == 1) { 930 parametersItem.setExample( 931 exampleExtensions.get(0).getValueAsPrimitive().getValueAsString()); 932 } else if (exampleExtensions.size() > 1) { 933 for (Extension next : exampleExtensions) { 934 String nextExample = next.getValueAsPrimitive().getValueAsString(); 935 parametersItem.addExample(nextExample, new Example().value(nextExample)); 936 } 937 } 938 } 939 940 } else { 941 942 Parameters exampleRequestBody = new Parameters(); 943 for (OperationDefinition.OperationDefinitionParameterComponent nextSearchParam : 944 theOperationDefinition.getParameter()) { 945 if ("0".equals(nextSearchParam.getMax()) 946 || !nextSearchParam.getUse().equals(OperationParameterUse.IN)) { 947 continue; 948 } 949 Parameters.ParametersParameterComponent param = exampleRequestBody.addParameter(); 950 param.setName(nextSearchParam.getName()); 951 String paramType = nextSearchParam.getType(); 952 switch (defaultString(paramType)) { 953 case "uri": 954 case "url": 955 case "code": 956 case "string": { 957 IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL 958 .getElementDefinition(paramType) 959 .newInstance(); 960 type.setValueAsString("example"); 961 param.setValue((Type) type); 962 break; 963 } 964 case "integer": { 965 IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL 966 .getElementDefinition(paramType) 967 .newInstance(); 968 type.setValueAsString("0"); 969 param.setValue((Type) type); 970 break; 971 } 972 case "boolean": { 973 IPrimitiveType<?> type = (IPrimitiveType<?>) FHIR_CONTEXT_CANONICAL 974 .getElementDefinition(paramType) 975 .newInstance(); 976 type.setValueAsString("false"); 977 param.setValue((Type) type); 978 break; 979 } 980 case "CodeableConcept": { 981 CodeableConcept type = new CodeableConcept(); 982 type.getCodingFirstRep().setSystem("http://example.com"); 983 type.getCodingFirstRep().setCode("1234"); 984 param.setValue(type); 985 break; 986 } 987 case "Coding": { 988 Coding type = new Coding(); 989 type.setSystem("http://example.com"); 990 type.setCode("1234"); 991 param.setValue(type); 992 break; 993 } 994 case "Reference": 995 Reference reference = new Reference("example"); 996 param.setValue(reference); 997 break; 998 case "Resource": 999 if (theResourceType != null) { 1000 if (FHIR_CONTEXT_CANONICAL.getResourceTypes().contains(theResourceType)) { 1001 IBaseResource resource = FHIR_CONTEXT_CANONICAL 1002 .getResourceDefinition(theResourceType) 1003 .newInstance(); 1004 resource.setId("1"); 1005 param.setResource((Resource) resource); 1006 } 1007 } 1008 break; 1009 } 1010 } 1011 1012 String exampleRequestBodyString = FHIR_CONTEXT_CANONICAL 1013 .newJsonParser() 1014 .setPrettyPrint(true) 1015 .encodeResourceToString(exampleRequestBody); 1016 theOperation.setRequestBody(new RequestBody()); 1017 theOperation.getRequestBody().setContent(new Content()); 1018 MediaType mediaType = new MediaType(); 1019 mediaType.setExample(exampleRequestBodyString); 1020 mediaType.setSchema(new Schema().type("object").title("FHIR Resource")); 1021 theOperation.getRequestBody().getContent().addMediaType(Constants.CT_FHIR_JSON_NEW, mediaType); 1022 } 1023 } 1024 1025 protected Operation getPathItem(Paths thePaths, String thePath, PathItem.HttpMethod theMethod) { 1026 PathItem pathItem; 1027 1028 if (thePaths.containsKey(thePath)) { 1029 pathItem = thePaths.get(thePath); 1030 } else { 1031 pathItem = new PathItem(); 1032 thePaths.addPathItem(thePath, pathItem); 1033 } 1034 1035 switch (theMethod) { 1036 case POST: 1037 assert pathItem.getPost() == null : "Have duplicate POST at path: " + thePath; 1038 return pathItem.post(new Operation()).getPost(); 1039 case GET: 1040 assert pathItem.getGet() == null : "Have duplicate GET at path: " + thePath; 1041 return pathItem.get(new Operation()).getGet(); 1042 case PUT: 1043 assert pathItem.getPut() == null; 1044 return pathItem.put(new Operation()).getPut(); 1045 case PATCH: 1046 assert pathItem.getPatch() == null; 1047 return pathItem.patch(new Operation()).getPatch(); 1048 case DELETE: 1049 assert pathItem.getDelete() == null; 1050 return pathItem.delete(new Operation()).getDelete(); 1051 case HEAD: 1052 case OPTIONS: 1053 case TRACE: 1054 default: 1055 throw new IllegalStateException(Msg.code(240)); 1056 } 1057 } 1058 1059 private void addFhirResourceRequestBody( 1060 OpenAPI theOpenApi, 1061 Operation theOperation, 1062 FhirContext theExampleFhirContext, 1063 Supplier<IBaseResource> theExampleSupplier) { 1064 RequestBody requestBody = new RequestBody(); 1065 requestBody.setContent(provideContentFhirResource(theOpenApi, theExampleFhirContext, theExampleSupplier)); 1066 theOperation.setRequestBody(requestBody); 1067 } 1068 1069 private void addResourceVersionIdParameter(Operation theOperation) { 1070 Parameter parameter = new Parameter(); 1071 parameter.setName("version_id"); 1072 parameter.setIn("path"); 1073 parameter.setDescription("The resource version ID"); 1074 parameter.setExample("1"); 1075 parameter.setRequired(true); 1076 parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); 1077 parameter.setStyle(Parameter.StyleEnum.SIMPLE); 1078 theOperation.addParametersItem(parameter); 1079 } 1080 1081 private void addFhirResourceResponse( 1082 FhirContext theFhirContext, OpenAPI theOpenApi, Operation theOperation, String theResourceType) { 1083 theOperation.setResponses(new ApiResponses()); 1084 ApiResponse response200 = new ApiResponse(); 1085 response200.setDescription("Success"); 1086 response200.setContent(provideContentFhirResource( 1087 theOpenApi, theFhirContext, genericExampleSupplier(theFhirContext, theResourceType))); 1088 theOperation.getResponses().addApiResponse("200", response200); 1089 } 1090 1091 private Supplier<IBaseResource> genericExampleSupplier(FhirContext theFhirContext, String theResourceType) { 1092 if (theResourceType == null) { 1093 return null; 1094 } 1095 return () -> { 1096 IBaseResource example = null; 1097 if (theResourceType != null && theFhirContext.getResourceTypes().contains(theResourceType)) { 1098 example = theFhirContext.getResourceDefinition(theResourceType).newInstance(); 1099 } 1100 return example; 1101 }; 1102 } 1103 1104 private Content provideContentFhirResource( 1105 OpenAPI theOpenApi, FhirContext theExampleFhirContext, Supplier<IBaseResource> theExampleSupplier) { 1106 addSchemaFhirResource(theOpenApi); 1107 Content retVal = new Content(); 1108 1109 MediaType jsonSchema = 1110 new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_JSON_RESOURCE)); 1111 if (theExampleSupplier != null) { 1112 jsonSchema.setExample(theExampleFhirContext 1113 .newJsonParser() 1114 .setPrettyPrint(true) 1115 .encodeResourceToString(theExampleSupplier.get())); 1116 } 1117 retVal.addMediaType(Constants.CT_FHIR_JSON_NEW, jsonSchema); 1118 1119 MediaType xmlSchema = 1120 new MediaType().schema(new ObjectSchema().$ref("#/components/schemas/" + FHIR_XML_RESOURCE)); 1121 if (theExampleSupplier != null) { 1122 xmlSchema.setExample(theExampleFhirContext 1123 .newXmlParser() 1124 .setPrettyPrint(true) 1125 .encodeResourceToString(theExampleSupplier.get())); 1126 } 1127 retVal.addMediaType(Constants.CT_FHIR_XML_NEW, xmlSchema); 1128 return retVal; 1129 } 1130 1131 private void addResourceIdParameter(Operation theOperation) { 1132 Parameter parameter = new Parameter(); 1133 parameter.setName("id"); 1134 parameter.setIn("path"); 1135 parameter.setDescription("The resource ID"); 1136 parameter.setExample("123"); 1137 parameter.setRequired(true); 1138 parameter.setSchema(new Schema().type("string").minimum(new BigDecimal(1))); 1139 parameter.setStyle(Parameter.StyleEnum.SIMPLE); 1140 theOperation.addParametersItem(parameter); 1141 } 1142 1143 protected ClassLoaderTemplateResource getIndexTemplate() { 1144 return new ClassLoaderTemplateResource( 1145 myResourcePathToClasspath.get("/swagger-ui/index.html"), StandardCharsets.UTF_8.name()); 1146 } 1147 1148 public String getBannerImage() { 1149 return myBannerImage; 1150 } 1151 1152 public OpenApiInterceptor setBannerImage(String theBannerImage) { 1153 myBannerImage = StringUtils.defaultIfBlank(theBannerImage, null); 1154 return this; 1155 } 1156 1157 public boolean isUseResourcePages() { 1158 return myUseResourcePages; 1159 } 1160 1161 public void setUseResourcePages(boolean theUseResourcePages) { 1162 myUseResourcePages = theUseResourcePages; 1163 } 1164 1165 @SuppressWarnings("unchecked") 1166 private static <T extends Resource> T toCanonicalVersion(IBaseResource theNonCanonical) { 1167 IBaseResource canonical; 1168 if (theNonCanonical instanceof org.hl7.fhir.dstu3.model.Resource) { 1169 canonical = 1170 VersionConvertorFactory_30_40.convertResource((org.hl7.fhir.dstu3.model.Resource) theNonCanonical); 1171 } else if (theNonCanonical instanceof org.hl7.fhir.r5.model.Resource) { 1172 canonical = VersionConvertorFactory_40_50.convertResource((org.hl7.fhir.r5.model.Resource) theNonCanonical); 1173 } else if (theNonCanonical instanceof org.hl7.fhir.r4b.model.Resource) { 1174 org.hl7.fhir.r5.model.Resource r5 = 1175 VersionConvertorFactory_43_50.convertResource((org.hl7.fhir.r4b.model.Resource) theNonCanonical); 1176 canonical = VersionConvertorFactory_40_50.convertResource(r5); 1177 } else { 1178 canonical = theNonCanonical; 1179 } 1180 return (T) canonical; 1181 } 1182 1183 private class SwaggerUiTemplateResolver implements ITemplateResolver { 1184 @Override 1185 public String getName() { 1186 return getClass().getName(); 1187 } 1188 1189 @Override 1190 public Integer getOrder() { 1191 return 0; 1192 } 1193 1194 @Override 1195 public TemplateResolution resolveTemplate( 1196 IEngineConfiguration configuration, 1197 String ownerTemplate, 1198 String template, 1199 Map<String, Object> templateResolutionAttributes) { 1200 ClassLoaderTemplateResource resource = getIndexTemplate(); 1201 ICacheEntryValidity cacheValidity = new AlwaysValidCacheEntryValidity(); 1202 return new TemplateResolution(resource, TemplateMode.HTML, cacheValidity); 1203 } 1204 } 1205 1206 private static class TemplateLinkBuilder extends AbstractLinkBuilder { 1207 1208 @Override 1209 public String buildLink( 1210 IExpressionContext theExpressionContext, String theBase, Map<String, Object> theParameters) { 1211 1212 ServletRequestDetails requestDetails = 1213 (ServletRequestDetails) theExpressionContext.getVariable(REQUEST_DETAILS); 1214 1215 IServerAddressStrategy addressStrategy = requestDetails.getServer().getServerAddressStrategy(); 1216 String baseUrl = addressStrategy.determineServerBase( 1217 requestDetails.getServletRequest().getServletContext(), requestDetails.getServletRequest()); 1218 1219 StringBuilder builder = new StringBuilder(); 1220 builder.append(baseUrl); 1221 builder.append(theBase); 1222 if (!theParameters.isEmpty()) { 1223 builder.append("?"); 1224 for (Iterator<Map.Entry<String, Object>> iter = 1225 theParameters.entrySet().iterator(); 1226 iter.hasNext(); ) { 1227 Map.Entry<String, Object> nextEntry = iter.next(); 1228 builder.append(UrlUtil.escapeUrlParam(nextEntry.getKey())); 1229 builder.append("="); 1230 builder.append(UrlUtil.escapeUrlParam( 1231 defaultIfNull(nextEntry.getValue(), "").toString())); 1232 if (iter.hasNext()) { 1233 builder.append("&"); 1234 } 1235 } 1236 } 1237 1238 return builder.toString(); 1239 } 1240 } 1241 1242 private Schema<?> toSchema(Enumerations.SearchParamType type) { 1243 if (type == null) { 1244 return new StringSchema(); 1245 } 1246 switch (type) { 1247 case NUMBER: 1248 return new NumberSchema(); 1249 case DATE: 1250 Schema<?> dateSchema = new Schema<>(); 1251 dateSchema.anyOf(ImmutableList.of(new DateTimeSchema(), new DateSchema())); 1252 return dateSchema; 1253 case QUANTITY: 1254 Schema<?> quantitySchema = new Schema<>(); 1255 quantitySchema.anyOf(ImmutableList.of(new StringSchema(), new NumberSchema())); 1256 return quantitySchema; 1257 case STRING: 1258 case TOKEN: 1259 case REFERENCE: 1260 case COMPOSITE: 1261 case URI: 1262 case SPECIAL: 1263 case NULL: 1264 default: 1265 return new StringSchema(); 1266 } 1267 } 1268}