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