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