001/* 002 * #%L 003 * HAPI FHIR - Server Framework 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.server.provider; 021 022import ca.uhn.fhir.context.FhirContext; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.context.support.IValidationSupport; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.model.primitive.InstantDt; 028import ca.uhn.fhir.parser.DataFormatException; 029import ca.uhn.fhir.rest.annotation.IdParam; 030import ca.uhn.fhir.rest.annotation.Metadata; 031import ca.uhn.fhir.rest.annotation.Read; 032import ca.uhn.fhir.rest.api.Constants; 033import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 034import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 035import ca.uhn.fhir.rest.api.server.RequestDetails; 036import ca.uhn.fhir.rest.server.Bindings; 037import ca.uhn.fhir.rest.server.IServerConformanceProvider; 038import ca.uhn.fhir.rest.server.RestfulServer; 039import ca.uhn.fhir.rest.server.RestfulServerConfiguration; 040import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 041import ca.uhn.fhir.rest.server.method.BaseMethodBinding; 042import ca.uhn.fhir.rest.server.method.IParameter; 043import ca.uhn.fhir.rest.server.method.OperationMethodBinding; 044import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; 045import ca.uhn.fhir.rest.server.method.OperationParameter; 046import ca.uhn.fhir.rest.server.method.SearchMethodBinding; 047import ca.uhn.fhir.rest.server.method.SearchParameter; 048import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; 049import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 050import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 051import ca.uhn.fhir.util.ExtensionUtil; 052import ca.uhn.fhir.util.FhirTerser; 053import ca.uhn.fhir.util.HapiExtensions; 054import com.google.common.collect.TreeMultimap; 055import jakarta.annotation.Nonnull; 056import jakarta.servlet.ServletContext; 057import jakarta.servlet.http.HttpServletRequest; 058import org.apache.commons.text.WordUtils; 059import org.hl7.fhir.instance.model.api.IBase; 060import org.hl7.fhir.instance.model.api.IBaseConformance; 061import org.hl7.fhir.instance.model.api.IBaseExtension; 062import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 063import org.hl7.fhir.instance.model.api.IBaseResource; 064import org.hl7.fhir.instance.model.api.IIdType; 065import org.hl7.fhir.instance.model.api.IPrimitiveType; 066import org.slf4j.Logger; 067import org.slf4j.LoggerFactory; 068 069import java.util.Date; 070import java.util.HashMap; 071import java.util.HashSet; 072import java.util.List; 073import java.util.Map; 074import java.util.Map.Entry; 075import java.util.NavigableSet; 076import java.util.Set; 077import java.util.TreeSet; 078import java.util.UUID; 079import java.util.stream.Collectors; 080 081import static org.apache.commons.lang3.StringUtils.defaultString; 082import static org.apache.commons.lang3.StringUtils.isBlank; 083import static org.apache.commons.lang3.StringUtils.isNotBlank; 084 085/** 086 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation 087 * <p> 088 * This class is version independent, but will only work on servers supporting FHIR R4+ (as this was 089 * the first FHIR release where CapabilityStatement was a normative resource) 090 */ 091public class ServerCapabilityStatementProvider implements IServerConformanceProvider<IBaseConformance> { 092 093 public static final boolean DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED = true; 094 private static final Logger ourLog = LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); 095 private final FhirContext myContext; 096 private final RestfulServer myServer; 097 private final ISearchParamRegistry mySearchParamRegistry; 098 private final RestfulServerConfiguration myServerConfiguration; 099 private final IValidationSupport myValidationSupport; 100 private String myPublisher = "Not provided"; 101 private boolean myRestResourceRevIncludesEnabled = DEFAULT_REST_RESOURCE_REV_INCLUDES_ENABLED; 102 private HashMap<String, String> operationCanonicalUrlToId = new HashMap<>(); 103 /** 104 * Constructor 105 */ 106 public ServerCapabilityStatementProvider(RestfulServer theServer) { 107 myServer = theServer; 108 myContext = theServer.getFhirContext(); 109 mySearchParamRegistry = null; 110 myServerConfiguration = null; 111 myValidationSupport = null; 112 } 113 114 /** 115 * Constructor 116 */ 117 public ServerCapabilityStatementProvider( 118 FhirContext theContext, RestfulServerConfiguration theServerConfiguration) { 119 myContext = theContext; 120 myServerConfiguration = theServerConfiguration; 121 mySearchParamRegistry = null; 122 myServer = null; 123 myValidationSupport = null; 124 } 125 126 /** 127 * Constructor 128 */ 129 public ServerCapabilityStatementProvider( 130 RestfulServer theRestfulServer, 131 ISearchParamRegistry theSearchParamRegistry, 132 IValidationSupport theValidationSupport) { 133 myContext = theRestfulServer.getFhirContext(); 134 mySearchParamRegistry = theSearchParamRegistry; 135 myServer = theRestfulServer; 136 myServerConfiguration = null; 137 myValidationSupport = theValidationSupport; 138 } 139 140 private void checkBindingForSystemOps( 141 FhirTerser theTerser, IBase theRest, Set<String> theSystemOps, BaseMethodBinding theMethodBinding) { 142 RestOperationTypeEnum restOperationType = theMethodBinding.getRestOperationType(); 143 if (restOperationType.isSystemLevel()) { 144 String sysOp = restOperationType.getCode(); 145 if (theSystemOps.contains(sysOp) == false) { 146 theSystemOps.add(sysOp); 147 IBase interaction = theTerser.addElement(theRest, "interaction"); 148 theTerser.addElement(interaction, "code", sysOp); 149 } 150 } 151 } 152 153 private String conformanceDate(RestfulServerConfiguration theServerConfiguration) { 154 IPrimitiveType<Date> buildDate = theServerConfiguration.getConformanceDate(); 155 if (buildDate != null && buildDate.getValue() != null) { 156 try { 157 return buildDate.getValueAsString(); 158 } catch (DataFormatException e) { 159 // fall through 160 } 161 } 162 return InstantDt.withCurrentTime().getValueAsString(); 163 } 164 165 private RestfulServerConfiguration getServerConfiguration() { 166 if (myServer != null) { 167 return myServer.createConfiguration(); 168 } 169 return myServerConfiguration; 170 } 171 172 /** 173 * Gets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The 174 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 175 */ 176 public String getPublisher() { 177 return myPublisher; 178 } 179 180 /** 181 * Sets the value of the "publisher" that will be placed in the generated conformance statement. As this is a mandatory element, the value should not be null (although this is not enforced). The 182 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 183 */ 184 public void setPublisher(String thePublisher) { 185 myPublisher = thePublisher; 186 } 187 188 @Override 189 @Metadata 190 public IBaseConformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { 191 192 HttpServletRequest servletRequest = null; 193 if (theRequestDetails instanceof ServletRequestDetails) { 194 servletRequest = ((ServletRequestDetails) theRequestDetails).getServletRequest(); 195 } 196 197 RestfulServerConfiguration configuration = getServerConfiguration(); 198 Bindings bindings = configuration.provideBindings(); 199 200 IBaseConformance retVal = (IBaseConformance) 201 myContext.getResourceDefinition("CapabilityStatement").newInstance(); 202 203 FhirTerser terser = myContext.newTerser(); 204 205 TreeMultimap<String, String> resourceTypeToSupportedProfiles = getSupportedProfileMultimap(terser); 206 207 terser.addElement(retVal, "id", UUID.randomUUID().toString()); 208 terser.addElement(retVal, "name", "RestServer"); 209 terser.addElement(retVal, "publisher", myPublisher); 210 terser.addElement(retVal, "date", conformanceDate(configuration)); 211 terser.addElement( 212 retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); 213 214 ServletContext servletContext = (ServletContext) 215 (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); 216 String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); 217 terser.addElement(retVal, "implementation.url", serverBase); 218 terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); 219 terser.addElement(retVal, "kind", "instance"); 220 if (myServer != null && isNotBlank(myServer.getCopyright())) { 221 terser.addElement(retVal, "copyright", myServer.getCopyright()); 222 } 223 terser.addElement(retVal, "software.name", configuration.getServerName()); 224 terser.addElement(retVal, "software.version", configuration.getServerVersion()); 225 if (myContext.isFormatXmlSupported()) { 226 terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); 227 terser.addElement(retVal, "format", Constants.FORMAT_XML); 228 } 229 if (myContext.isFormatJsonSupported()) { 230 terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); 231 terser.addElement(retVal, "format", Constants.FORMAT_JSON); 232 } 233 if (myContext.isFormatRdfSupported()) { 234 terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); 235 terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); 236 } 237 terser.addElement(retVal, "status", "active"); 238 239 IBase rest = terser.addElement(retVal, "rest"); 240 terser.addElement(rest, "mode", "server"); 241 242 Set<String> systemOps = new HashSet<>(); 243 244 Map<String, List<BaseMethodBinding>> resourceToMethods = configuration.collectMethodBindings(); 245 Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = 246 configuration.getNameToSharedSupertype(); 247 List<BaseMethodBinding> globalMethodBindings = configuration.getGlobalBindings(); 248 249 TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create(); 250 TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create(); 251 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 252 String resourceName = nextEntry.getKey(); 253 for (BaseMethodBinding nextMethod : nextEntry.getValue()) { 254 if (nextMethod instanceof SearchMethodBinding) { 255 resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); 256 resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); 257 } 258 } 259 } 260 261 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 262 263 Set<String> operationNames = new HashSet<>(); 264 String resourceName = nextEntry.getKey(); 265 if (nextEntry.getKey().isEmpty() == false) { 266 Set<String> resourceOps = new HashSet<>(); 267 IBase resource = terser.addElement(rest, "resource"); 268 269 postProcessRestResource(terser, resource, resourceName); 270 271 RuntimeResourceDefinition def; 272 FhirContext context = configuration.getFhirContext(); 273 if (resourceNameToSharedSupertype.containsKey(resourceName)) { 274 def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); 275 } else { 276 def = context.getResourceDefinition(resourceName); 277 } 278 terser.addElement(resource, "type", def.getName()); 279 terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); 280 281 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 282 RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); 283 if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { 284 String resOp; 285 resOp = resOpCode.getCode(); 286 if (resourceOps.contains(resOp) == false) { 287 resourceOps.add(resOp); 288 IBase interaction = terser.addElement(resource, "interaction"); 289 terser.addElement(interaction, "code", resOp); 290 } 291 if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { 292 // vread implies read 293 resOp = "read"; 294 if (resourceOps.contains(resOp) == false) { 295 resourceOps.add(resOp); 296 IBase interaction = terser.addElement(resource, "interaction"); 297 terser.addElement(interaction, "code", resOp); 298 } 299 } 300 } 301 302 if (nextMethodBinding.isSupportsConditional()) { 303 switch (resOpCode) { 304 case CREATE: 305 terser.setElement(resource, "conditionalCreate", "true"); 306 break; 307 case DELETE: 308 if (nextMethodBinding.isSupportsConditionalMultiple()) { 309 terser.setElement(resource, "conditionalDelete", "multiple"); 310 } else { 311 terser.setElement(resource, "conditionalDelete", "single"); 312 } 313 break; 314 case UPDATE: 315 terser.setElement(resource, "conditionalUpdate", "true"); 316 break; 317 case HISTORY_INSTANCE: 318 case HISTORY_SYSTEM: 319 case HISTORY_TYPE: 320 case READ: 321 case SEARCH_SYSTEM: 322 case SEARCH_TYPE: 323 case TRANSACTION: 324 case VALIDATE: 325 case VREAD: 326 case METADATA: 327 case META_ADD: 328 case META: 329 case META_DELETE: 330 case PATCH: 331 case BATCH: 332 case ADD_TAGS: 333 case DELETE_TAGS: 334 case GET_TAGS: 335 case GET_PAGE: 336 case GRAPHQL_REQUEST: 337 case EXTENDED_OPERATION_SERVER: 338 case EXTENDED_OPERATION_TYPE: 339 case EXTENDED_OPERATION_INSTANCE: 340 default: 341 break; 342 } 343 } 344 345 checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); 346 347 // Resource Operations 348 if (nextMethodBinding instanceof SearchMethodBinding) { 349 addSearchMethodIfSearchIsNamedQuery( 350 theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding) 351 nextMethodBinding); 352 } else if (nextMethodBinding instanceof OperationMethodBinding) { 353 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 354 String opName = bindings.getOperationBindingToId().get(methodBinding); 355 // Only add each operation (by name) once 356 if (operationNames.add(opName)) { 357 IBase operation = terser.addElement(resource, "operation"); 358 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 359 } 360 } 361 } 362 363 // Find any global operations (Operations defines at the system level but with the 364 // global flag set to true, meaning they apply to all resource types) 365 if (globalMethodBindings != null) { 366 Set<String> globalOperationNames = new HashSet<>(); 367 for (BaseMethodBinding next : globalMethodBindings) { 368 if (next instanceof OperationMethodBinding) { 369 OperationMethodBinding methodBinding = (OperationMethodBinding) next; 370 if (methodBinding.isGlobalMethod()) { 371 if (methodBinding.isCanOperateAtInstanceLevel() 372 || methodBinding.isCanOperateAtTypeLevel()) { 373 String opName = 374 bindings.getOperationBindingToId().get(methodBinding); 375 // Only add each operation (by name) once 376 if (globalOperationNames.add(opName)) { 377 IBase operation = terser.addElement(resource, "operation"); 378 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 379 } 380 } 381 } 382 } 383 } 384 } 385 386 ISearchParamRegistry serverConfiguration; 387 if (myServerConfiguration != null) { 388 serverConfiguration = myServerConfiguration; 389 } else { 390 serverConfiguration = myServer.createConfiguration(); 391 } 392 393 /* 394 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, 395 * but also fill in any gaps using params from the server itself. This makes sure we include 396 * global params like _lastUpdated 397 */ 398 ResourceSearchParams searchParams; 399 ISearchParamRegistry searchParamRegistry; 400 ResourceSearchParams serverConfigurationActiveSearchParams = 401 serverConfiguration.getActiveSearchParams(resourceName); 402 if (mySearchParamRegistry != null) { 403 searchParamRegistry = mySearchParamRegistry; 404 searchParams = mySearchParamRegistry 405 .getActiveSearchParams(resourceName) 406 .makeCopy(); 407 for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) { 408 if (nextBuiltInSpName.startsWith("_") 409 && !searchParams.containsParamName(nextBuiltInSpName) 410 && searchParamEnabled(nextBuiltInSpName)) { 411 searchParams.put( 412 nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName)); 413 } 414 } 415 } else { 416 searchParamRegistry = serverConfiguration; 417 searchParams = serverConfigurationActiveSearchParams; 418 } 419 420 for (RuntimeSearchParam next : searchParams.values()) { 421 IBase searchParam = terser.addElement(resource, "searchParam"); 422 terser.addElement(searchParam, "name", next.getName()); 423 terser.addElement(searchParam, "type", next.getParamType().getCode()); 424 if (isNotBlank(next.getDescription())) { 425 terser.addElement(searchParam, "documentation", next.getDescription()); 426 } 427 428 String spUri = next.getUri(); 429 430 if (isNotBlank(spUri)) { 431 terser.addElement(searchParam, "definition", spUri); 432 } 433 } 434 435 // Add Include to CapabilityStatement.rest.resource 436 NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName); 437 if (resourceIncludes.isEmpty()) { 438 List<String> includes = searchParams.values().stream() 439 .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) 440 .map(t -> resourceName + ":" + t.getName()) 441 .sorted() 442 .collect(Collectors.toList()); 443 terser.addElement(resource, "searchInclude", "*"); 444 for (String nextInclude : includes) { 445 terser.addElement(resource, "searchInclude", nextInclude); 446 } 447 } else { 448 for (String resourceInclude : resourceIncludes) { 449 terser.addElement(resource, "searchInclude", resourceInclude); 450 } 451 } 452 453 // Add RevInclude to CapabilityStatement.rest.resource 454 if (myRestResourceRevIncludesEnabled) { 455 NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); 456 if (resourceRevIncludes.isEmpty()) { 457 TreeSet<String> revIncludes = new TreeSet<>(); 458 for (String nextResourceName : resourceToMethods.keySet()) { 459 if (isBlank(nextResourceName)) { 460 continue; 461 } 462 463 for (RuntimeSearchParam t : searchParamRegistry 464 .getActiveSearchParams(nextResourceName) 465 .values()) { 466 if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 467 if (isNotBlank(t.getName())) { 468 boolean appropriateTarget = false; 469 if (t.getTargets().contains(resourceName) 470 || t.getTargets().isEmpty()) { 471 appropriateTarget = true; 472 } 473 474 if (appropriateTarget) { 475 revIncludes.add(nextResourceName + ":" + t.getName()); 476 } 477 } 478 } 479 } 480 } 481 for (String nextInclude : revIncludes) { 482 terser.addElement(resource, "searchRevInclude", nextInclude); 483 } 484 } else { 485 for (String resourceInclude : resourceRevIncludes) { 486 terser.addElement(resource, "searchRevInclude", resourceInclude); 487 } 488 } 489 } 490 491 // Add SupportedProfile to CapabilityStatement.rest.resource 492 for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { 493 terser.addElement(resource, "supportedProfile", supportedProfile); 494 } 495 496 } else { 497 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 498 checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); 499 if (nextMethodBinding instanceof OperationMethodBinding) { 500 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 501 if (!methodBinding.isGlobalMethod()) { 502 String opName = bindings.getOperationBindingToId().get(methodBinding); 503 if (operationNames.add(opName)) { 504 ourLog.debug("Found bound operation: {}", opName); 505 IBase operation = terser.addElement(rest, "operation"); 506 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 507 } 508 } 509 } else if (nextMethodBinding instanceof SearchMethodBinding) { 510 addSearchMethodIfSearchIsNamedQuery( 511 theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) 512 nextMethodBinding); 513 } 514 } 515 } 516 } 517 518 // Find any global operations (Operations defines at the system level but with the 519 // global flag set to true, meaning they apply to all resource types) 520 if (globalMethodBindings != null) { 521 Set<String> globalOperationNames = new HashSet<>(); 522 for (BaseMethodBinding next : globalMethodBindings) { 523 if (next instanceof OperationMethodBinding) { 524 OperationMethodBinding methodBinding = (OperationMethodBinding) next; 525 if (methodBinding.isGlobalMethod()) { 526 if (methodBinding.isCanOperateAtServerLevel()) { 527 String opName = bindings.getOperationBindingToId().get(methodBinding); 528 // Only add each operation (by name) once 529 if (globalOperationNames.add(opName)) { 530 IBase operation = terser.addElement(rest, "operation"); 531 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 532 } 533 } 534 } 535 } 536 } 537 } 538 539 maybeAddBulkDataDeclarationToConformingToIg(terser, retVal, configuration.getServerBindings()); 540 541 postProcessRest(terser, rest); 542 postProcess(terser, retVal); 543 544 return retVal; 545 } 546 547 private void maybeAddBulkDataDeclarationToConformingToIg( 548 FhirTerser theTerser, IBaseConformance theBaseConformance, List<BaseMethodBinding> theServerBindings) { 549 boolean bulkExportEnabled = theServerBindings.stream() 550 .filter(OperationMethodBinding.class::isInstance) 551 .map(OperationMethodBinding.class::cast) 552 .map(OperationMethodBinding::getName) 553 .anyMatch(ProviderConstants.OPERATION_EXPORT::equals); 554 555 if (bulkExportEnabled) { 556 theTerser.addElement(theBaseConformance, "instantiates", Constants.BULK_DATA_ACCESS_IG_URL); 557 } 558 } 559 560 /** 561 * 562 * @param theSearchParam 563 * @return true if theSearchParam is enabled on this server 564 */ 565 protected boolean searchParamEnabled(String theSearchParam) { 566 return true; 567 } 568 569 private void addSearchMethodIfSearchIsNamedQuery( 570 RequestDetails theRequestDetails, 571 Bindings theBindings, 572 FhirTerser theTerser, 573 Set<String> theOperationNamesAlreadyAdded, 574 IBase theElementToAddTo, 575 SearchMethodBinding theSearchMethodBinding) { 576 if (theSearchMethodBinding.getQueryName() != null) { 577 String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding); 578 if (theOperationNamesAlreadyAdded.add(queryName)) { 579 IBase operation = theTerser.addElement(theElementToAddTo, "operation"); 580 theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName()); 581 theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName))); 582 } 583 } 584 } 585 586 private void populateOperation( 587 RequestDetails theRequestDetails, 588 FhirTerser theTerser, 589 OperationMethodBinding theMethodBinding, 590 String theOpName, 591 IBase theOperation) { 592 String operationName = theMethodBinding.getName().substring(1); 593 theTerser.addElement(theOperation, "name", operationName); 594 String operationCanonicalUrl = theMethodBinding.getCanonicalUrl(); 595 if (isNotBlank(operationCanonicalUrl)) { 596 theTerser.addElement(theOperation, "definition", operationCanonicalUrl); 597 operationCanonicalUrlToId.put(operationCanonicalUrl, theOpName); 598 } else { 599 theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName)); 600 } 601 if (isNotBlank(theMethodBinding.getDescription())) { 602 theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription()); 603 } 604 } 605 606 @Nonnull 607 private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) { 608 return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName; 609 } 610 611 private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) { 612 TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create(); 613 if (myValidationSupport != null) { 614 List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); 615 if (allStructureDefinitions != null) { 616 for (IBaseResource next : allStructureDefinitions) { 617 String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); 618 String url = terser.getSinglePrimitiveValueOrNull(next, "url"); 619 String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); 620 if ("resource".equals(kind) && isNotBlank(url)) { 621 622 // Don't include the base resource definitions in the supported profile list - This isn't 623 // helpful 624 if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") 625 || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { 626 continue; 627 } 628 629 String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); 630 if (isBlank(resourceType)) { 631 resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); 632 } 633 634 if (isNotBlank(resourceType)) { 635 resourceTypeToSupportedProfiles.put(resourceType, url); 636 } 637 } 638 } 639 } 640 } 641 return resourceTypeToSupportedProfiles; 642 } 643 644 /** 645 * Subclasses may override 646 */ 647 protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { 648 // nothing 649 } 650 651 /** 652 * Subclasses may override 653 */ 654 protected void postProcessRest(FhirTerser theTerser, IBase theRest) { 655 // nothing 656 } 657 658 /** 659 * Subclasses may override 660 */ 661 protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { 662 // nothing 663 } 664 665 protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { 666 if (theRequestDetails == null) { 667 return ""; 668 } 669 return theRequestDetails.getServerBaseForRequest() + "/"; 670 } 671 672 @Override 673 @Read(typeName = "OperationDefinition") 674 public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { 675 if (theId == null || theId.hasIdPart() == false) { 676 throw new ResourceNotFoundException(Msg.code(2245) + theId); 677 } 678 RestfulServerConfiguration configuration = getServerConfiguration(); 679 Bindings bindings = configuration.provideBindings(); 680 String operationId = getOperationId(theId); 681 List<OperationMethodBinding> operationBindings = 682 bindings.getOperationIdToBindings().get(operationId); 683 if (operationBindings != null && !operationBindings.isEmpty()) { 684 return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings); 685 } 686 687 List<SearchMethodBinding> searchBindings = 688 bindings.getSearchNameToBindings().get(theId.getIdPart()); 689 if (searchBindings != null && !searchBindings.isEmpty()) { 690 return readOperationDefinitionForNamedSearch(searchBindings); 691 } 692 throw new ResourceNotFoundException(Msg.code(2249) + theId); 693 } 694 695 private String getOperationId(IIdType theId) { 696 if (operationCanonicalUrlToId.get(theId.getValue()) != null) { 697 return operationCanonicalUrlToId.get(theId.getValue()); 698 } 699 return theId.getIdPart(); 700 } 701 702 private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { 703 IBaseResource op = 704 myContext.getResourceDefinition("OperationDefinition").newInstance(); 705 FhirTerser terser = myContext.newTerser(); 706 707 terser.addElement(op, "status", "active"); 708 terser.addElement(op, "kind", "query"); 709 terser.addElement(op, "affectsState", "false"); 710 711 terser.addElement(op, "instance", "false"); 712 713 Set<String> inParams = new HashSet<>(); 714 715 String operationCode = null; 716 for (SearchMethodBinding binding : bindings) { 717 if (isNotBlank(binding.getDescription())) { 718 terser.addElement(op, "description", binding.getDescription()); 719 } 720 if (isBlank(binding.getResourceProviderResourceName())) { 721 terser.addElement(op, "system", "true"); 722 terser.addElement(op, "type", "false"); 723 } else { 724 terser.addElement(op, "system", "false"); 725 terser.addElement(op, "type", "true"); 726 terser.addElement(op, "resource", binding.getResourceProviderResourceName()); 727 } 728 729 if (operationCode == null) { 730 operationCode = binding.getQueryName(); 731 } 732 733 for (IParameter nextParamUntyped : binding.getParameters()) { 734 if (nextParamUntyped instanceof SearchParameter) { 735 SearchParameter nextParam = (SearchParameter) nextParamUntyped; 736 if (!inParams.add(nextParam.getName())) { 737 continue; 738 } 739 740 IBase param = terser.addElement(op, "parameter"); 741 terser.addElement(param, "use", "in"); 742 terser.addElement(param, "type", "string"); 743 terser.addElement( 744 param, "searchType", nextParam.getParamType().getCode()); 745 terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); 746 terser.addElement(param, "max", "1"); 747 terser.addElement(param, "name", nextParam.getName()); 748 } 749 } 750 } 751 752 terser.addElement(op, "code", operationCode); 753 754 String operationName = WordUtils.capitalize(operationCode); 755 terser.addElement(op, "name", operationName); 756 757 return op; 758 } 759 760 private IBaseResource readOperationDefinitionForOperation( 761 RequestDetails theRequestDetails, 762 Bindings theBindings, 763 List<OperationMethodBinding> theOperationMethodBindings) { 764 IBaseResource op = 765 myContext.getResourceDefinition("OperationDefinition").newInstance(); 766 FhirTerser terser = myContext.newTerser(); 767 768 terser.addElement(op, "status", "active"); 769 terser.addElement(op, "kind", "operation"); 770 771 boolean systemLevel = false; 772 boolean typeLevel = false; 773 boolean instanceLevel = false; 774 boolean affectsState = false; 775 String description = null; 776 String title = null; 777 String code = null; 778 String url = null; 779 780 Set<String> resourceNames = new TreeSet<>(); 781 Map<String, IBase> inParams = new HashMap<>(); 782 Map<String, IBase> outParams = new HashMap<>(); 783 784 for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) { 785 if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) { 786 description = operationMethodBinding.getDescription(); 787 } 788 if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) { 789 title = operationMethodBinding.getShortDescription(); 790 } 791 if (operationMethodBinding.isCanOperateAtInstanceLevel()) { 792 instanceLevel = true; 793 } 794 if (operationMethodBinding.isCanOperateAtServerLevel()) { 795 systemLevel = true; 796 } 797 if (operationMethodBinding.isCanOperateAtTypeLevel()) { 798 typeLevel = true; 799 } 800 if (!operationMethodBinding.isIdempotent()) { 801 affectsState |= true; 802 } 803 804 code = operationMethodBinding.getName().substring(1); 805 806 if (isNotBlank(operationMethodBinding.getResourceName())) { 807 resourceNames.add(operationMethodBinding.getResourceName()); 808 } 809 810 if (isBlank(url)) { 811 url = theBindings.getOperationBindingToId().get(operationMethodBinding); 812 if (isNotBlank(url)) { 813 url = createOperationUrl(theRequestDetails, url); 814 } 815 } 816 817 for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) { 818 if (nextParamUntyped instanceof OperationParameter) { 819 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 820 821 IBase param = inParams.get(nextParam.getName()); 822 if (param == null) { 823 param = terser.addElement(op, "parameter"); 824 inParams.put(nextParam.getName(), param); 825 } 826 827 IBase existingParam = inParams.get(nextParam.getName()); 828 if (isNotBlank(nextParam.getDescription()) 829 && terser.getValues(existingParam, "documentation").isEmpty()) { 830 terser.addElement(existingParam, "documentation", nextParam.getDescription()); 831 } 832 833 if (nextParam.getParamType() != null) { 834 String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type"); 835 if (!nextParam.getParamType().equals(existingType)) { 836 if (existingType == null) { 837 terser.setElement(existingParam, "type", nextParam.getParamType()); 838 } else { 839 terser.setElement(existingParam, "type", "Resource"); 840 } 841 } 842 } 843 844 terser.setElement(param, "use", "in"); 845 if (nextParam.getSearchParamType() != null) { 846 terser.setElement(param, "searchType", nextParam.getSearchParamType()); 847 } 848 terser.setElement(param, "min", Integer.toString(nextParam.getMin())); 849 terser.setElement( 850 param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 851 terser.setElement(param, "name", nextParam.getName()); 852 853 List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl( 854 (IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); 855 Set<String> existingExamples = existingExampleExtensions.stream() 856 .map(t -> t.getValue()) 857 .filter(t -> t != null) 858 .map(t -> (IPrimitiveType<?>) t) 859 .map(t -> t.getValueAsString()) 860 .collect(Collectors.toSet()); 861 for (String nextExample : nextParam.getExampleValues()) { 862 if (!existingExamples.contains(nextExample)) { 863 ExtensionUtil.addExtension( 864 myContext, 865 param, 866 HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, 867 "string", 868 nextExample); 869 } 870 } 871 } 872 } 873 874 for (ReturnType nextParam : operationMethodBinding.getReturnParams()) { 875 if (outParams.containsKey(nextParam.getName())) { 876 continue; 877 } 878 879 IBase param = terser.addElement(op, "parameter"); 880 outParams.put(nextParam.getName(), param); 881 882 terser.addElement(param, "use", "out"); 883 if (nextParam.getType() != null) { 884 terser.addElement(param, "type", nextParam.getType()); 885 } 886 terser.addElement(param, "min", Integer.toString(nextParam.getMin())); 887 terser.addElement( 888 param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 889 terser.addElement(param, "name", nextParam.getName()); 890 } 891 } 892 String name = WordUtils.capitalize(code); 893 894 terser.addElements(op, "resource", resourceNames); 895 terser.addElement(op, "name", name); 896 terser.addElement(op, "url", url); 897 terser.addElement(op, "code", code); 898 terser.addElement(op, "description", description); 899 terser.addElement(op, "title", title); 900 terser.addElement(op, "affectsState", Boolean.toString(affectsState)); 901 terser.addElement(op, "system", Boolean.toString(systemLevel)); 902 terser.addElement(op, "type", Boolean.toString(typeLevel)); 903 terser.addElement(op, "instance", Boolean.toString(instanceLevel)); 904 905 return op; 906 } 907 908 @Override 909 public void setRestfulServer(RestfulServer theRestfulServer) { 910 // ignore 911 } 912 913 public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { 914 myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; 915 } 916}