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