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.*; 051import org.hl7.fhir.dstu2.model.DateTimeType; 052import org.hl7.fhir.dstu2.model.Enumerations.ConformanceResourceStatus; 053import org.hl7.fhir.dstu2.model.Enumerations.ResourceType; 054import org.hl7.fhir.dstu2.model.IdType; 055import org.hl7.fhir.dstu2.model.OperationDefinition; 056import org.hl7.fhir.dstu2.model.OperationDefinition.OperationDefinitionParameterComponent; 057import org.hl7.fhir.dstu2.model.OperationDefinition.OperationParameterUse; 058import org.hl7.fhir.instance.model.api.IBaseResource; 059import org.hl7.fhir.instance.model.api.IPrimitiveType; 060 061import java.util.Map.Entry; 062import java.util.*; 063 064import static org.apache.commons.lang3.StringUtils.isNotBlank; 065 066/** 067 * Server FHIR Provider which serves the conformance statement for a RESTful 068 * server implementation 069 * 070 * <p> 071 * Note: This class is safe to extend, but it is important to note that the same 072 * instance of {@link Conformance} is always returned unless 073 * {@link #setCache(boolean)} is called with a value of <code>false</code>. This 074 * means that if you are adding anything to the returned conformance instance on 075 * each call you should call <code>setCache(false)</code> in your provider 076 * constructor. 077 * </p> 078 */ 079public class ServerConformanceProvider extends BaseServerCapabilityStatementProvider 080 implements IServerConformanceProvider<Conformance> { 081 082 private String myPublisher = "Not provided"; 083 084 /** 085 * No-arg constructor and seetter so that the ServerConfirmanceProvider can be Spring-wired with the RestfulService avoiding the potential reference cycle that would happen. 086 */ 087 public ServerConformanceProvider() { 088 super(); 089 } 090 091 /** 092 * Constructor 093 * 094 * @deprecated Use no-args constructor instead. Deprecated in 4.0.0 095 */ 096 @Deprecated 097 public ServerConformanceProvider(RestfulServer theRestfulServer) { 098 this(); 099 } 100 101 /** 102 * Constructor 103 */ 104 public ServerConformanceProvider(RestfulServerConfiguration theServerConfiguration) { 105 super(theServerConfiguration); 106 } 107 108 @Override 109 public void setRestfulServer(RestfulServer theRestfulServer) { 110 // ignore 111 } 112 113 private void checkBindingForSystemOps( 114 ConformanceRestComponent rest, 115 Set<SystemRestfulInteraction> systemOps, 116 BaseMethodBinding nextMethodBinding) { 117 if (nextMethodBinding.getRestOperationType() != null) { 118 String sysOpCode = nextMethodBinding.getRestOperationType().getCode(); 119 if (sysOpCode != null) { 120 SystemRestfulInteraction sysOp; 121 try { 122 sysOp = SystemRestfulInteraction.fromCode(sysOpCode); 123 } catch (Exception e) { 124 sysOp = null; 125 } 126 if (sysOp == null) { 127 return; 128 } 129 if (systemOps.contains(sysOp) == false) { 130 systemOps.add(sysOp); 131 rest.addInteraction().setCode(sysOp); 132 } 133 } 134 } 135 } 136 137 private Map<String, List<BaseMethodBinding>> collectMethodBindings(RequestDetails theRequestDetails) { 138 Map<String, List<BaseMethodBinding>> resourceToMethods = new TreeMap<String, List<BaseMethodBinding>>(); 139 for (ResourceBinding next : getServerConfiguration(theRequestDetails).getResourceBindings()) { 140 String resourceName = next.getResourceName(); 141 for (BaseMethodBinding nextMethodBinding : next.getMethodBindings()) { 142 if (resourceToMethods.containsKey(resourceName) == false) { 143 resourceToMethods.put(resourceName, new ArrayList<BaseMethodBinding>()); 144 } 145 resourceToMethods.get(resourceName).add(nextMethodBinding); 146 } 147 } 148 for (BaseMethodBinding nextMethodBinding : 149 getServerConfiguration(theRequestDetails).getServerBindings()) { 150 String resourceName = ""; 151 if (resourceToMethods.containsKey(resourceName) == false) { 152 resourceToMethods.put(resourceName, new ArrayList<>()); 153 } 154 resourceToMethods.get(resourceName).add(nextMethodBinding); 155 } 156 return resourceToMethods; 157 } 158 159 private String createOperationName(OperationMethodBinding theMethodBinding) { 160 return theMethodBinding.getName().substring(1); 161 } 162 163 /** 164 * Gets the value of the "publisher" that will be placed in the generated 165 * conformance statement. As this is a mandatory element, the value should not 166 * be null (although this is not enforced). The value defaults to 167 * "Not provided" but may be set to null, which will cause this element to be 168 * 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_HL7ORG.getFhirVersionString()); 186 retVal.setAcceptUnknown( 187 UnknownContentCode 188 .EXTENSIONS); // TODO: make this configurable - this is a fairly big effort since the parser 189 // needs to be modified to actually allow it 190 191 retVal.getImplementation().setDescription(serverConfiguration.getImplementationDescription()); 192 retVal.setKind(ConformanceStatementKind.INSTANCE); 193 retVal.getSoftware().setName(serverConfiguration.getServerName()); 194 retVal.getSoftware().setVersion(serverConfiguration.getServerVersion()); 195 retVal.addFormat(Constants.CT_FHIR_XML); 196 retVal.addFormat(Constants.CT_FHIR_JSON); 197 198 ConformanceRestComponent rest = retVal.addRest(); 199 rest.setMode(RestfulConformanceMode.SERVER); 200 201 Set<SystemRestfulInteraction> systemOps = new HashSet<>(); 202 Set<String> operationNames = new HashSet<>(); 203 204 Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings(theRequestDetails); 205 for (Entry<String, List<BaseMethodBinding>> nextEntry : resourceToMethods.entrySet()) { 206 207 if (nextEntry.getKey().isEmpty() == false) { 208 Set<TypeRestfulInteraction> resourceOps = new HashSet<>(); 209 ConformanceRestResourceComponent resource = rest.addResource(); 210 String resourceName = nextEntry.getKey(); 211 RuntimeResourceDefinition def = 212 serverConfiguration.getFhirContext().getResourceDefinition(resourceName); 213 resource.getTypeElement().setValue(def.getName()); 214 ServletContext servletContext = (ServletContext) 215 (theRequest == null ? null : theRequest.getAttribute(RestfulServer.SERVLET_CONTEXT_ATTRIBUTE)); 216 String serverBase = 217 serverConfiguration.getServerAddressStrategy().determineServerBase(servletContext, theRequest); 218 resource.getProfile().setReference((def.getResourceProfile(serverBase))); 219 220 TreeSet<String> includes = new TreeSet<>(); 221 222 // Map<String, Conformance.RestResourceSearchParam> nameToSearchParam = 223 // new HashMap<String, 224 // Conformance.RestResourceSearchParam>(); 225 for (BaseMethodBinding nextMethodBinding : nextEntry.getValue()) { 226 if (nextMethodBinding.getRestOperationType() != null) { 227 String resOpCode = 228 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 resource.setConditionalDelete(ConditionalDeleteStatus.SINGLE); 257 break; 258 case UPDATE: 259 resource.setConditionalUpdate(true); 260 break; 261 default: 262 break; 263 } 264 } 265 } 266 } 267 } 268 269 checkBindingForSystemOps(rest, systemOps, nextMethodBinding); 270 271 if (nextMethodBinding instanceof SearchMethodBinding) { 272 handleSearchMethodBinding( 273 resource, def, includes, (SearchMethodBinding) nextMethodBinding, theRequestDetails); 274 } else if (nextMethodBinding instanceof OperationMethodBinding) { 275 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 276 String opName = bindings.getOperationBindingToId().get(methodBinding); 277 if (operationNames.add(opName)) { 278 // Only add each operation (by name) once 279 rest.addOperation() 280 .setName(methodBinding.getName()) 281 .getDefinition() 282 .setReference("OperationDefinition/" + opName); 283 } 284 } 285 286 Collections.sort(resource.getInteraction(), 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 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.getOperationBindingToId().get(methodBinding); 314 if (operationNames.add(opName)) { 315 rest.addOperation() 316 .setName(methodBinding.getName()) 317 .getDefinition() 318 .setReference("OperationDefinition/" + opName); 319 } 320 } 321 } 322 } 323 } 324 325 return retVal; 326 } 327 328 private DateTimeType conformanceDate(RequestDetails theRequestDetails) { 329 IPrimitiveType<Date> buildDate = 330 getServerConfiguration(theRequestDetails).getConformanceDate(); 331 if (buildDate != null && buildDate.getValue() != null) { 332 try { 333 return new DateTimeType(buildDate.getValueAsString()); 334 } catch (DataFormatException e) { 335 // fall through 336 } 337 } 338 return DateTimeType.now(); 339 } 340 341 private void handleSearchMethodBinding( 342 ConformanceRestResourceComponent resource, 343 RuntimeResourceDefinition def, 344 TreeSet<String> includes, 345 SearchMethodBinding searchMethodBinding, 346 RequestDetails theRequestDetails) { 347 includes.addAll(searchMethodBinding.getIncludes()); 348 349 List<IParameter> params = searchMethodBinding.getParameters(); 350 List<SearchParameter> searchParameters = new ArrayList<>(); 351 for (IParameter nextParameter : params) { 352 if ((nextParameter instanceof SearchParameter)) { 353 searchParameters.add((SearchParameter) nextParameter); 354 } 355 } 356 sortSearchParameters(searchParameters); 357 if (!searchParameters.isEmpty()) { 358 // boolean allOptional = searchParameters.get(0).isRequired() == false; 359 // 360 // OperationDefinition query = null; 361 // if (!allOptional) { 362 // RestOperation operation = rest.addOperation(); 363 // query = new OperationDefinition(); 364 // operation.setDefinition(new ResourceReferenceDt(query)); 365 // query.getDescriptionElement().setValue(searchMethodBinding.getDescription()); 366 // query.addUndeclaredExtension(false, 367 // ExtensionConstants.QUERY_RETURN_TYPE, new CodeDt(resourceName)); 368 // for (String nextInclude : searchMethodBinding.getIncludes()) { 369 // query.addUndeclaredExtension(false, 370 // ExtensionConstants.QUERY_ALLOWED_INCLUDE, new StringDt(nextInclude)); 371 // } 372 // } 373 374 for (SearchParameter nextParameter : searchParameters) { 375 376 String nextParamName = nextParameter.getName(); 377 378 String chain = null; 379 String nextParamUnchainedName = nextParamName; 380 if (nextParamName.contains(".")) { 381 chain = nextParamName.substring(nextParamName.indexOf('.') + 1); 382 nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); 383 } 384 385 String nextParamDescription = nextParameter.getDescription(); 386 387 /* 388 * If the parameter has no description, default to the one from the 389 * resource 390 */ 391 if (StringUtils.isBlank(nextParamDescription)) { 392 RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); 393 if (paramDef != null) { 394 nextParamDescription = paramDef.getDescription(); 395 } 396 } 397 398 ConformanceRestResourceSearchParamComponent param = resource.addSearchParam(); 399 param.setName(nextParamUnchainedName); 400 if (StringUtils.isNotBlank(chain)) { 401 param.addChain(chain); 402 } 403 param.setDocumentation(nextParamDescription); 404 if (nextParameter.getParamType() != null) { 405 param.getTypeElement() 406 .setValueAsString(nextParameter.getParamType().getCode()); 407 } 408 for (Class<? extends IBaseResource> nextTarget : nextParameter.getDeclaredTypes()) { 409 RuntimeResourceDefinition targetDef = getServerConfiguration(theRequestDetails) 410 .getFhirContext() 411 .getResourceDefinition(nextTarget); 412 if (targetDef != null) { 413 ResourceType code; 414 try { 415 code = ResourceType.fromCode(targetDef.getName()); 416 } catch (Exception e) { 417 code = null; 418 } 419 if (code != null) { 420 param.addTarget(code.toCode()); 421 } 422 } 423 } 424 } 425 } 426 } 427 428 @Read(type = OperationDefinition.class) 429 public OperationDefinition readOperationDefinition(@IdParam IdType theId, RequestDetails theRequestDetails) { 430 if (theId == null || theId.hasIdPart() == false) { 431 throw new ResourceNotFoundException(Msg.code(1986) + theId); 432 } 433 List<OperationMethodBinding> sharedDescriptions = getServerConfiguration(theRequestDetails) 434 .provideBindings() 435 .getOperationIdToBindings() 436 .get(theId.getIdPart()); 437 if (sharedDescriptions == null || sharedDescriptions.isEmpty()) { 438 throw new ResourceNotFoundException(Msg.code(1987) + theId); 439 } 440 441 OperationDefinition op = new OperationDefinition(); 442 op.setStatus(ConformanceResourceStatus.ACTIVE); 443 op.setIdempotent(true); 444 445 Set<String> inParams = new HashSet<>(); 446 Set<String> outParams = new HashSet<>(); 447 448 for (OperationMethodBinding sharedDescription : sharedDescriptions) { 449 if (isNotBlank(sharedDescription.getDescription())) { 450 op.setDescription(sharedDescription.getDescription()); 451 } 452 if (!sharedDescription.isIdempotent()) { 453 op.setIdempotent(sharedDescription.isIdempotent()); 454 } 455 op.setCode(sharedDescription.getName()); 456 if (sharedDescription.isCanOperateAtInstanceLevel()) { 457 op.setInstance(sharedDescription.isCanOperateAtInstanceLevel()); 458 } 459 if (sharedDescription.isCanOperateAtServerLevel()) { 460 op.setSystem(sharedDescription.isCanOperateAtServerLevel()); 461 } 462 if (isNotBlank(sharedDescription.getResourceName())) { 463 op.addTypeElement().setValue(sharedDescription.getResourceName()); 464 } 465 466 for (IParameter nextParamUntyped : sharedDescription.getParameters()) { 467 if (nextParamUntyped instanceof OperationParameter) { 468 OperationParameter nextParam = (OperationParameter) nextParamUntyped; 469 OperationDefinitionParameterComponent param = op.addParameter(); 470 if (!inParams.add(nextParam.getName())) { 471 continue; 472 } 473 param.setUse(OperationParameterUse.IN); 474 if (nextParam.getParamType() != null) { 475 param.setType(nextParam.getParamType()); 476 } 477 param.setMin(nextParam.getMin()); 478 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 479 param.setName(nextParam.getName()); 480 } 481 } 482 483 for (ReturnType nextParam : sharedDescription.getReturnParams()) { 484 if (!outParams.add(nextParam.getName())) { 485 continue; 486 } 487 OperationDefinitionParameterComponent param = op.addParameter(); 488 param.setUse(OperationParameterUse.OUT); 489 if (nextParam.getType() != null) { 490 param.setType(nextParam.getType()); 491 } 492 param.setMin(nextParam.getMin()); 493 param.setMax(nextParam.getMax() == -1 ? "*" : Integer.toString(nextParam.getMax())); 494 param.setName(nextParam.getName()); 495 } 496 } 497 498 return op; 499 } 500 501 /** 502 * Sets the cache property (default is true). If set to true, the same 503 * response will be returned for each invocation. 504 * <p> 505 * See the class documentation for an important note if you are extending this 506 * class 507 * </p> 508 * @deprecated Since 4.0.0 this method doesn't do anything 509 */ 510 @Deprecated 511 public void setCache(boolean theCache) { 512 // nothing 513 } 514 515 /** 516 * Sets the value of the "publisher" that will be placed in the generated 517 * conformance statement. As this is a mandatory element, the value should not 518 * be null (although this is not enforced). The value defaults to 519 * "Not provided" but may be set to null, which will cause this element to be 520 * omitted. 521 */ 522 public void setPublisher(String thePublisher) { 523 myPublisher = thePublisher; 524 } 525 526 private void sortSearchParameters(List<SearchParameter> searchParameters) { 527 Collections.sort(searchParameters, new Comparator<SearchParameter>() { 528 @Override 529 public int compare(SearchParameter theO1, SearchParameter theO2) { 530 if (theO1.isRequired() == theO2.isRequired()) { 531 return theO1.getName().compareTo(theO2.getName()); 532 } 533 if (theO1.isRequired()) { 534 return -1; 535 } 536 return 1; 537 } 538 }); 539 } 540}