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