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