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 IBase text = terser.addElement(retVal, "text"); 210 terser.addElement(text, "status", "generated"); 211 terser.addElement( 212 text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">" + configuration.getServerName() + "</div>"); 213 terser.addElement(retVal, "publisher", myPublisher); 214 terser.addElement(retVal, "date", conformanceDate(configuration)); 215 terser.addElement( 216 retVal, "fhirVersion", myContext.getVersion().getVersion().getFhirVersionString()); 217 218 ServletContext servletContext = (ServletContext) 219 (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); 220 String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); 221 terser.addElement(retVal, "implementation.url", serverBase); 222 terser.addElement(retVal, "implementation.description", configuration.getImplementationDescription()); 223 terser.addElement(retVal, "kind", "instance"); 224 if (myServer != null && isNotBlank(myServer.getCopyright())) { 225 terser.addElement(retVal, "copyright", myServer.getCopyright()); 226 } 227 terser.addElement(retVal, "software.name", configuration.getServerName()); 228 terser.addElement(retVal, "software.version", configuration.getServerVersion()); 229 if (myContext.isFormatXmlSupported()) { 230 terser.addElement(retVal, "format", Constants.CT_FHIR_XML_NEW); 231 terser.addElement(retVal, "format", Constants.FORMAT_XML); 232 } 233 if (myContext.isFormatJsonSupported()) { 234 terser.addElement(retVal, "format", Constants.CT_FHIR_JSON_NEW); 235 terser.addElement(retVal, "format", Constants.FORMAT_JSON); 236 } 237 if (myContext.isFormatRdfSupported()) { 238 terser.addElement(retVal, "format", Constants.CT_RDF_TURTLE); 239 terser.addElement(retVal, "format", Constants.FORMAT_TURTLE); 240 } 241 terser.addElement(retVal, "status", "active"); 242 243 IBase rest = terser.addElement(retVal, "rest"); 244 terser.addElement(rest, "mode", "server"); 245 246 Set<String> systemOps = new HashSet<>(); 247 248 Map<String, List<BaseMethodBinding>> resourceToMethods = configuration.collectMethodBindings(); 249 Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype = 250 configuration.getNameToSharedSupertype(); 251 List<BaseMethodBinding> globalMethodBindings = configuration.getGlobalBindings(); 252 253 TreeMultimap<String, String> resourceNameToIncludes = TreeMultimap.create(); 254 TreeMultimap<String, String> resourceNameToRevIncludes = TreeMultimap.create(); 255 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 256 String resourceName = nextEntry.getKey(); 257 for (BaseMethodBinding nextMethod : nextEntry.getValue()) { 258 if (nextMethod instanceof SearchMethodBinding) { 259 resourceNameToIncludes.putAll(resourceName, nextMethod.getIncludes()); 260 resourceNameToRevIncludes.putAll(resourceName, nextMethod.getRevIncludes()); 261 } 262 } 263 } 264 265 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 266 267 Set<String> operationNames = new HashSet<>(); 268 String resourceName = nextEntry.getKey(); 269 if (nextEntry.getKey().isEmpty() == false) { 270 Set<String> resourceOps = new HashSet<>(); 271 IBase resource = terser.addElement(rest, "resource"); 272 273 postProcessRestResource(terser, resource, resourceName); 274 275 RuntimeResourceDefinition def; 276 FhirContext context = configuration.getFhirContext(); 277 if (resourceNameToSharedSupertype.containsKey(resourceName)) { 278 def = context.getResourceDefinition(resourceNameToSharedSupertype.get(resourceName)); 279 } else { 280 def = context.getResourceDefinition(resourceName); 281 } 282 terser.addElement(resource, "type", def.getName()); 283 terser.addElement(resource, "profile", def.getResourceProfile(serverBase)); 284 285 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 286 RestOperationTypeEnum resOpCode = nextMethodBinding.getRestOperationType(); 287 if (resOpCode.isTypeLevel() || resOpCode.isInstanceLevel()) { 288 String resOp; 289 resOp = resOpCode.getCode(); 290 if (resourceOps.contains(resOp) == false) { 291 resourceOps.add(resOp); 292 IBase interaction = terser.addElement(resource, "interaction"); 293 terser.addElement(interaction, "code", resOp); 294 } 295 if (RestOperationTypeEnum.VREAD.equals(resOpCode)) { 296 // vread implies read 297 resOp = "read"; 298 if (resourceOps.contains(resOp) == false) { 299 resourceOps.add(resOp); 300 IBase interaction = terser.addElement(resource, "interaction"); 301 terser.addElement(interaction, "code", resOp); 302 } 303 } 304 } 305 306 if (nextMethodBinding.isSupportsConditional()) { 307 switch (resOpCode) { 308 case CREATE: 309 terser.setElement(resource, "conditionalCreate", "true"); 310 break; 311 case DELETE: 312 if (nextMethodBinding.isSupportsConditionalMultiple()) { 313 terser.setElement(resource, "conditionalDelete", "multiple"); 314 } else { 315 terser.setElement(resource, "conditionalDelete", "single"); 316 } 317 break; 318 case UPDATE: 319 terser.setElement(resource, "conditionalUpdate", "true"); 320 break; 321 case UPDATE_REWRITE_HISTORY: 322 case HISTORY_INSTANCE: 323 case HISTORY_SYSTEM: 324 case HISTORY_TYPE: 325 case READ: 326 case SEARCH_SYSTEM: 327 case SEARCH_TYPE: 328 case TRANSACTION: 329 case VALIDATE: 330 case VREAD: 331 case METADATA: 332 case META_ADD: 333 case META: 334 case META_DELETE: 335 case PATCH: 336 case BATCH: 337 case ADD_TAGS: 338 case DELETE_TAGS: 339 case GET_TAGS: 340 case GET_PAGE: 341 case GRAPHQL_REQUEST: 342 case EXTENDED_OPERATION_SERVER: 343 case EXTENDED_OPERATION_TYPE: 344 case EXTENDED_OPERATION_INSTANCE: 345 default: 346 break; 347 } 348 } 349 350 checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); 351 352 // Resource Operations 353 if (nextMethodBinding instanceof SearchMethodBinding) { 354 addSearchMethodIfSearchIsNamedQuery( 355 theRequestDetails, bindings, terser, operationNames, resource, (SearchMethodBinding) 356 nextMethodBinding); 357 } else if (nextMethodBinding instanceof OperationMethodBinding) { 358 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 359 String opName = bindings.getOperationBindingToId().get(methodBinding); 360 // Only add each operation (by name) once 361 if (operationNames.add(opName)) { 362 IBase operation = terser.addElement(resource, "operation"); 363 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 364 } 365 } 366 } 367 368 // Find any global operations (Operations defines at the system level but with the 369 // global flag set to true, meaning they apply to all resource types) 370 if (globalMethodBindings != null) { 371 Set<String> globalOperationNames = new HashSet<>(); 372 for (BaseMethodBinding next : globalMethodBindings) { 373 if (next instanceof OperationMethodBinding) { 374 OperationMethodBinding methodBinding = (OperationMethodBinding) next; 375 if (methodBinding.isGlobalMethod()) { 376 if (methodBinding.isCanOperateAtInstanceLevel() 377 || methodBinding.isCanOperateAtTypeLevel()) { 378 String opName = 379 bindings.getOperationBindingToId().get(methodBinding); 380 // Only add each operation (by name) once 381 if (globalOperationNames.add(opName)) { 382 IBase operation = terser.addElement(resource, "operation"); 383 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 384 } 385 } 386 } 387 } 388 } 389 } 390 391 ISearchParamRegistry serverConfiguration; 392 if (myServerConfiguration != null) { 393 serverConfiguration = myServerConfiguration; 394 } else { 395 serverConfiguration = myServer.createConfiguration(); 396 } 397 398 /* 399 * If we have an explicit registry (which will be the case in the JPA server) we use it as priority, 400 * but also fill in any gaps using params from the server itself. This makes sure we include 401 * global params like _lastUpdated 402 */ 403 ResourceSearchParams searchParams; 404 ISearchParamRegistry searchParamRegistry; 405 ResourceSearchParams serverConfigurationActiveSearchParams = serverConfiguration.getActiveSearchParams( 406 resourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH); 407 if (mySearchParamRegistry != null) { 408 searchParamRegistry = mySearchParamRegistry; 409 searchParams = mySearchParamRegistry 410 .getActiveSearchParams( 411 resourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 412 .makeCopy(); 413 for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) { 414 if (nextBuiltInSpName.startsWith("_") 415 && !searchParams.containsParamName(nextBuiltInSpName) 416 && searchParamEnabled(nextBuiltInSpName)) { 417 searchParams.put( 418 nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName)); 419 } 420 } 421 } else { 422 searchParamRegistry = serverConfiguration; 423 searchParams = serverConfigurationActiveSearchParams; 424 } 425 426 for (RuntimeSearchParam next : searchParams.values()) { 427 IBase searchParam = terser.addElement(resource, "searchParam"); 428 terser.addElement(searchParam, "name", next.getName()); 429 terser.addElement(searchParam, "type", next.getParamType().getCode()); 430 if (isNotBlank(next.getDescription())) { 431 terser.addElement(searchParam, "documentation", next.getDescription()); 432 } 433 434 String spUri = next.getUri(); 435 436 if (isNotBlank(spUri)) { 437 terser.addElement(searchParam, "definition", spUri); 438 } 439 } 440 441 // Add Include to CapabilityStatement.rest.resource 442 NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName); 443 if (resourceIncludes.isEmpty()) { 444 List<String> includes = searchParams.values().stream() 445 .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) 446 .map(t -> resourceName + ":" + t.getName()) 447 .sorted() 448 .collect(Collectors.toList()); 449 terser.addElement(resource, "searchInclude", "*"); 450 for (String nextInclude : includes) { 451 terser.addElement(resource, "searchInclude", nextInclude); 452 } 453 } else { 454 for (String resourceInclude : resourceIncludes) { 455 terser.addElement(resource, "searchInclude", resourceInclude); 456 } 457 } 458 459 // Add RevInclude to CapabilityStatement.rest.resource 460 if (myRestResourceRevIncludesEnabled) { 461 NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); 462 if (resourceRevIncludes.isEmpty()) { 463 TreeSet<String> revIncludes = new TreeSet<>(); 464 for (String nextResourceName : resourceToMethods.keySet()) { 465 if (isBlank(nextResourceName)) { 466 continue; 467 } 468 469 for (RuntimeSearchParam t : searchParamRegistry 470 .getActiveSearchParams( 471 nextResourceName, ISearchParamRegistry.SearchParamLookupContextEnum.SEARCH) 472 .values()) { 473 if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 474 if (isNotBlank(t.getName())) { 475 boolean appropriateTarget = false; 476 if (t.getTargets().contains(resourceName) 477 || t.getTargets().isEmpty()) { 478 appropriateTarget = true; 479 } 480 481 if (appropriateTarget) { 482 revIncludes.add(nextResourceName + ":" + t.getName()); 483 } 484 } 485 } 486 } 487 } 488 for (String nextInclude : revIncludes) { 489 terser.addElement(resource, "searchRevInclude", nextInclude); 490 } 491 } else { 492 for (String resourceInclude : resourceRevIncludes) { 493 terser.addElement(resource, "searchRevInclude", resourceInclude); 494 } 495 } 496 } 497 498 // Add SupportedProfile to CapabilityStatement.rest.resource 499 for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { 500 terser.addElement(resource, "supportedProfile", supportedProfile); 501 } 502 503 } else { 504 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 505 checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); 506 if (nextMethodBinding instanceof OperationMethodBinding) { 507 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 508 if (!methodBinding.isGlobalMethod()) { 509 String opName = bindings.getOperationBindingToId().get(methodBinding); 510 if (operationNames.add(opName)) { 511 ourLog.debug("Found bound operation: {}", opName); 512 IBase operation = terser.addElement(rest, "operation"); 513 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 514 } 515 } 516 } else if (nextMethodBinding instanceof SearchMethodBinding) { 517 addSearchMethodIfSearchIsNamedQuery( 518 theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) 519 nextMethodBinding); 520 } 521 } 522 } 523 } 524 525 // Find any global operations (Operations defines at the system level but with the 526 // global flag set to true, meaning they apply to all resource types) 527 if (globalMethodBindings != null) { 528 Set<String> globalOperationNames = new HashSet<>(); 529 for (BaseMethodBinding next : globalMethodBindings) { 530 if (next instanceof OperationMethodBinding) { 531 OperationMethodBinding methodBinding = (OperationMethodBinding) next; 532 if (methodBinding.isGlobalMethod()) { 533 if (methodBinding.isCanOperateAtServerLevel()) { 534 String opName = bindings.getOperationBindingToId().get(methodBinding); 535 // Only add each operation (by name) once 536 if (globalOperationNames.add(opName)) { 537 IBase operation = terser.addElement(rest, "operation"); 538 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 539 } 540 } 541 } 542 } 543 } 544 } 545 546 maybeAddBulkDataDeclarationToConformingToIg(terser, retVal, configuration.getServerBindings()); 547 548 postProcessRest(terser, rest); 549 postProcess(terser, retVal); 550 551 return retVal; 552 } 553 554 private void maybeAddBulkDataDeclarationToConformingToIg( 555 FhirTerser theTerser, IBaseConformance theBaseConformance, List<BaseMethodBinding> theServerBindings) { 556 boolean bulkExportEnabled = theServerBindings.stream() 557 .filter(OperationMethodBinding.class::isInstance) 558 .map(OperationMethodBinding.class::cast) 559 .map(OperationMethodBinding::getName) 560 .anyMatch(ProviderConstants.OPERATION_EXPORT::equals); 561 562 if (bulkExportEnabled) { 563 theTerser.addElement(theBaseConformance, "instantiates", Constants.BULK_DATA_ACCESS_IG_URL); 564 } 565 } 566 567 /** 568 * 569 * @param theSearchParam 570 * @return true if theSearchParam is enabled on this server 571 */ 572 protected boolean searchParamEnabled(String theSearchParam) { 573 return true; 574 } 575 576 private void addSearchMethodIfSearchIsNamedQuery( 577 RequestDetails theRequestDetails, 578 Bindings theBindings, 579 FhirTerser theTerser, 580 Set<String> theOperationNamesAlreadyAdded, 581 IBase theElementToAddTo, 582 SearchMethodBinding theSearchMethodBinding) { 583 if (theSearchMethodBinding.getQueryName() != null) { 584 String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding); 585 if (theOperationNamesAlreadyAdded.add(queryName)) { 586 IBase operation = theTerser.addElement(theElementToAddTo, "operation"); 587 theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName()); 588 theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName))); 589 } 590 } 591 } 592 593 private void populateOperation( 594 RequestDetails theRequestDetails, 595 FhirTerser theTerser, 596 OperationMethodBinding theMethodBinding, 597 String theOpName, 598 IBase theOperation) { 599 String operationName = theMethodBinding.getName().substring(1); 600 theTerser.addElement(theOperation, "name", operationName); 601 String operationCanonicalUrl = theMethodBinding.getCanonicalUrl(); 602 if (isNotBlank(operationCanonicalUrl)) { 603 theTerser.addElement(theOperation, "definition", operationCanonicalUrl); 604 operationCanonicalUrlToId.put(operationCanonicalUrl, theOpName); 605 } else { 606 theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName)); 607 } 608 if (isNotBlank(theMethodBinding.getDescription())) { 609 theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription()); 610 } 611 } 612 613 @Nonnull 614 private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) { 615 return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName; 616 } 617 618 private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) { 619 TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create(); 620 if (myValidationSupport != null) { 621 List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); 622 if (allStructureDefinitions != null) { 623 for (IBaseResource next : allStructureDefinitions) { 624 String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); 625 String url = terser.getSinglePrimitiveValueOrNull(next, "url"); 626 String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); 627 if ("resource".equals(kind) && isNotBlank(url)) { 628 629 // Don't include the base resource definitions in the supported profile list - This isn't 630 // helpful 631 if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") 632 || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { 633 continue; 634 } 635 636 String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); 637 if (isBlank(resourceType)) { 638 resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); 639 } 640 641 if (isNotBlank(resourceType)) { 642 resourceTypeToSupportedProfiles.put(resourceType, url); 643 } 644 } 645 } 646 } 647 } 648 return resourceTypeToSupportedProfiles; 649 } 650 651 /** 652 * Subclasses may override 653 */ 654 protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { 655 // nothing 656 } 657 658 /** 659 * Subclasses may override 660 */ 661 protected void postProcessRest(FhirTerser theTerser, IBase theRest) { 662 // nothing 663 } 664 665 /** 666 * Subclasses may override 667 */ 668 protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { 669 // nothing 670 } 671 672 protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { 673 if (theRequestDetails == null) { 674 return ""; 675 } 676 return theRequestDetails.getServerBaseForRequest() + "/"; 677 } 678 679 @Override 680 @Read(typeName = "OperationDefinition") 681 public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { 682 if (theId == null || theId.hasIdPart() == false) { 683 throw new ResourceNotFoundException(Msg.code(2245) + theId); 684 } 685 RestfulServerConfiguration configuration = getServerConfiguration(); 686 Bindings bindings = configuration.provideBindings(); 687 String operationId = getOperationId(theId); 688 List<OperationMethodBinding> operationBindings = 689 bindings.getOperationIdToBindings().get(operationId); 690 if (operationBindings != null && !operationBindings.isEmpty()) { 691 return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings); 692 } 693 694 List<SearchMethodBinding> searchBindings = 695 bindings.getSearchNameToBindings().get(theId.getIdPart()); 696 if (searchBindings != null && !searchBindings.isEmpty()) { 697 return readOperationDefinitionForNamedSearch(searchBindings); 698 } 699 throw new ResourceNotFoundException(Msg.code(2249) + theId); 700 } 701 702 private String getOperationId(IIdType theId) { 703 if (operationCanonicalUrlToId.get(theId.getValue()) != null) { 704 return operationCanonicalUrlToId.get(theId.getValue()); 705 } 706 return theId.getIdPart(); 707 } 708 709 private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { 710 IBaseResource op = 711 myContext.getResourceDefinition("OperationDefinition").newInstance(); 712 FhirTerser terser = myContext.newTerser(); 713 714 IBase text = terser.addElement(op, "text"); 715 terser.addElement(text, "status", "generated"); 716 terser.addElement(text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">Operation Named Search</div>"); 717 718 terser.addElement(op, "status", "active"); 719 terser.addElement(op, "kind", "query"); 720 terser.addElement(op, "affectsState", "false"); 721 722 terser.addElement(op, "instance", "false"); 723 724 Set<String> inParams = new HashSet<>(); 725 726 String operationCode = null; 727 for (SearchMethodBinding binding : bindings) { 728 if (isNotBlank(binding.getDescription())) { 729 terser.addElement(op, "description", binding.getDescription()); 730 } 731 if (isBlank(binding.getResourceProviderResourceName())) { 732 terser.addElement(op, "system", "true"); 733 terser.addElement(op, "type", "false"); 734 } else { 735 terser.addElement(op, "system", "false"); 736 terser.addElement(op, "type", "true"); 737 terser.addElement(op, "resource", binding.getResourceProviderResourceName()); 738 } 739 740 if (operationCode == null) { 741 operationCode = binding.getQueryName(); 742 } 743 744 for (IParameter nextParamUntyped : binding.getParameters()) { 745 if (nextParamUntyped instanceof SearchParameter) { 746 SearchParameter nextParam = (SearchParameter) nextParamUntyped; 747 if (!inParams.add(nextParam.getName())) { 748 continue; 749 } 750 751 IBase param = terser.addElement(op, "parameter"); 752 terser.addElement(param, "use", "in"); 753 terser.addElement(param, "type", "string"); 754 terser.addElement( 755 param, "searchType", nextParam.getParamType().getCode()); 756 terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); 757 terser.addElement(param, "max", "1"); 758 terser.addElement(param, "name", nextParam.getName()); 759 } 760 } 761 } 762 763 terser.addElement(op, "code", operationCode); 764 765 String operationName = WordUtils.capitalize(operationCode); 766 terser.addElement(op, "name", operationName); 767 768 return op; 769 } 770 771 private IBaseResource readOperationDefinitionForOperation( 772 RequestDetails theRequestDetails, 773 Bindings theBindings, 774 List<OperationMethodBinding> theOperationMethodBindings) { 775 IBaseResource op = 776 myContext.getResourceDefinition("OperationDefinition").newInstance(); 777 FhirTerser terser = myContext.newTerser(); 778 779 IBase text = terser.addElement(op, "text"); 780 terser.addElement(text, "status", "generated"); 781 terser.addElement(text, "div", "<div xmlns=\"http://www.w3.org/1999/xhtml\">Operation</div>"); 782 783 terser.addElement(op, "status", "active"); 784 terser.addElement(op, "kind", "operation"); 785 786 boolean systemLevel = false; 787 boolean typeLevel = false; 788 boolean instanceLevel = false; 789 boolean affectsState = false; 790 String description = null; 791 String title = null; 792 String code = null; 793 String url = null; 794 795 Set<String> resourceNames = new TreeSet<>(); 796 Map<String, IBase> inParams = new HashMap<>(); 797 Map<String, IBase> outParams = new HashMap<>(); 798 799 for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) { 800 if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) { 801 description = operationMethodBinding.getDescription(); 802 } 803 if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) { 804 title = operationMethodBinding.getShortDescription(); 805 } 806 if (operationMethodBinding.isCanOperateAtInstanceLevel()) { 807 instanceLevel = true; 808 } 809 if (operationMethodBinding.isCanOperateAtServerLevel()) { 810 systemLevel = true; 811 } 812 if (operationMethodBinding.isCanOperateAtTypeLevel()) { 813 typeLevel = true; 814 } 815 if (!operationMethodBinding.isIdempotent()) { 816 affectsState |= true; 817 } 818 819 code = operationMethodBinding.getName().substring(1); 820 821 if (isNotBlank(operationMethodBinding.getResourceName())) { 822 resourceNames.add(operationMethodBinding.getResourceName()); 823 } 824 825 if (isBlank(url)) { 826 url = theBindings.getOperationBindingToId().get(operationMethodBinding); 827 if (isNotBlank(url)) { 828 url = createOperationUrl(theRequestDetails, url); 829 } 830 } 831 832 for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) { 833 if (nextParamUntyped instanceof OperationParameter) { 834 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 835 836 IBase param = inParams.get(nextParam.getName()); 837 if (param == null) { 838 param = terser.addElement(op, "parameter"); 839 inParams.put(nextParam.getName(), param); 840 } 841 842 IBase existingParam = inParams.get(nextParam.getName()); 843 if (isNotBlank(nextParam.getDescription()) 844 && terser.getValues(existingParam, "documentation").isEmpty()) { 845 terser.addElement(existingParam, "documentation", nextParam.getDescription()); 846 } 847 848 if (nextParam.getParamType() != null) { 849 String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type"); 850 if (!nextParam.getParamType().equals(existingType)) { 851 if (existingType == null) { 852 terser.setElement(existingParam, "type", nextParam.getParamType()); 853 } else { 854 terser.setElement(existingParam, "type", "Resource"); 855 } 856 } 857 } 858 859 terser.setElement(param, "use", "in"); 860 if (nextParam.getSearchParamType() != null) { 861 terser.setElement(param, "searchType", nextParam.getSearchParamType()); 862 } 863 terser.setElement(param, "min", Integer.toString(nextParam.getMin())); 864 terser.setElement( 865 param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 866 terser.setElement(param, "name", nextParam.getName()); 867 868 List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl( 869 (IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); 870 Set<String> existingExamples = existingExampleExtensions.stream() 871 .map(t -> t.getValue()) 872 .filter(t -> t != null) 873 .map(t -> (IPrimitiveType<?>) t) 874 .map(t -> t.getValueAsString()) 875 .collect(Collectors.toSet()); 876 for (String nextExample : nextParam.getExampleValues()) { 877 if (!existingExamples.contains(nextExample)) { 878 ExtensionUtil.addExtension( 879 myContext, 880 param, 881 HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, 882 "string", 883 nextExample); 884 } 885 } 886 } 887 } 888 889 for (ReturnType nextParam : operationMethodBinding.getReturnParams()) { 890 if (outParams.containsKey(nextParam.getName())) { 891 continue; 892 } 893 894 IBase param = terser.addElement(op, "parameter"); 895 outParams.put(nextParam.getName(), param); 896 897 terser.addElement(param, "use", "out"); 898 if (nextParam.getType() != null) { 899 terser.addElement(param, "type", nextParam.getType()); 900 } 901 terser.addElement(param, "min", Integer.toString(nextParam.getMin())); 902 terser.addElement( 903 param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 904 terser.addElement(param, "name", nextParam.getName()); 905 } 906 } 907 String name = WordUtils.capitalize(code); 908 909 terser.addElements(op, "resource", resourceNames); 910 terser.addElement(op, "name", name); 911 terser.addElement(op, "url", url); 912 terser.addElement(op, "code", code); 913 terser.addElement(op, "description", description); 914 terser.addElement(op, "title", title); 915 terser.addElement(op, "affectsState", Boolean.toString(affectsState)); 916 terser.addElement(op, "system", Boolean.toString(systemLevel)); 917 terser.addElement(op, "type", Boolean.toString(typeLevel)); 918 terser.addElement(op, "instance", Boolean.toString(instanceLevel)); 919 920 return op; 921 } 922 923 @Override 924 public void setRestfulServer(RestfulServer theRestfulServer) { 925 // ignore 926 } 927 928 public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { 929 myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; 930 } 931}