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