001/* 002 * #%L 003 * HAPI FHIR Structures - DSTU2 (FHIR v1.0.0) 004 * %% 005 * Copyright (C) 2014 - 2024 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.dstu2; 021 022import ca.uhn.fhir.context.FhirVersionEnum; 023import ca.uhn.fhir.context.RuntimeResourceDefinition; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.model.dstu2.resource.Conformance; 027import ca.uhn.fhir.model.dstu2.resource.Conformance.Rest; 028import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResource; 029import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceInteraction; 030import ca.uhn.fhir.model.dstu2.resource.Conformance.RestResourceSearchParam; 031import ca.uhn.fhir.model.dstu2.resource.OperationDefinition; 032import ca.uhn.fhir.model.dstu2.resource.OperationDefinition.Parameter; 033import ca.uhn.fhir.model.dstu2.valueset.ConditionalDeleteStatusEnum; 034import ca.uhn.fhir.model.dstu2.valueset.ConformanceResourceStatusEnum; 035import ca.uhn.fhir.model.dstu2.valueset.ConformanceStatementKindEnum; 036import ca.uhn.fhir.model.dstu2.valueset.OperationKindEnum; 037import ca.uhn.fhir.model.dstu2.valueset.OperationParameterUseEnum; 038import ca.uhn.fhir.model.dstu2.valueset.ResourceTypeEnum; 039import ca.uhn.fhir.model.dstu2.valueset.RestfulConformanceModeEnum; 040import ca.uhn.fhir.model.dstu2.valueset.SystemRestfulInteractionEnum; 041import ca.uhn.fhir.model.dstu2.valueset.TypeRestfulInteractionEnum; 042import ca.uhn.fhir.model.dstu2.valueset.UnknownContentCodeEnum; 043import ca.uhn.fhir.model.primitive.DateTimeDt; 044import ca.uhn.fhir.model.primitive.IdDt; 045import ca.uhn.fhir.parser.DataFormatException; 046import ca.uhn.fhir.rest.annotation.IdParam; 047import ca.uhn.fhir.rest.annotation.Metadata; 048import ca.uhn.fhir.rest.annotation.Read; 049import ca.uhn.fhir.rest.api.Constants; 050import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 051import ca.uhn.fhir.rest.api.server.RequestDetails; 052import ca.uhn.fhir.rest.server.Bindings; 053import ca.uhn.fhir.rest.server.IServerConformanceProvider; 054import ca.uhn.fhir.rest.server.ResourceBinding; 055import ca.uhn.fhir.rest.server.RestfulServer; 056import ca.uhn.fhir.rest.server.RestfulServerConfiguration; 057import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 058import ca.uhn.fhir.rest.server.method.BaseMethodBinding; 059import ca.uhn.fhir.rest.server.method.IParameter; 060import ca.uhn.fhir.rest.server.method.OperationMethodBinding; 061import ca.uhn.fhir.rest.server.method.OperationMethodBinding.ReturnType; 062import ca.uhn.fhir.rest.server.method.OperationParameter; 063import ca.uhn.fhir.rest.server.method.SearchMethodBinding; 064import ca.uhn.fhir.rest.server.method.SearchParameter; 065import ca.uhn.fhir.rest.server.util.BaseServerCapabilityStatementProvider; 066import jakarta.servlet.ServletContext; 067import jakarta.servlet.http.HttpServletRequest; 068import org.apache.commons.lang3.StringUtils; 069import org.hl7.fhir.instance.model.api.IBaseResource; 070import org.hl7.fhir.instance.model.api.IPrimitiveType; 071 072import java.util.ArrayList; 073import java.util.Collections; 074import java.util.Comparator; 075import java.util.Date; 076import java.util.HashSet; 077import java.util.List; 078import java.util.Map; 079import java.util.Map.Entry; 080import java.util.Set; 081import java.util.TreeMap; 082import java.util.TreeSet; 083 084import static org.apache.commons.lang3.StringUtils.isBlank; 085import static org.apache.commons.lang3.StringUtils.isNotBlank; 086 087/** 088 * Server FHIR Provider which serves the conformance statement for a RESTful server implementation 089 */ 090public class ServerConformanceProvider extends BaseServerCapabilityStatementProvider 091 implements IServerConformanceProvider<Conformance> { 092 093 private String myPublisher = "Not provided"; 094 095 /** 096 * No-arg constructor and setter so that the ServerConfirmanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen. 097 */ 098 public ServerConformanceProvider() { 099 super(); 100 } 101 102 /** 103 * Constructor 104 * 105 * @deprecated Use no-args constructor instead. Deprecated in 4.0.0 106 */ 107 @Deprecated 108 public ServerConformanceProvider(RestfulServer theRestfulServer) { 109 this(); 110 } 111 112 /** 113 * Constructor 114 */ 115 public ServerConformanceProvider(RestfulServerConfiguration theServerConfiguration) { 116 super(theServerConfiguration); 117 } 118 119 private void checkBindingForSystemOps( 120 Rest rest, Set<SystemRestfulInteractionEnum> systemOps, BaseMethodBinding nextMethodBinding) { 121 if (nextMethodBinding.getRestOperationType() != null) { 122 String sysOpCode = nextMethodBinding.getRestOperationType().getCode(); 123 if (sysOpCode != null) { 124 SystemRestfulInteractionEnum sysOp = 125 SystemRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(sysOpCode); 126 if (sysOp == null) { 127 return; 128 } 129 if (systemOps.contains(sysOp) == false) { 130 systemOps.add(sysOp); 131 rest.addInteraction().setCode(sysOp); 132 } 133 } 134 } 135 } 136 137 private Map<String, List<BaseMethodBinding>> collectMethodBindings(RequestDetails theRequestDetails) { 138 Map<String, List<BaseMethodBinding>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding>>(); 139 for (ResourceBinding next : getServerConfiguration(theRequestDetails).getResourceBindings()) { 140 String resourceName = next.getResourceName(); 141 for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { 142 if (resourceToMethods.containsKey(resourceName) == false) { 143 resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding>()); 144 } 145 resourceToMethods.get(resourceName).add(nextMethodBinding); 146 } 147 } 148 for (BaseMethodBinding nextMethodBinding : 149 getServerConfiguration(theRequestDetails).getServerBindings()) { 150 String resourceName = ""; 151 if (resourceToMethods.containsKey(resourceName) == false) { 152 resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding>()); 153 } 154 resourceToMethods.get(resourceName).add(nextMethodBinding); 155 } 156 return resourceToMethods; 157 } 158 159 private DateTimeDt conformanceDate(RequestDetails theRequestDetails) { 160 IPrimitiveType<Date> buildDate = 161 getServerConfiguration(theRequestDetails).getConformanceDate(); 162 if (buildDate != null && buildDate.getValue() != null) { 163 try { 164 return new DateTimeDt(buildDate.getValueAsString()); 165 } catch (DataFormatException e) { 166 // fall through 167 } 168 } 169 return DateTimeDt.withCurrentTime(); 170 } 171 172 private String createOperationName(OperationMethodBinding theMethodBinding) { 173 StringBuilder retVal = new StringBuilder(); 174 if (theMethodBinding.getResourceName() != null) { 175 retVal.append(theMethodBinding.getResourceName()); 176 } 177 178 retVal.append('-'); 179 if (theMethodBinding.isCanOperateAtInstanceLevel()) { 180 retVal.append('i'); 181 } 182 if (theMethodBinding.isCanOperateAtServerLevel()) { 183 retVal.append('s'); 184 } 185 retVal.append('-'); 186 187 // Exclude the leading $ 188 retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length()); 189 190 return retVal.toString(); 191 } 192 193 /** 194 * 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 195 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 196 */ 197 public String getPublisher() { 198 return myPublisher; 199 } 200 201 /** 202 * 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 203 * value defaults to "Not provided" but may be set to null, which will cause this element to be omitted. 204 */ 205 public void setPublisher(String thePublisher) { 206 myPublisher = thePublisher; 207 } 208 209 @SuppressWarnings("EnumSwitchStatementWhichMissesCases") 210 @Override 211 @Metadata 212 public Conformance getServerConformance(HttpServletRequest theRequest, RequestDetails theRequestDetails) { 213 RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); 214 Bindings bindings = serverConfiguration.provideBindings(); 215 216 Conformance retVal = new Conformance(); 217 218 retVal.setPublisher(myPublisher); 219 retVal.setDate(conformanceDate(theRequestDetails)); 220 retVal.setFhirVersion(FhirVersionEnum.DSTU2.getFhirVersionString()); 221 retVal.setAcceptUnknown( 222 UnknownContentCodeEnum 223 .UNKNOWN_EXTENSIONS); // TODO: make this configurable - this is a fairly big effort since the 224 // parser 225 // needs to be modified to actually allow it 226 227 ServletContext servletContext = (ServletContext) 228 (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); 229 String serverBase = 230 serverConfiguration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); 231 retVal.getImplementation() 232 .setUrl(serverBase) 233 .setDescription(serverConfiguration.getImplementationDescription()); 234 235 retVal.setKind(ConformanceStatementKindEnum.INSTANCE); 236 retVal.getSoftware().setName(serverConfiguration.getServerName()); 237 retVal.getSoftware().setVersion(serverConfiguration.getServerVersion()); 238 retVal.addFormat(Constants.CT_FHIR_XML); 239 retVal.addFormat(Constants.CT_FHIR_JSON); 240 241 Rest rest = retVal.addRest(); 242 rest.setMode(RestfulConformanceModeEnum.SERVER); 243 244 Set<SystemRestfulInteractionEnum> systemOps = new HashSet<>(); 245 Set<String> operationNames = new HashSet<>(); 246 247 Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings(theRequestDetails); 248 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 249 250 if (nextEntry.getKey().isEmpty() == false) { 251 Set<TypeRestfulInteractionEnum> resourceOps = new HashSet<>(); 252 RestResource resource = rest.addResource(); 253 String resourceName = nextEntry.getKey(); 254 RuntimeResourceDefinition def = 255 serverConfiguration.getFhirContext().getResourceDefinition(resourceName); 256 resource.getTypeElement().setValue(def.getName()); 257 resource.getProfile().setReference(new IdDt(def.getResourceProfile(serverBase))); 258 259 TreeSet<String> includes = new TreeSet<>(); 260 261 // Map<String, Conformance.RestResourceSearchParam> nameToSearchParam = new HashMap<String, 262 // Conformance.RestResourceSearchParam>(); 263 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 264 if (nextMethodBinding.getRestOperationType() != null) { 265 String resOpCode = 266 nextMethodBinding.getRestOperationType().getCode(); 267 if (resOpCode != null) { 268 TypeRestfulInteractionEnum resOp = 269 TypeRestfulInteractionEnum.VALUESET_BINDER.fromCodeString(resOpCode); 270 if (resOp != null) { 271 if (resourceOps.contains(resOp) == false) { 272 resourceOps.add(resOp); 273 resource.addInteraction().setCode(resOp); 274 } 275 if ("vread".equals(resOpCode)) { 276 // vread implies read 277 resOp = TypeRestfulInteractionEnum.READ; 278 if (resourceOps.contains(resOp) == false) { 279 resourceOps.add(resOp); 280 resource.addInteraction().setCode(resOp); 281 } 282 } 283 284 if (nextMethodBinding.isSupportsConditional()) { 285 switch (resOp) { 286 case CREATE: 287 resource.setConditionalCreate(true); 288 break; 289 case DELETE: 290 if (nextMethodBinding.isSupportsConditionalMultiple()) { 291 resource.setConditionalDelete( 292 ConditionalDeleteStatusEnum.MULTIPLE_DELETES_SUPPORTED); 293 } else { 294 resource.setConditionalDelete( 295 ConditionalDeleteStatusEnum.SINGLE_DELETES_SUPPORTED); 296 } 297 break; 298 case UPDATE: 299 resource.setConditionalUpdate(true); 300 break; 301 default: 302 break; 303 } 304 } 305 } 306 } 307 } 308 309 checkBindingForSystemOps(rest, systemOps, nextMethodBinding); 310 311 if (nextMethodBinding instanceof SearchMethodBinding) { 312 handleSearchMethodBinding( 313 resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); 314 } else if (nextMethodBinding instanceof OperationMethodBinding) { 315 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 316 String opName = bindings.getOperationBindingToId().get(methodBinding); 317 if (operationNames.add(opName)) { 318 // Only add each operation (by name) once 319 rest.addOperation() 320 .setName(methodBinding.getName().substring(1)) 321 .getDefinition() 322 .setReference("OperationDefinition/" + opName); 323 } 324 } 325 326 Collections.sort(resource.getInteraction(), new Comparator<RestResourceInteraction>() { 327 @Override 328 public int compare(RestResourceInteraction theO1, RestResourceInteraction theO2) { 329 TypeRestfulInteractionEnum o1 = 330 theO1.getCodeElement().getValueAsEnum(); 331 TypeRestfulInteractionEnum o2 = 332 theO2.getCodeElement().getValueAsEnum(); 333 if (o1 == null && o2 == null) { 334 return 0; 335 } 336 if (o1 == null) { 337 return 1; 338 } 339 if (o2 == null) { 340 return -1; 341 } 342 return o1.ordinal() - o2.ordinal(); 343 } 344 }); 345 } 346 347 for (String nextInclude : includes) { 348 resource.addSearchInclude(nextInclude); 349 } 350 } else { 351 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 352 checkBindingForSystemOps(rest, systemOps, nextMethodBinding); 353 if (nextMethodBinding instanceof OperationMethodBinding) { 354 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 355 String opName = bindings.getOperationBindingToId().get(methodBinding); 356 if (operationNames.add(opName)) { 357 rest.addOperation() 358 .setName(methodBinding.getName().substring(1)) 359 .getDefinition() 360 .setReference("OperationDefinition/" + opName); 361 } 362 } 363 } 364 } 365 } 366 367 return retVal; 368 } 369 370 private void handleSearchMethodBinding( 371 RestResource resource, 372 RuntimeResourceDefinition def, 373 TreeSet<String> includes, 374 SearchMethodBinding searchMethodBinding, 375 RequestDetails theRequestDetails) { 376 includes.addAll(searchMethodBinding.getIncludes()); 377 378 List<IParameter> params = searchMethodBinding.getParameters(); 379 List<SearchParameter> searchParameters = new ArrayList<>(); 380 for (IParameter nextParameter : params) { 381 if ((nextParameter instanceof SearchParameter)) { 382 searchParameters.add((SearchParameter) nextParameter); 383 } 384 } 385 sortSearchParameters(searchParameters); 386 if (!searchParameters.isEmpty()) { 387 // boolean allOptional = searchParameters.get(0).isRequired() == false; 388 // 389 // OperationDefinition query = null; 390 // if (!allOptional) { 391 // RestOperation operation = rest.addOperation(); 392 // query = new OperationDefinition(); 393 // operation.setDefinition(new ResourceReferenceDt(query)); 394 // query.getDescriptionElement().setValue(searchMethodBinding.getDescription()); 395 // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); 396 // for (String nextInclude : searchMethodBinding.getIncludes()) { 397 // query.addUndeclaredExtension(false, ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); 398 // } 399 // } 400 401 for (SearchParameter nextParameter : searchParameters) { 402 403 String nextParamName = nextParameter.getName(); 404 405 String chain = null; 406 String nextParamUnchainedName = nextParamName; 407 if (nextParamName.contains(".")) { 408 chain = nextParamName.substring(nextParamName.indexOf('.') + 1); 409 nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); 410 } 411 412 String nextParamDescription = nextParameter.getDescription(); 413 414 /* 415 * If the parameter has no description, default to the one from the resource 416 */ 417 if (StringUtils.isBlank(nextParamDescription)) { 418 RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); 419 if (paramDef != null) { 420 nextParamDescription = paramDef.getDescription(); 421 } 422 } 423 424 String finalNextParamUnchainedName = nextParamUnchainedName; 425 RestResourceSearchParam param = resource.getSearchParam().stream() 426 .filter(t -> t.getName().equals(finalNextParamUnchainedName)) 427 .findFirst() 428 .orElseGet(() -> resource.addSearchParam()); 429 430 param.setName(nextParamUnchainedName); 431 if (StringUtils.isNotBlank(chain)) { 432 param.addChain(chain); 433 } else { 434 if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) { 435 for (String nextWhitelist : new TreeSet<>(nextParameter.getQualifierWhitelist())) { 436 if (nextWhitelist.startsWith(".")) { 437 param.addChain(nextWhitelist.substring(1)); 438 } 439 } 440 } 441 } 442 443 param.setDocumentation(nextParamDescription); 444 if (nextParameter.getParamType() != null) { 445 param.getTypeElement() 446 .setValueAsString(nextParameter.getParamType().getCode()); 447 } 448 for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) { 449 RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails) 450 .getFhirContext() 451 .getResourceDefinition(nextTarget); 452 if (targetDef != null) { 453 ResourceTypeEnum code = ResourceTypeEnum.VALUESET_BINDER.fromCodeString(targetDef.getName()); 454 if (code != null) { 455 param.addTarget(code); 456 } 457 } 458 } 459 } 460 } 461 } 462 463 @Read(type = OperationDefinition.class) 464 public OperationDefinition readOperationDefinition(@IdParam IdDt theId, RequestDetails theRequestDetails) { 465 if (theId == null || theId.hasIdPart() == false) { 466 throw new ResourceNotFoundException(Msg.code(1988) + theId); 467 } 468 RestfulServerConfiguration serverConfiguration = getServerConfiguration(theRequestDetails); 469 Bindings bindings = serverConfiguration.provideBindings(); 470 471 List<OperationMethodBinding> sharedDescriptions = 472 bindings.getOperationIdToBindings().get(theId.getIdPart()); 473 if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { 474 throw new ResourceNotFoundException(Msg.code(1989) + theId); 475 } 476 477 OperationDefinition op = new OperationDefinition(); 478 op.setStatus(ConformanceResourceStatusEnum.ACTIVE); 479 op.setKind(OperationKindEnum.OPERATION); 480 op.setIdempotent(true); 481 482 Set<String> inParams = new HashSet<>(); 483 Set<String> outParams = new HashSet<>(); 484 485 for (OperationMethodBinding sharedDescription : sharedDescriptions) { 486 if (isNotBlank(sharedDescription.getDescription())) { 487 op.setDescription(sharedDescription.getDescription()); 488 } 489 if (!sharedDescription.isIdempotent()) { 490 op.setIdempotent(sharedDescription.isIdempotent()); 491 } 492 op.setCode(sharedDescription.getName().substring(1)); 493 if (sharedDescription.isCanOperateAtInstanceLevel()) { 494 op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); 495 } 496 if (sharedDescription.isCanOperateAtServerLevel()) { 497 op.setSystem(sharedDescription.isCanOperateAtServerLevel()); 498 } 499 if (isNotBlank(sharedDescription.getResourceName())) { 500 op.addType().setValue(sharedDescription.getResourceName()); 501 } 502 503 for (IParameter nextParamUntyped : sharedDescription.getParameters()) { 504 if (nextParamUntyped instanceof OperationParameter) { 505 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 506 if (!inParams.add(nextParam.getName())) { 507 continue; 508 } 509 Parameter param = op.addParameter(); 510 param.setUse(OperationParameterUseEnum.IN); 511 if (nextParam.getParamType() != null) { 512 param.setType(nextParam.getParamType()); 513 } 514 param.setMin(nextParam.getMin()); 515 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 516 param.setName(nextParam.getName()); 517 } 518 } 519 520 for (ReturnType nextParam : sharedDescription.getReturnParams()) { 521 if (!outParams.add(nextParam.getName())) { 522 continue; 523 } 524 Parameter param = op.addParameter(); 525 param.setUse(OperationParameterUseEnum.OUT); 526 if (nextParam.getType() != null) { 527 param.setType(nextParam.getType()); 528 } 529 param.setMin(nextParam.getMin()); 530 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 531 param.setName(nextParam.getName()); 532 } 533 } 534 535 if (isBlank(op.getName())) { 536 if (isNotBlank(op.getDescription())) { 537 op.setName(op.getDescription()); 538 } else { 539 op.setName(op.getCode()); 540 } 541 } 542 543 if (op.getSystem() == null) { 544 op.setSystem(false); 545 } 546 if (op.getInstance() == null) { 547 op.setInstance(false); 548 } 549 550 return op; 551 } 552 553 /** 554 * Sets the cache property (default is true). If set to true, the same response will be returned for each invocation. 555 * <p> 556 * See the class documentation for an important note if you are extending this class 557 * </p> 558 * @deprecated Since 4.0.0 this does nothing 559 */ 560 @Deprecated 561 public void setCache(boolean theCache) { 562 // nothing 563 } 564 565 @Override 566 public void setRestfulServer(RestfulServer theRestfulServer) { 567 // nothing 568 } 569 570 private void sortSearchParameters(List<SearchParameter> searchParameters) { 571 Collections.sort(searchParameters, new Comparator<SearchParameter>() { 572 @Override 573 public int compare(SearchParameter theO1, SearchParameter theO2) { 574 if (theO1.isRequired() == theO2.isRequired()) { 575 return theO1.getName().compareTo(theO2.getName()); 576 } 577 if (theO1.isRequired()) { 578 return -1; 579 } 580 return 1; 581 } 582 }); 583 } 584}