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