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