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