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