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