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