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