001package org.hl7.fhir.r4.hapi.rest.server; 002 003import ca.uhn.fhir.context.FhirVersionEnum; 004import ca.uhn.fhir.context.RuntimeResourceDefinition; 005import ca.uhn.fhir.context.RuntimeSearchParam; 006import ca.uhn.fhir.parser.DataFormatException; 007import ca.uhn.fhir.rest.annotation.IdParam; 008import ca.uhn.fhir.rest.annotation.Metadata; 009import ca.uhn.fhir.rest.annotation.Read; 010import ca.uhn.fhir.rest.api.Constants; 011import ca.uhn.fhir.rest.api.server.RequestDetails; 012import ca.uhn.fhir.rest.server.*; 013import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 014import ca.uhn.fhir.rest.server.method.*; 015import ca.uhn.fhir.rest.server.method.SearchParameter; 016import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; 017import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider; 018import org.apache.commons.lang3.StringUtils; 019import org.hl7.fhir.exceptions.FHIRException; 020import org.hl7.fhir.instance.model.api.IBaseResource; 021import org.hl7.fhir.instance.model.api.IPrimitiveType; 022import org.hl7.fhir.r4.model.*; 023import org.hl7.fhir.r4.model.CapabilityStatement.*; 024import org.hl7.fhir.r4.model.Enumerations.PublicationStatus; 025import org.hl7.fhir.r4.model.OperationDefinition.OperationDefinitionParameterComponent; 026import org.hl7.fhir.r4.model.OperationDefinition.OperationKind; 027import org.hl7.fhir.r4.model.OperationDefinition.OperationParameterUse; 028 029import javax.servlet.ServletContext; 030import javax.servlet.http.HttpServletRequest; 031import java.util.*; 032import java.util.Map.Entry; 033 034import static org.apache.commons.lang3.StringUtils.isBlank; 035import static org.apache.commons.lang3.StringUtils.isNotBlank; 036 037/* 038 * #%L 039 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) 040 * %% 041 * Copyright (C) 2014 - 2015 University Health Network 042 * %% 043 * Licensed under the Apache License, Version 2.0 (the "License"); 044 * you may not use this file except in compliance with the License. 045 * You may obtain a copy of the License at 046 * 047 * http://www.apache.org/licenses/LICENSE-2.0 048 * 049 * Unless required by applicable law or agreed to in writing, software 050 * distributed under the License is distributed on an "AS IS" BASIS, 051 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 052 * See the License for the specific language governing permissions and 053 * limitations under the License. 054 * #L% 055 */ 056 057/** 058 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation 059 * 060 * <p> 061 * Note: This class is safe to extend, but it is important to note that the same instance of {@link CapabilityStatement} is always returned unless {@link #setCache(boolean)} is called with a value of 062 * <code>false</code>. This means that if you are adding anything to the returned conformance instance on each call you should call <code>setCache(false)</code> in your provider constructor. 063 * </p> 064 */ 065public class ServerCapabilityStatementProvider extends BaseServerCapabilityStatementProvider implements IServerConformanceProvider<CapabilityStatement> { 066 067 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProvider.class); 068 private String myPublisher = "Not provided"; 069 070 /** 071 * No-arg constructor and setter so that the ServerConformanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen. 072 */ 073 public ServerCapabilityStatementProvider() { 074 super(); 075 } 076 077 /** 078 * Constructor 079 * 080 * @deprecated Use no-args constructor instead. Deprecated in 4.0.0 081 */ 082 @Deprecated 083 public ServerCapabilityStatementProvider(RestfulServer theRestfulServer) { 084 this(); 085 } 086 087 /** 088 * Constructor - This is intended only for JAX-RS server 089 */ 090 public ServerCapabilityStatementProvider(RestfulServerConfiguration theServerConfiguration) { 091 super(theServerConfiguration); 092 } 093 094 private void checkBindingForSystemOps(CapabilityStatementRestComponent rest, Set<SystemRestfulInteraction> systemOps, BaseMethodBinding<?> nextMethodBinding) { 095 if (nextMethodBinding.getRestOperationType() != null) { 096 String sysOpCode = nextMethodBinding.getRestOperationType().getCode(); 097 if (sysOpCode != null) { 098 SystemRestfulInteraction sysOp; 099 try { 100 sysOp = SystemRestfulInteraction.fromCode(sysOpCode); 101 } catch (FHIRException e) { 102 return; 103 } 104 if (sysOp == null) { 105 return; 106 } 107 if (systemOps.contains(sysOp) == false) { 108 systemOps.add(sysOp); 109 rest.addInteraction().setCode(sysOp); 110 } 111 } 112 } 113 } 114 115 116 117 private DateTimeType conformanceDate(RequestDetails theRequestDetails) { 118 IPrimitiveType<Date> buildDate = getServerConfiguration(theRequestDetails).getConformanceDate(); 119 if (buildDate != null && buildDate.getValue() != null) { 120 try { 121 return new DateTimeType(buildDate.getValueAsString()); 122 } catch (DataFormatException e) { 123 // fall through 124 } 125 } 126 return DateTimeType.now(); 127 } 128 129 130 131 /** 132 * 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 133 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 134 */ 135 public String getPublisher() { 136 return myPublisher; 137 } 138 139 /** 140 * 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 141 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 142 */ 143 public void setPublisher(String thePublisher) { 144 myPublisher = thePublisher; 145 } 146 147 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 148 @Override 149 @Metadata 150 public CapabilityStatement getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { 151 152 RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); 153 Bindings bindings = configuration.provideBindings(); 154 155 CapabilityStatement retVal = new CapabilityStatement(); 156 157 retVal.setPublisher(myPublisher); 158 retVal.setDateElement(conformanceDate(theRequestDetails)); 159 retVal.setFhirVersion(Enumerations.FHIRVersion.fromCode(FhirVersionEnum.R4.getFhirVersionString())); 160 161 ServletContext servletContext = (ServletContext) (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); 162 String serverBase = configuration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); 163 retVal 164 .getImplementation() 165 .setUrl(serverBase) 166 .setDescription(configuration.getImplementationDescription()); 167 168 retVal.setKind(CapabilityStatementKind.INSTANCE); 169 retVal.getSoftware().setName(configuration.getServerName()); 170 retVal.getSoftware().setVersion(configuration.getServerVersion()); 171 retVal.addFormat(Constants.CT_FHIR_XML_NEW); 172 retVal.addFormat(Constants.CT_FHIR_JSON_NEW); 173 retVal.setStatus(PublicationStatus.ACTIVE); 174 175 CapabilityStatementRestComponent rest = retVal.addRest(); 176 rest.setMode(RestfulCapabilityMode.SERVER); 177 178 Set<SystemRestfulInteraction> systemOps = new HashSet<>(); 179 Set<String> operationNames = new HashSet<>(); 180 181 Map<String, List<BaseMethodBinding<?>>> resourceToMethods = configuration.collectMethodBindings(); 182 for (Entry<String, List<BaseMethodBinding<?>>> nextEntry : resourceToMethods.entrySet()) { 183 184 if (nextEntry.getKey().isEmpty() == false) { 185 Set<TypeRestfulInteraction> resourceOps = new HashSet<>(); 186 CapabilityStatementRestResourceComponent resource = rest.addResource(); 187 String resourceName = nextEntry.getKey(); 188 RuntimeResourceDefinition def = configuration.getFhirContext().getResourceDefinition(resourceName); 189 resource.getTypeElement().setValue(def.getName()); 190 resource.getProfileElement().setValue((def.getResourceProfile(serverBase))); 191 192 TreeSet<String> includes = new TreeSet<>(); 193 194 // Map<String, CapabilityStatement.RestResourceSearchParam> nameToSearchParam = new HashMap<String, 195 // CapabilityStatement.RestResourceSearchParam>(); 196 for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { 197 if (nextMethodBinding.getRestOperationType() != null) { 198 String resOpCode = nextMethodBinding.getRestOperationType().getCode(); 199 if (resOpCode != null) { 200 TypeRestfulInteraction resOp; 201 try { 202 resOp = TypeRestfulInteraction.fromCode(resOpCode); 203 } catch (Exception e) { 204 resOp = null; 205 } 206 if (resOp != null) { 207 if (resourceOps.contains(resOp) == false) { 208 resourceOps.add(resOp); 209 resource.addInteraction().setCode(resOp); 210 } 211 if ("vread".equals(resOpCode)) { 212 // vread implies read 213 resOp = TypeRestfulInteraction.READ; 214 if (resourceOps.contains(resOp) == false) { 215 resourceOps.add(resOp); 216 resource.addInteraction().setCode(resOp); 217 } 218 } 219 220 if (nextMethodBinding.isSupportsConditional()) { 221 switch (resOp) { 222 case CREATE: 223 resource.setConditionalCreate(true); 224 break; 225 case DELETE: 226 if (nextMethodBinding.isSupportsConditionalMultiple()) { 227 resource.setConditionalDelete(ConditionalDeleteStatus.MULTIPLE); 228 } else { 229 resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); 230 } 231 break; 232 case UPDATE: 233 resource.setConditionalUpdate(true); 234 break; 235 default: 236 break; 237 } 238 } 239 } 240 } 241 } 242 243 checkBindingForSystemOps(rest, systemOps, nextMethodBinding); 244 245 if (nextMethodBinding instanceof SearchMethodBinding) { 246 SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; 247 if (methodBinding.getQueryName() != null) { 248 String queryName = bindings.getNamedSearchMethodBindingToName().get(methodBinding); 249 if (operationNames.add(queryName)) { 250 rest.addOperation().setName(methodBinding.getQueryName()).setDefinition(("OperationDefinition/" + queryName)); 251 } 252 } else { 253 handleNamelessSearchMethodBinding(resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); 254 } 255 } else if (nextMethodBinding instanceof OperationMethodBinding) { 256 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 257 String opName = bindings.getOperationBindingToName().get(methodBinding); 258 if (operationNames.add(opName)) { 259 // Only add each operation (by name) once 260 rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(("OperationDefinition/" + opName)); 261 } 262 } 263 264 resource.getInteraction().sort(new Comparator<ResourceInteractionComponent>() { 265 @Override 266 public int compare(ResourceInteractionComponent theO1, ResourceInteractionComponent theO2) { 267 TypeRestfulInteraction o1 = theO1.getCode(); 268 TypeRestfulInteraction o2 = theO2.getCode(); 269 if (o1 == null && o2 == null) { 270 return 0; 271 } 272 if (o1 == null) { 273 return 1; 274 } 275 if (o2 == null) { 276 return -1; 277 } 278 return o1.ordinal() - o2.ordinal(); 279 } 280 }); 281 282 } 283 284 for (String nextInclude : includes) { 285 resource.addSearchInclude(nextInclude); 286 } 287 } else { 288 for (BaseMethodBinding<?> nextMethodBinding : nextEntry.getValue()) { 289 checkBindingForSystemOps(rest, systemOps, nextMethodBinding); 290 if (nextMethodBinding instanceof OperationMethodBinding) { 291 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 292 String opName = bindings.getOperationBindingToName().get(methodBinding); 293 if (operationNames.add(opName)) { 294 ourLog.debug("Found bound operation: {}", opName); 295 rest.addOperation().setName(methodBinding.getName().substring(1)).setDefinition(("OperationDefinition/" + opName)); 296 } 297 } 298 } 299 } 300 } 301 302 return retVal; 303 } 304 305 private void handleNamelessSearchMethodBinding(CapabilityStatementRestResourceComponent resource, RuntimeResourceDefinition def, TreeSet<String> includes, 306 SearchMethodBinding searchMethodBinding, RequestDetails theRequestDetails) { 307 includes.addAll(searchMethodBinding.getIncludes()); 308 309 List<IParameter> params = searchMethodBinding.getParameters(); 310 List<SearchParameter> searchParameters = new ArrayList<>(); 311 for (IParameter nextParameter : params) { 312 if ((nextParameter instanceof SearchParameter)) { 313 searchParameters.add((SearchParameter) nextParameter); 314 } 315 } 316 sortSearchParameters(searchParameters); 317 if (!searchParameters.isEmpty()) { 318 // boolean allOptional = searchParameters.get(0).isRequired() == false; 319 // 320 // OperationDefinition query = null; 321 // if (!allOptional) { 322 // RestOperation operation = rest.addOperation(); 323 // query = new OperationDefinition(); 324 // operation.setDefinition(new ResourceReferenceDt(query)); 325 // query.getDescriptionElement().setValue(searchMethodBinding.getDescription()); 326 // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); 327 // for (String nextInclude : searchMethodBinding.getIncludes()) { 328 // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); 329 // } 330 // } 331 332 for (SearchParameter nextParameter : searchParameters) { 333 334 String nextParamName = nextParameter.getName(); 335 336 String chain = null; 337 String nextParamUnchainedName = nextParamName; 338 if (nextParamName.contains(".")) { 339 chain = nextParamName.substring(nextParamName.indexOf('.') + 1); 340 nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); 341 } 342 343 String nextParamDescription = nextParameter.getDescription(); 344 345 /* 346 * If the parameter has no description, default to the one from the resource 347 */ 348 if (StringUtils.isBlank(nextParamDescription)) { 349 RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); 350 if (paramDef != null) { 351 nextParamDescription = paramDef.getDescription(); 352 } 353 } 354 355 CapabilityStatementRestResourceSearchParamComponent param = resource.addSearchParam(); 356 param.setName(nextParamUnchainedName); 357 358// if (StringUtils.isNotBlank(chain)) { 359// param.addChain(chain); 360// } 361// 362// if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 363// for (String nextWhitelist : new TreeSet<String>(nextParameter.getQualifierWhitelist())) { 364// if (nextWhitelist.startsWith(".")) { 365// param.addChain(nextWhitelist.substring(1)); 366// } 367// } 368// } 369 370 param.setDocumentation(nextParamDescription); 371 if (nextParameter.getParamType() != null) { 372 param.getTypeElement().setValueAsString(nextParameter.getParamType().getCode()); 373 } 374 for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) { 375 RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails).getFhirContext().getResourceDefinition(nextTarget); 376 if (targetDef != null) { 377 ResourceType code; 378 try { 379 code = ResourceType.fromCode(targetDef.getName()); 380 } catch (FHIRException e) { 381 code = null; 382 } 383// if (code != null) { 384// param.addTarget(targetDef.getName()); 385// } 386 } 387 } 388 } 389 } 390 } 391 392 393 394 @Read(type = OperationDefinition.class) 395 public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) { 396 if (theId == null || theId.hasIdPart() == false) { 397 throw new ResourceNotFoundException(theId); 398 } 399 RestfulServerConfiguration configuration = getServerConfiguration(theRequestDetails); 400 Bindings bindings = configuration.provideBindings(); 401 402 List<OperationMethodBinding> operationBindings = bindings.getOperationNameToBindings().get(theId.getIdPart()); 403 if (operationBindings != null && !operationBindings.isEmpty()) { 404 return readOperationDefinitionForOperation(operationBindings); 405 } 406 List<SearchMethodBinding> searchBindings = bindings.getSearchNameToBindings().get(theId.getIdPart()); 407 if (searchBindings != null && !searchBindings.isEmpty()) { 408 return readOperationDefinitionForNamedSearch(searchBindings); 409 } 410 throw new ResourceNotFoundException(theId); 411 } 412 413 private OperationDefinition readOperationDefinitionForNamedSearch(List<SearchMethodBinding> bindings) { 414 OperationDefinition op = new OperationDefinition(); 415 op.setStatus(PublicationStatus.ACTIVE); 416 op.setKind(OperationKind.QUERY); 417 op.setAffectsState(false); 418 419 op.setSystem(false); 420 op.setType(false); 421 op.setInstance(false); 422 423 Set<String> inParams = new HashSet<>(); 424 425 for (SearchMethodBinding binding : bindings) { 426 if (isNotBlank(binding.getDescription())) { 427 op.setDescription(binding.getDescription()); 428 } 429 if (isBlank(binding.getResourceProviderResourceName())) { 430 op.setSystem(true); 431 } else { 432 op.setType(true); 433 op.addResourceElement().setValue(binding.getResourceProviderResourceName()); 434 } 435 op.setCode(binding.getQueryName()); 436 for (IParameter nextParamUntyped : binding.getParameters()) { 437 if (nextParamUntyped instanceof SearchParameter) { 438 SearchParameter nextParam = (SearchParameter) nextParamUntyped; 439 if (!inParams.add(nextParam.getName())) { 440 continue; 441 } 442 OperationDefinitionParameterComponent param = op.addParameter(); 443 param.setUse(OperationParameterUse.IN); 444 param.setType("string"); 445 param.getSearchTypeElement().setValueAsString(nextParam.getParamType().getCode()); 446 param.setMin(nextParam.isRequired() ? 1 : 0); 447 param.setMax("1"); 448 param.setName(nextParam.getName()); 449 } 450 } 451 452 if (isBlank(op.getName())) { 453 if (isNotBlank(op.getDescription())) { 454 op.setName(op.getDescription()); 455 } else { 456 op.setName(op.getCode()); 457 } 458 } 459 } 460 461 return op; 462 } 463 464 private OperationDefinition readOperationDefinitionForOperation(List<OperationMethodBinding> bindings) { 465 OperationDefinition op = new OperationDefinition(); 466 op.setStatus(PublicationStatus.ACTIVE); 467 op.setKind(OperationKind.OPERATION); 468 op.setAffectsState(false); 469 470 // We reset these to true below if we find a binding that can handle the level 471 op.setSystem(false); 472 op.setType(false); 473 op.setInstance(false); 474 475 Set<String> inParams = new HashSet<>(); 476 Set<String> outParams = new HashSet<>(); 477 478 for (OperationMethodBinding sharedDescription : bindings) { 479 if (isNotBlank(sharedDescription.getDescription())) { 480 op.setDescription(sharedDescription.getDescription()); 481 } 482 if (sharedDescription.isCanOperateAtInstanceLevel()) { 483 op.setInstance(true); 484 } 485 if (sharedDescription.isCanOperateAtServerLevel()) { 486 op.setSystem(true); 487 } 488 if (sharedDescription.isCanOperateAtTypeLevel()) { 489 op.setType(true); 490 } 491 if (!sharedDescription.isIdempotent()) { 492 op.setAffectsState(!sharedDescription.isIdempotent()); 493 } 494 op.setCode(sharedDescription.getName().substring(1)); 495 if (sharedDescription.isCanOperateAtInstanceLevel()) { 496 op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); 497 } 498 if (sharedDescription.isCanOperateAtServerLevel()) { 499 op.setSystem(sharedDescription.isCanOperateAtServerLevel()); 500 } 501 if (isNotBlank(sharedDescription.getResourceName())) { 502 op.addResourceElement().setValue(sharedDescription.getResourceName()); 503 } 504 505 for (IParameter nextParamUntyped : sharedDescription.getParameters()) { 506 if (nextParamUntyped instanceof OperationParameter) { 507 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 508 OperationDefinitionParameterComponent param = op.addParameter(); 509 if (!inParams.add(nextParam.getName())) { 510 continue; 511 } 512 param.setUse(OperationParameterUse.IN); 513 if (nextParam.getParamType() != null) { 514 param.setType(nextParam.getParamType()); 515 } 516 if (nextParam.getSearchParamType() != null) { 517 param.getSearchTypeElement().setValueAsString(nextParam.getSearchParamType()); 518 } 519 param.setMin(nextParam.getMin()); 520 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 521 param.setName(nextParam.getName()); 522 } 523 } 524 525 for (ReturnType nextParam : sharedDescription.getReturnParams()) { 526 if (!outParams.add(nextParam.getName())) { 527 continue; 528 } 529 OperationDefinitionParameterComponent param = op.addParameter(); 530 param.setUse(OperationParameterUse.OUT); 531 if (nextParam.getType() != null) { 532 param.setType(nextParam.getType()); 533 } 534 param.setMin(nextParam.getMin()); 535 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 536 param.setName(nextParam.getName()); 537 } 538 } 539 540 if (isBlank(op.getName())) { 541 if (isNotBlank(op.getDescription())) { 542 op.setName(op.getDescription()); 543 } else { 544 op.setName(op.getCode()); 545 } 546 } 547 548 if (op.hasSystem() == false) { 549 op.setSystem(false); 550 } 551 if (op.hasInstance() == false) { 552 op.setInstance(false); 553 } 554 555 return op; 556 } 557 558 /** 559 * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation. 560 * <p> 561 * See the class documentation for an important note if you are extending this class 562 * </p> 563 * 564 * @deprecated Since 4.0.0 - This method no longer does anything 565 */ 566 @Deprecated 567 public ServerCapabilityStatementProvider setCache(boolean theCache) { 568 return this; 569 } 570 571 @Override 572 public void setRestfulServer(RestfulServer theRestfulServer) { 573 // ignore 574 } 575 576 private void sortRuntimeSearchParameters(List<RuntimeSearchParam> searchParameters) { 577 Collections.sort(searchParameters, new Comparator<RuntimeSearchParam>() { 578 @Override 579 public int compare(RuntimeSearchParam theO1, RuntimeSearchParam theO2) { 580 return theO1.getName().compareTo(theO2.getName()); 581 } 582 }); 583 } 584 585 private void sortSearchParameters(List<SearchParameter> searchParameters) { 586 Collections.sort(searchParameters, new Comparator<SearchParameter>() { 587 @Override 588 public int compare(SearchParameter theO1, SearchParameter theO2) { 589 if (theO1.isRequired() == theO2.isRequired()) { 590 return theO1.getName().compareTo(theO2.getName()); 591 } 592 if (theO1.isRequired()) { 593 return -1; 594 } 595 return 1; 596 } 597 }); 598 } 599}