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