001package ca.uhn.fhir.rest.server; 002 003/* 004 * #%L 005 * HAPI FHIR - Server Framework 006 * %% 007 * Copyright (C) 2014 - 2022 Smile CDR, Inc. 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.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeResourceDefinition; 025import ca.uhn.fhir.context.RuntimeSearchParam; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 028import ca.uhn.fhir.rest.server.method.BaseMethodBinding; 029import ca.uhn.fhir.rest.server.method.OperationMethodBinding; 030import ca.uhn.fhir.rest.server.method.SearchMethodBinding; 031import ca.uhn.fhir.rest.server.method.SearchParameter; 032import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; 033import ca.uhn.fhir.rest.server.util.ResourceSearchParams; 034import ca.uhn.fhir.util.VersionUtil; 035import com.google.common.collect.ArrayListMultimap; 036import com.google.common.collect.ListMultimap; 037import org.apache.commons.lang3.StringUtils; 038import org.apache.commons.lang3.Validate; 039import org.hl7.fhir.instance.model.api.IBaseResource; 040import org.hl7.fhir.instance.model.api.IIdType; 041import org.hl7.fhir.instance.model.api.IPrimitiveType; 042import org.slf4j.Logger; 043import org.slf4j.LoggerFactory; 044 045import javax.annotation.Nonnull; 046import javax.annotation.Nullable; 047import java.util.ArrayList; 048import java.util.Collection; 049import java.util.Collections; 050import java.util.Comparator; 051import java.util.Date; 052import java.util.HashMap; 053import java.util.IdentityHashMap; 054import java.util.Iterator; 055import java.util.List; 056import java.util.Map; 057import java.util.Optional; 058import java.util.Set; 059import java.util.TreeMap; 060import java.util.TreeSet; 061import java.util.stream.Collectors; 062 063import static org.apache.commons.lang3.StringUtils.isBlank; 064 065public class RestfulServerConfiguration implements ISearchParamRegistry { 066 067 public static final String GLOBAL = "GLOBAL"; 068 private static final Logger ourLog = LoggerFactory.getLogger(RestfulServerConfiguration.class); 069 private Collection<ResourceBinding> resourceBindings; 070 private List<BaseMethodBinding<?>> serverBindings; 071 private List<BaseMethodBinding<?>> myGlobalBindings; 072 private Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype; 073 private String myImplementationDescription; 074 private String myServerName = "HAPI FHIR"; 075 private String myServerVersion = VersionUtil.getVersion(); 076 private FhirContext myFhirContext; 077 private IServerAddressStrategy myServerAddressStrategy; 078 private IPrimitiveType<Date> myConformanceDate; 079 080 /** 081 * Constructor 082 */ 083 public RestfulServerConfiguration() { 084 super(); 085 } 086 087 /** 088 * Get the resourceBindings 089 * 090 * @return the resourceBindings 091 */ 092 public Collection<ResourceBinding> getResourceBindings() { 093 return resourceBindings; 094 } 095 096 /** 097 * Set the resourceBindings 098 * 099 * @param resourceBindings the resourceBindings to set 100 */ 101 public RestfulServerConfiguration setResourceBindings(Collection<ResourceBinding> resourceBindings) { 102 this.resourceBindings = resourceBindings; 103 return this; 104 } 105 106 /** 107 * Get the serverBindings 108 * 109 * @return the serverBindings 110 */ 111 public List<BaseMethodBinding<?>> getServerBindings() { 112 return serverBindings; 113 } 114 115 /** 116 * Set the theServerBindings 117 */ 118 public RestfulServerConfiguration setServerBindings(List<BaseMethodBinding<?>> theServerBindings) { 119 this.serverBindings = theServerBindings; 120 return this; 121 } 122 123 public Map<String, Class<? extends IBaseResource>> getNameToSharedSupertype() { 124 return resourceNameToSharedSupertype; 125 } 126 127 public RestfulServerConfiguration setNameToSharedSupertype(Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype) { 128 this.resourceNameToSharedSupertype = resourceNameToSharedSupertype; 129 return this; 130 } 131 132 /** 133 * Get the implementationDescription 134 * 135 * @return the implementationDescription 136 */ 137 public String getImplementationDescription() { 138 if (isBlank(myImplementationDescription)) { 139 return "HAPI FHIR"; 140 } 141 return myImplementationDescription; 142 } 143 144 /** 145 * Set the implementationDescription 146 * 147 * @param implementationDescription the implementationDescription to set 148 */ 149 public RestfulServerConfiguration setImplementationDescription(String implementationDescription) { 150 this.myImplementationDescription = implementationDescription; 151 return this; 152 } 153 154 /** 155 * Get the serverVersion 156 * 157 * @return the serverVersion 158 */ 159 public String getServerVersion() { 160 return myServerVersion; 161 } 162 163 /** 164 * Set the serverVersion 165 * 166 * @param serverVersion the serverVersion to set 167 */ 168 public RestfulServerConfiguration setServerVersion(String serverVersion) { 169 this.myServerVersion = serverVersion; 170 return this; 171 } 172 173 /** 174 * Get the serverName 175 * 176 * @return the serverName 177 */ 178 public String getServerName() { 179 return myServerName; 180 } 181 182 /** 183 * Set the serverName 184 * 185 * @param serverName the serverName to set 186 */ 187 public RestfulServerConfiguration setServerName(String serverName) { 188 this.myServerName = serverName; 189 return this; 190 } 191 192 /** 193 * Gets the {@link FhirContext} associated with this server. For efficient processing, resource providers and plain providers should generally use this context if one is needed, as opposed to 194 * creating their own. 195 */ 196 public FhirContext getFhirContext() { 197 return this.myFhirContext; 198 } 199 200 /** 201 * Set the fhirContext 202 * 203 * @param fhirContext the fhirContext to set 204 */ 205 public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) { 206 this.myFhirContext = fhirContext; 207 return this; 208 } 209 210 /** 211 * Get the serverAddressStrategy 212 * 213 * @return the serverAddressStrategy 214 */ 215 public IServerAddressStrategy getServerAddressStrategy() { 216 return myServerAddressStrategy; 217 } 218 219 /** 220 * Set the serverAddressStrategy 221 * 222 * @param serverAddressStrategy the serverAddressStrategy to set 223 */ 224 public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) { 225 this.myServerAddressStrategy = serverAddressStrategy; 226 } 227 228 /** 229 * Get the date that will be specified in the conformance profile 230 * exported by this server. Typically this would be populated with 231 * an InstanceType. 232 */ 233 public IPrimitiveType<Date> getConformanceDate() { 234 return myConformanceDate; 235 } 236 237 /** 238 * Set the date that will be specified in the conformance profile 239 * exported by this server. Typically this would be populated with 240 * an InstanceType. 241 */ 242 public void setConformanceDate(IPrimitiveType<Date> theConformanceDate) { 243 myConformanceDate = theConformanceDate; 244 } 245 246 public Bindings provideBindings() { 247 IdentityHashMap<SearchMethodBinding, String> namedSearchMethodBindingToName = new IdentityHashMap<>(); 248 HashMap<String, List<SearchMethodBinding>> searchNameToBindings = new HashMap<>(); 249 IdentityHashMap<OperationMethodBinding, String> operationBindingToId = new IdentityHashMap<>(); 250 HashMap<String, List<OperationMethodBinding>> operationIdToBindings = new HashMap<>(); 251 252 Map<String, List<BaseMethodBinding<?>>> resourceToMethods = collectMethodBindings(); 253 List<BaseMethodBinding<?>> methodBindings = resourceToMethods 254 .values() 255 .stream().flatMap(t -> t.stream()) 256 .collect(Collectors.toList()); 257 if (myGlobalBindings != null) { 258 methodBindings.addAll(myGlobalBindings); 259 } 260 261 ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create(); 262 for (BaseMethodBinding<?> nextMethodBinding : methodBindings) { 263 if (nextMethodBinding instanceof OperationMethodBinding) { 264 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 265 nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding); 266 } else if (nextMethodBinding instanceof SearchMethodBinding) { 267 SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; 268 if (namedSearchMethodBindingToName.containsKey(methodBinding)) { 269 continue; 270 } 271 272 String name = createNamedQueryName(methodBinding); 273 ourLog.debug("Detected named query: {}", name); 274 275 namedSearchMethodBindingToName.put(methodBinding, name); 276 if (!searchNameToBindings.containsKey(name)) { 277 searchNameToBindings.put(name, new ArrayList<>()); 278 } 279 searchNameToBindings.get(name).add(methodBinding); 280 } 281 } 282 283 for (String nextName : nameToOperationMethodBindings.keySet()) { 284 List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName); 285 286 boolean global = false; 287 boolean system = false; 288 boolean instance = false; 289 boolean type = false; 290 Set<String> resourceTypes = null; 291 292 for (OperationMethodBinding nextMethodBinding : nextMethodBindings) { 293 global |= nextMethodBinding.isGlobalMethod(); 294 system |= nextMethodBinding.isCanOperateAtServerLevel(); 295 type |= nextMethodBinding.isCanOperateAtTypeLevel(); 296 instance |= nextMethodBinding.isCanOperateAtInstanceLevel(); 297 if (nextMethodBinding.getResourceName() != null) { 298 resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>(); 299 resourceTypes.add(nextMethodBinding.getResourceName()); 300 } 301 } 302 303 StringBuilder operationIdBuilder = new StringBuilder(); 304 if (global) { 305 operationIdBuilder.append("Global"); 306 } else if (resourceTypes != null && resourceTypes.size() == 1) { 307 operationIdBuilder.append(resourceTypes.iterator().next()); 308 } else if (resourceTypes != null && resourceTypes.size() == 2) { 309 Iterator<String> iterator = resourceTypes.iterator(); 310 operationIdBuilder.append(iterator.next()); 311 operationIdBuilder.append(iterator.next()); 312 } else if (resourceTypes != null) { 313 operationIdBuilder.append("Multi"); 314 } 315 316 operationIdBuilder.append('-'); 317 if (instance) { 318 operationIdBuilder.append('i'); 319 } 320 if (type) { 321 operationIdBuilder.append('t'); 322 } 323 if (system) { 324 operationIdBuilder.append('s'); 325 } 326 operationIdBuilder.append('-'); 327 328 // Exclude the leading $ 329 operationIdBuilder.append(nextName, 1, nextName.length()); 330 331 String operationId = operationIdBuilder.toString(); 332 operationIdToBindings.put(operationId, nextMethodBindings); 333 nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId)); 334 } 335 336 for (BaseMethodBinding<?> nextMethodBinding : methodBindings) { 337 if (nextMethodBinding instanceof OperationMethodBinding) { 338 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 339 if (operationBindingToId.containsKey(methodBinding)) { 340 continue; 341 } 342 343 String name = createOperationName(methodBinding); 344 ourLog.debug("Detected operation: {}", name); 345 346 operationBindingToId.put(methodBinding, name); 347 if (operationIdToBindings.containsKey(name) == false) { 348 operationIdToBindings.put(name, new ArrayList<>()); 349 } 350 operationIdToBindings.get(name).add(methodBinding); 351 } 352 } 353 354 return new Bindings(namedSearchMethodBindingToName, searchNameToBindings, operationIdToBindings, operationBindingToId); 355 } 356 357 public Map<String, List<BaseMethodBinding<?>>> collectMethodBindings() { 358 Map<String, List<BaseMethodBinding<?>>> resourceToMethods = new TreeMap<>(); 359 for (ResourceBinding next : getResourceBindings()) { 360 String resourceName = next.getResourceName(); 361 for (BaseMethodBinding<?> nextMethodBinding : next.getMethodBindings()) { 362 if (resourceToMethods.containsKey(resourceName) == false) { 363 resourceToMethods.put(resourceName, new ArrayList<>()); 364 } 365 resourceToMethods.get(resourceName).add(nextMethodBinding); 366 } 367 } 368 for (BaseMethodBinding<?> nextMethodBinding : getServerBindings()) { 369 String resourceName = ""; 370 if (resourceToMethods.containsKey(resourceName) == false) { 371 resourceToMethods.put(resourceName, new ArrayList<>()); 372 } 373 resourceToMethods.get(resourceName).add(nextMethodBinding); 374 } 375 return resourceToMethods; 376 } 377 378 public List<BaseMethodBinding<?>> getGlobalBindings() { 379 return myGlobalBindings; 380 } 381 382 public void setGlobalBindings(List<BaseMethodBinding<?>> theGlobalBindings) { 383 myGlobalBindings = theGlobalBindings; 384 } 385 386 /* 387 * Populates {@link #resourceNameToSharedSupertype} by scanning the given resource providers. Only resource provider getResourceType values 388 * are taken into account. {@link ProvidesResources} and method return types are deliberately ignored. 389 * 390 * Given a resource name, the common superclass for all getResourceType return values for that name's providers is the common superclass 391 * for all returned/received resources with that name. Since {@link ProvidesResources} resources and method return types must also be 392 * subclasses of this common supertype, they can't affect the result of this method. 393 */ 394 public void computeSharedSupertypeForResourcePerName(Collection<IResourceProvider> providers) { 395 Map<String, CommonResourceSupertypeScanner> resourceNameToScanner = new HashMap<>(); 396 397 List<Class<? extends IBaseResource>> providedResourceClasses = providers.stream() 398 .map(provider -> provider.getResourceType()) 399 .collect(Collectors.toList()); 400 providedResourceClasses.stream() 401 .forEach(resourceClass -> { 402 RuntimeResourceDefinition baseDefinition = getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition(); 403 CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(baseDefinition.getName(), key -> new CommonResourceSupertypeScanner()); 404 scanner.register(resourceClass); 405 }); 406 407 resourceNameToSharedSupertype = resourceNameToScanner.entrySet().stream() 408 .filter(entry -> entry.getValue().getLowestCommonSuperclass().isPresent()) 409 .collect(Collectors.toMap( 410 entry -> entry.getKey(), 411 entry -> entry.getValue().getLowestCommonSuperclass().get())); 412 } 413 414 private String createNamedQueryName(SearchMethodBinding searchMethodBinding) { 415 StringBuilder retVal = new StringBuilder(); 416 if (searchMethodBinding.getResourceName() != null) { 417 retVal.append(searchMethodBinding.getResourceName()); 418 } 419 retVal.append("-query-"); 420 retVal.append(searchMethodBinding.getQueryName()); 421 422 return retVal.toString(); 423 } 424 425 @Override 426 public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) { 427 return getActiveSearchParams(theResourceName).get(theParamName); 428 } 429 430 @Override 431 public ResourceSearchParams getActiveSearchParams(@Nonnull String theResourceName) { 432 Validate.notBlank(theResourceName, "theResourceName must not be null or blank"); 433 434 ResourceSearchParams retval = new ResourceSearchParams(theResourceName); 435 436 collectMethodBindings() 437 .getOrDefault(theResourceName, Collections.emptyList()) 438 .stream() 439 .filter(t -> theResourceName.equals(t.getResourceName())) 440 .filter(t -> t instanceof SearchMethodBinding) 441 .map(t -> (SearchMethodBinding) t) 442 .filter(t -> t.getQueryName() == null) 443 .forEach(t -> createRuntimeBinding(retval, t)); 444 445 return retval; 446 } 447 448 @Nullable 449 @Override 450 public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) { 451 throw new UnsupportedOperationException(Msg.code(286)); 452 } 453 454 private void createRuntimeBinding(ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) { 455 456 List<SearchParameter> parameters = theSearchMethodBinding 457 .getParameters() 458 .stream() 459 .filter(t -> t instanceof SearchParameter) 460 .map(t -> (SearchParameter) t) 461 .sorted(SearchParameterComparator.INSTANCE) 462 .collect(Collectors.toList()); 463 464 for (SearchParameter nextParameter : parameters) { 465 466 String nextParamName = nextParameter.getName(); 467 468 String nextParamUnchainedName = nextParamName; 469 if (nextParamName.contains(".")) { 470 nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); 471 } 472 473 String nextParamDescription = nextParameter.getDescription(); 474 475 /* 476 * If the parameter has no description, default to the one from the resource 477 */ 478 if (StringUtils.isBlank(nextParamDescription)) { 479 RuntimeResourceDefinition def = getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName()); 480 RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); 481 if (paramDef != null) { 482 nextParamDescription = paramDef.getDescription(); 483 } 484 } 485 486 if (theMapToPopulate.containsParamName(nextParamUnchainedName)) { 487 continue; 488 } 489 490 IIdType id = getFhirContext().getVersion().newIdType().setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName); 491 String uri = null; 492 String description = nextParamDescription; 493 String path = null; 494 RestSearchParameterTypeEnum type = nextParameter.getParamType(); 495 Set<String> providesMembershipInCompartments = Collections.emptySet(); 496 Set<String> targets = Collections.emptySet(); 497 RuntimeSearchParam.RuntimeSearchParamStatusEnum status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; 498 Collection<String> base = Collections.singletonList(theSearchMethodBinding.getResourceName()); 499 RuntimeSearchParam param = new RuntimeSearchParam(id, uri, nextParamName, description, path, type, providesMembershipInCompartments, targets, status, null, null, base); 500 theMapToPopulate.put(nextParamName, param); 501 502 } 503 504 } 505 506 private static class SearchParameterComparator implements Comparator<SearchParameter> { 507 private static final SearchParameterComparator INSTANCE = new SearchParameterComparator(); 508 509 @Override 510 public int compare(SearchParameter theO1, SearchParameter theO2) { 511 if (theO1.isRequired() == theO2.isRequired()) { 512 return theO1.getName().compareTo(theO2.getName()); 513 } 514 if (theO1.isRequired()) { 515 return -1; 516 } 517 return 1; 518 } 519 } 520 521 private static String createOperationName(OperationMethodBinding theMethodBinding) { 522 StringBuilder retVal = new StringBuilder(); 523 if (theMethodBinding.getResourceName() != null) { 524 retVal.append(theMethodBinding.getResourceName()); 525 } else if (theMethodBinding.isGlobalMethod()) { 526 retVal.append("Global"); 527 } 528 529 retVal.append('-'); 530 if (theMethodBinding.isCanOperateAtInstanceLevel()) { 531 retVal.append('i'); 532 } 533 if (theMethodBinding.isCanOperateAtServerLevel()) { 534 retVal.append('s'); 535 } 536 retVal.append('-'); 537 538 // Exclude the leading $ 539 retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length()); 540 541 return retVal.toString(); 542 } 543}