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