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