
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2023 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 org.apache.commons.text.WordUtils; 056import org.hl7.fhir.instance.model.api.IBase; 057import org.hl7.fhir.instance.model.api.IBaseConformance; 058import org.hl7.fhir.instance.model.api.IBaseExtension; 059import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 060import org.hl7.fhir.instance.model.api.IBaseResource; 061import org.hl7.fhir.instance.model.api.IIdType; 062import org.hl7.fhir.instance.model.api.IPrimitiveType; 063import org.slf4j.Logger; 064import org.slf4j.LoggerFactory; 065 066import javax.annotation.Nonnull; 067import javax.servlet.ServletContext; 068import javax.servlet.http.HttpServletRequest; 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(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 ResourceSearchParams searchParams; 390 ISearchParamRegistry searchParamRegistry; 391 ResourceSearchParams serverConfigurationActiveSearchParams = serverConfiguration.getActiveSearchParams(resourceName); 392 if (mySearchParamRegistry != null) { 393 searchParamRegistry = mySearchParamRegistry; 394 searchParams = mySearchParamRegistry.getActiveSearchParams(resourceName).makeCopy(); 395 for (String nextBuiltInSpName : serverConfigurationActiveSearchParams.getSearchParamNames()) { 396 if (nextBuiltInSpName.startsWith("_") && 397 !searchParams.containsParamName(nextBuiltInSpName) && 398 searchParamEnabled(nextBuiltInSpName)) { 399 searchParams.put(nextBuiltInSpName, serverConfigurationActiveSearchParams.get(nextBuiltInSpName)); 400 } 401 } 402 } else { 403 searchParamRegistry = serverConfiguration; 404 searchParams = serverConfigurationActiveSearchParams; 405 } 406 407 408 for (RuntimeSearchParam next : searchParams.values()) { 409 IBase searchParam = terser.addElement(resource, "searchParam"); 410 terser.addElement(searchParam, "name", next.getName()); 411 terser.addElement(searchParam, "type", next.getParamType().getCode()); 412 if (isNotBlank(next.getDescription())) { 413 terser.addElement(searchParam, "documentation", next.getDescription()); 414 } 415 416 String spUri = next.getUri(); 417 418 if (isNotBlank(spUri)) { 419 terser.addElement(searchParam, "definition", spUri); 420 } 421 } 422 423 // Add Include to CapabilityStatement.rest.resource 424 NavigableSet<String> resourceIncludes = resourceNameToIncludes.get(resourceName); 425 if (resourceIncludes.isEmpty()) { 426 List<String> includes = searchParams 427 .values() 428 .stream() 429 .filter(t -> t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) 430 .map(t -> resourceName + ":" + t.getName()) 431 .sorted() 432 .collect(Collectors.toList()); 433 terser.addElement(resource, "searchInclude", "*"); 434 for (String nextInclude : includes) { 435 terser.addElement(resource, "searchInclude", nextInclude); 436 } 437 } else { 438 for (String resourceInclude : resourceIncludes) { 439 terser.addElement(resource, "searchInclude", resourceInclude); 440 } 441 } 442 443 // Add RevInclude to CapabilityStatement.rest.resource 444 if (myRestResourceRevIncludesEnabled) { 445 NavigableSet<String> resourceRevIncludes = resourceNameToRevIncludes.get(resourceName); 446 if (resourceRevIncludes.isEmpty()) { 447 TreeSet<String> revIncludes = new TreeSet<>(); 448 for (String nextResourceName : resourceToMethods.keySet()) { 449 if (isBlank(nextResourceName)) { 450 continue; 451 } 452 453 for (RuntimeSearchParam t : searchParamRegistry.getActiveSearchParams(nextResourceName).values()) { 454 if (t.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 455 if (isNotBlank(t.getName())) { 456 boolean appropriateTarget = false; 457 if (t.getTargets().contains(resourceName) || t.getTargets().isEmpty()) { 458 appropriateTarget = true; 459 } 460 461 if (appropriateTarget) { 462 revIncludes.add(nextResourceName + ":" + t.getName()); 463 } 464 } 465 } 466 } 467 } 468 for (String nextInclude : revIncludes) { 469 terser.addElement(resource, "searchRevInclude", nextInclude); 470 } 471 } else { 472 for (String resourceInclude : resourceRevIncludes) { 473 terser.addElement(resource, "searchRevInclude", resourceInclude); 474 } 475 } 476 } 477 478 // Add SupportedProfile to CapabilityStatement.rest.resource 479 for (String supportedProfile : resourceTypeToSupportedProfiles.get(resourceName)) { 480 terser.addElement(resource, "supportedProfile", supportedProfile); 481 } 482 483 } else { 484 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 485 checkBindingForSystemOps(terser, rest, systemOps, nextMethodBinding); 486 if (nextMethodBinding instanceof OperationMethodBinding) { 487 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 488 if (!methodBinding.isGlobalMethod()) { 489 String opName = bindings.getOperationBindingToId().get(methodBinding); 490 if (operationNames.add(opName)) { 491 ourLog.debug("Found bound operation: {}", opName); 492 IBase operation = terser.addElement(rest, "operation"); 493 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 494 } 495 } 496 } else if (nextMethodBinding instanceof SearchMethodBinding) { 497 addSearchMethodIfSearchIsNamedQuery(theRequestDetails, bindings, terser, operationNames, rest, (SearchMethodBinding) nextMethodBinding); 498 } 499 } 500 } 501 502 } 503 504 505 // Find any global operations (Operations defines at the system level but with the 506 // global flag set to true, meaning they apply to all resource types) 507 if (globalMethodBindings != null) { 508 Set<String> globalOperationNames = new HashSet<>(); 509 for (BaseMethodBinding next : globalMethodBindings) { 510 if (next instanceof OperationMethodBinding) { 511 OperationMethodBinding methodBinding = (OperationMethodBinding) next; 512 if (methodBinding.isGlobalMethod()) { 513 if (methodBinding.isCanOperateAtServerLevel()) { 514 String opName = bindings.getOperationBindingToId().get(methodBinding); 515 // Only add each operation (by name) once 516 if (globalOperationNames.add(opName)) { 517 IBase operation = terser.addElement(rest, "operation"); 518 populateOperation(theRequestDetails, terser, methodBinding, opName, operation); 519 } 520 } 521 } 522 } 523 } 524 } 525 526 527 postProcessRest(terser, rest); 528 postProcess(terser, retVal); 529 530 return retVal; 531 } 532 533 /** 534 * 535 * @param theSearchParam 536 * @return true if theSearchParam is enabled on this server 537 */ 538 protected boolean searchParamEnabled(String theSearchParam) { 539 return true; 540 } 541 542 private void addSearchMethodIfSearchIsNamedQuery(RequestDetails theRequestDetails, Bindings theBindings, FhirTerser theTerser, Set<String> theOperationNamesAlreadyAdded, IBase theElementToAddTo, SearchMethodBinding theSearchMethodBinding) { 543 if (theSearchMethodBinding.getQueryName() != null) { 544 String queryName = theBindings.getNamedSearchMethodBindingToName().get(theSearchMethodBinding); 545 if (theOperationNamesAlreadyAdded.add(queryName)) { 546 IBase operation = theTerser.addElement(theElementToAddTo, "operation"); 547 theTerser.addElement(operation, "name", theSearchMethodBinding.getQueryName()); 548 theTerser.addElement(operation, "definition", (createOperationUrl(theRequestDetails, queryName))); 549 } 550 } 551 } 552 553 private void populateOperation(RequestDetails theRequestDetails, FhirTerser theTerser, OperationMethodBinding theMethodBinding, String theOpName, IBase theOperation) { 554 String operationName = theMethodBinding.getName().substring(1); 555 theTerser.addElement(theOperation, "name", operationName); 556 String operationCanonicalUrl = theMethodBinding.getCanonicalUrl(); 557 if (isNotBlank(operationCanonicalUrl)) { 558 theTerser.addElement(theOperation, "definition", operationCanonicalUrl); 559 operationCanonicalUrlToId.put(operationCanonicalUrl, theOpName); 560 } 561 else { 562 theTerser.addElement(theOperation, "definition", createOperationUrl(theRequestDetails, theOpName)); 563 } 564 if (isNotBlank(theMethodBinding.getDescription())) { 565 theTerser.addElement(theOperation, "documentation", theMethodBinding.getDescription()); 566 } 567 } 568 569 @Nonnull 570 private String createOperationUrl(RequestDetails theRequestDetails, String theOpName) { 571 return getOperationDefinitionPrefix(theRequestDetails) + "OperationDefinition/" + theOpName; 572 } 573 574 private TreeMultimap<String, String> getSupportedProfileMultimap(FhirTerser terser) { 575 TreeMultimap<String, String> resourceTypeToSupportedProfiles = TreeMultimap.create(); 576 if (myValidationSupport != null) { 577 List<IBaseResource> allStructureDefinitions = myValidationSupport.fetchAllNonBaseStructureDefinitions(); 578 if (allStructureDefinitions != null) { 579 for (IBaseResource next : allStructureDefinitions) { 580 String kind = terser.getSinglePrimitiveValueOrNull(next, "kind"); 581 String url = terser.getSinglePrimitiveValueOrNull(next, "url"); 582 String baseDefinition = defaultString(terser.getSinglePrimitiveValueOrNull(next, "baseDefinition")); 583 if ("resource".equals(kind) && isNotBlank(url)) { 584 585 // Don't include the base resource definitions in the supported profile list - This isn't helpful 586 if (baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/DomainResource") || baseDefinition.equals("http://hl7.org/fhir/StructureDefinition/Resource")) { 587 continue; 588 } 589 590 String resourceType = terser.getSinglePrimitiveValueOrNull(next, "snapshot.element.path"); 591 if (isBlank(resourceType)) { 592 resourceType = terser.getSinglePrimitiveValueOrNull(next, "differential.element.path"); 593 } 594 595 if (isNotBlank(resourceType)) { 596 resourceTypeToSupportedProfiles.put(resourceType, url); 597 } 598 } 599 } 600 } 601 } 602 return resourceTypeToSupportedProfiles; 603 } 604 605 /** 606 * Subclasses may override 607 */ 608 protected void postProcess(FhirTerser theTerser, IBaseConformance theCapabilityStatement) { 609 // nothing 610 } 611 612 /** 613 * Subclasses may override 614 */ 615 protected void postProcessRest(FhirTerser theTerser, IBase theRest) { 616 // nothing 617 } 618 619 /** 620 * Subclasses may override 621 */ 622 protected void postProcessRestResource(FhirTerser theTerser, IBase theResource, String theResourceName) { 623 // nothing 624 } 625 626 protected String getOperationDefinitionPrefix(RequestDetails theRequestDetails) { 627 if (theRequestDetails == null) { 628 return ""; 629 } 630 return theRequestDetails.getServerBaseForRequest() + "/"; 631 } 632 633 634 @Override 635 @Read(typeName = "OperationDefinition") 636 public IBaseResource readOperationDefinition(@IdParam IIdType theId, RequestDetails theRequestDetails) { 637 if (theId == null || theId.hasIdPart() == false) { 638 throw new ResourceNotFoundException(Msg.code(2245) + theId); 639 } 640 RestfulServerConfiguration configuration = getServerConfiguration(); 641 Bindings bindings = configuration.provideBindings(); 642 String operationId = getOperationId(theId); 643 List<OperationMethodBinding> operationBindings = bindings.getOperationIdToBindings().get(operationId); 644 if (operationBindings != null && !operationBindings.isEmpty()) { 645 return readOperationDefinitionForOperation(theRequestDetails, bindings, operationBindings); 646 } 647 648 List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); 649 if (searchBindings != null && !searchBindings.isEmpty()) { 650 return readOperationDefinitionForNamedSearch(searchBindings); 651 } 652 throw new ResourceNotFoundException(Msg.code(2249) + theId); 653 } 654 655 private String getOperationId(IIdType theId) { 656 if (operationCanonicalUrlToId.get(theId.getValue()) !=null ) { 657 return operationCanonicalUrlToId.get(theId.getValue()); 658 } 659 return theId.getIdPart(); 660 } 661 662 private IBaseResource readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { 663 IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); 664 FhirTerser terser = myContext.newTerser(); 665 666 terser.addElement(op, "status", "active"); 667 terser.addElement(op, "kind", "query"); 668 terser.addElement(op, "affectsState", "false"); 669 670 terser.addElement(op, "instance", "false"); 671 672 Set<String> inParams = new HashSet<>(); 673 674 String operationCode = null; 675 for (SearchMethodBinding binding : bindings) { 676 if (isNotBlank(binding.getDescription())) { 677 terser.addElement(op, "description", binding.getDescription()); 678 } 679 if (isBlank(binding.getResourceProviderResourceName())) { 680 terser.addElement(op, "system", "true"); 681 terser.addElement(op, "type", "false"); 682 } else { 683 terser.addElement(op, "system", "false"); 684 terser.addElement(op, "type", "true"); 685 terser.addElement(op, "resource", binding.getResourceProviderResourceName()); 686 } 687 688 if (operationCode == null) { 689 operationCode = binding.getQueryName(); 690 } 691 692 for (IParameter nextParamUntyped : binding.getParameters()) { 693 if (nextParamUntyped instanceof SearchParameter) { 694 SearchParameter nextParam = (SearchParameter) nextParamUntyped; 695 if (!inParams.add(nextParam.getName())) { 696 continue; 697 } 698 699 IBase param = terser.addElement(op, "parameter"); 700 terser.addElement(param, "use", "in"); 701 terser.addElement(param, "type", "string"); 702 terser.addElement(param, "searchType", nextParam.getParamType().getCode()); 703 terser.addElement(param, "min", nextParam.isRequired() ? "1" : "0"); 704 terser.addElement(param, "max", "1"); 705 terser.addElement(param, "name", nextParam.getName()); 706 } 707 } 708 709 } 710 711 terser.addElement(op, "code", operationCode); 712 713 String operationName = WordUtils.capitalize(operationCode); 714 terser.addElement(op, "name", operationName); 715 716 return op; 717 } 718 719 private IBaseResource readOperationDefinitionForOperation(RequestDetails theRequestDetails, Bindings theBindings, List<OperationMethodBinding> theOperationMethodBindings) { 720 IBaseResource op = myContext.getResourceDefinition("OperationDefinition").newInstance(); 721 FhirTerser terser = myContext.newTerser(); 722 723 terser.addElement(op, "status", "active"); 724 terser.addElement(op, "kind", "operation"); 725 726 boolean systemLevel = false; 727 boolean typeLevel = false; 728 boolean instanceLevel = false; 729 boolean affectsState = false; 730 String description = null; 731 String title = null; 732 String code = null; 733 String url = null; 734 735 Set<String> resourceNames = new TreeSet<>(); 736 Map<String, IBase> inParams = new HashMap<>(); 737 Map<String, IBase> outParams = new HashMap<>(); 738 739 for (OperationMethodBinding operationMethodBinding : theOperationMethodBindings) { 740 if (isNotBlank(operationMethodBinding.getDescription()) && isBlank(description)) { 741 description = operationMethodBinding.getDescription(); 742 } 743 if (isNotBlank(operationMethodBinding.getShortDescription()) && isBlank(title)) { 744 title = operationMethodBinding.getShortDescription(); 745 } 746 if (operationMethodBinding.isCanOperateAtInstanceLevel()) { 747 instanceLevel = true; 748 } 749 if (operationMethodBinding.isCanOperateAtServerLevel()) { 750 systemLevel = true; 751 } 752 if (operationMethodBinding.isCanOperateAtTypeLevel()) { 753 typeLevel = true; 754 } 755 if (!operationMethodBinding.isIdempotent()) { 756 affectsState |= true; 757 } 758 759 code = operationMethodBinding.getName().substring(1); 760 761 if (isNotBlank(operationMethodBinding.getResourceName())) { 762 resourceNames.add(operationMethodBinding.getResourceName()); 763 } 764 765 if (isBlank(url)) { 766 url = theBindings.getOperationBindingToId().get(operationMethodBinding); 767 if (isNotBlank(url)) { 768 url = createOperationUrl(theRequestDetails, url); 769 } 770 } 771 772 773 for (IParameter nextParamUntyped : operationMethodBinding.getParameters()) { 774 if (nextParamUntyped instanceof OperationParameter) { 775 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 776 777 IBase param = inParams.get(nextParam.getName()); 778 if (param == null){ 779 param = terser.addElement(op, "parameter"); 780 inParams.put(nextParam.getName(), param); 781 } 782 783 IBase existingParam = inParams.get(nextParam.getName()); 784 if (isNotBlank(nextParam.getDescription()) && terser.getValues(existingParam, "documentation").isEmpty()) { 785 terser.addElement(existingParam, "documentation", nextParam.getDescription()); 786 } 787 788 if (nextParam.getParamType() != null) { 789 String existingType = terser.getSinglePrimitiveValueOrNull(existingParam, "type"); 790 if (!nextParam.getParamType().equals(existingType)) { 791 if (existingType == null) { 792 terser.setElement(existingParam, "type", nextParam.getParamType()); 793 } else { 794 terser.setElement(existingParam, "type", "Resource"); 795 } 796 } 797 } 798 799 terser.setElement(param, "use", "in"); 800 if (nextParam.getSearchParamType() != null) { 801 terser.setElement(param, "searchType", nextParam.getSearchParamType()); 802 } 803 terser.setElement(param, "min", Integer.toString(nextParam.getMin())); 804 terser.setElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 805 terser.setElement(param, "name", nextParam.getName()); 806 807 List<IBaseExtension<?, ?>> existingExampleExtensions = ExtensionUtil.getExtensionsByUrl((IBaseHasExtensions) param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE); 808 Set<String> existingExamples = existingExampleExtensions 809 .stream() 810 .map(t -> t.getValue()) 811 .filter(t -> t != null) 812 .map(t -> (IPrimitiveType<?>) t) 813 .map(t -> t.getValueAsString()) 814 .collect(Collectors.toSet()); 815 for (String nextExample : nextParam.getExampleValues()) { 816 if (!existingExamples.contains(nextExample)) { 817 ExtensionUtil.addExtension(myContext, param, HapiExtensions.EXT_OP_PARAMETER_EXAMPLE_VALUE, "string", nextExample); 818 } 819 } 820 821 } 822 } 823 824 for (ReturnType nextParam : operationMethodBinding.getReturnParams()) { 825 if (outParams.containsKey(nextParam.getName())) { 826 continue; 827 } 828 829 IBase param = terser.addElement(op, "parameter"); 830 outParams.put(nextParam.getName(), param); 831 832 terser.addElement(param, "use", "out"); 833 if (nextParam.getType() != null) { 834 terser.addElement(param, "type", nextParam.getType()); 835 } 836 terser.addElement(param, "min", Integer.toString(nextParam.getMin())); 837 terser.addElement(param, "max", (nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax()))); 838 terser.addElement(param, "name", nextParam.getName()); 839 } 840 } 841 String name = WordUtils.capitalize(code); 842 843 terser.addElements(op, "resource", resourceNames); 844 terser.addElement(op, "name", name); 845 terser.addElement(op, "url", url); 846 terser.addElement(op, "code", code); 847 terser.addElement(op, "description", description); 848 terser.addElement(op, "title", title); 849 terser.addElement(op, "affectsState", Boolean.toString(affectsState)); 850 terser.addElement(op, "system", Boolean.toString(systemLevel)); 851 terser.addElement(op, "type", Boolean.toString(typeLevel)); 852 terser.addElement(op, "instance", Boolean.toString(instanceLevel)); 853 854 return op; 855 } 856 857 @Override 858 public void setRestfulServer(RestfulServer theRestfulServer) { 859 // ignore 860 } 861 862 public void setRestResourceRevIncludesEnabled(boolean theRestResourceRevIncludesEnabled) { 863 myRestResourceRevIncludesEnabled = theRestResourceRevIncludesEnabled; 864 } 865}