
001/* 002 * #%L 003 * HAPI FHIR - Server Framework 004 * %% 005 * Copyright (C) 2014 - 2023 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 org.apache.commons.lang3.StringUtils; 037import org.apache.commons.lang3.Validate; 038import org.hl7.fhir.instance.model.api.IBaseResource; 039import org.hl7.fhir.instance.model.api.IIdType; 040import org.hl7.fhir.instance.model.api.IPrimitiveType; 041import org.slf4j.Logger; 042import org.slf4j.LoggerFactory; 043 044import javax.annotation.Nonnull; 045import javax.annotation.Nullable; 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(Map<String, Class<? extends IBaseResource>> resourceNameToSharedSupertype) { 125 this.myResourceNameToSharedSupertype = resourceNameToSharedSupertype; 126 return this; 127 } 128 129 /** 130 * Get the implementationDescription 131 * 132 * @return the implementationDescription 133 */ 134 public String getImplementationDescription() { 135 if (isBlank(myImplementationDescription)) { 136 return "HAPI FHIR"; 137 } 138 return myImplementationDescription; 139 } 140 141 /** 142 * Set the implementationDescription 143 * 144 * @param implementationDescription the implementationDescription to set 145 */ 146 public RestfulServerConfiguration setImplementationDescription(String implementationDescription) { 147 this.myImplementationDescription = implementationDescription; 148 return this; 149 } 150 151 /** 152 * Get the serverVersion 153 * 154 * @return the serverVersion 155 */ 156 public String getServerVersion() { 157 return myServerVersion; 158 } 159 160 /** 161 * Set the serverVersion 162 * 163 * @param serverVersion the serverVersion to set 164 */ 165 public RestfulServerConfiguration setServerVersion(String serverVersion) { 166 this.myServerVersion = serverVersion; 167 return this; 168 } 169 170 /** 171 * Get the serverName 172 * 173 * @return the serverName 174 */ 175 public String getServerName() { 176 return myServerName; 177 } 178 179 /** 180 * Set the serverName 181 * 182 * @param serverName the serverName to set 183 */ 184 public RestfulServerConfiguration setServerName(String serverName) { 185 this.myServerName = serverName; 186 return this; 187 } 188 189 /** 190 * 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 191 * creating their own. 192 */ 193 public FhirContext getFhirContext() { 194 return this.myFhirContext; 195 } 196 197 /** 198 * Set the fhirContext 199 * 200 * @param fhirContext the fhirContext to set 201 */ 202 public RestfulServerConfiguration setFhirContext(FhirContext fhirContext) { 203 this.myFhirContext = fhirContext; 204 return this; 205 } 206 207 /** 208 * Get the serverAddressStrategy 209 * 210 * @return the serverAddressStrategy 211 */ 212 public IServerAddressStrategy getServerAddressStrategy() { 213 return myServerAddressStrategy; 214 } 215 216 /** 217 * Set the serverAddressStrategy 218 * 219 * @param serverAddressStrategy the serverAddressStrategy to set 220 */ 221 public void setServerAddressStrategy(IServerAddressStrategy serverAddressStrategy) { 222 this.myServerAddressStrategy = serverAddressStrategy; 223 } 224 225 /** 226 * Get the date that will be specified in the conformance profile 227 * exported by this server. Typically this would be populated with 228 * an InstanceType. 229 */ 230 public IPrimitiveType<Date> getConformanceDate() { 231 return myConformanceDate; 232 } 233 234 /** 235 * Set the date that will be specified in the conformance profile 236 * exported by this server. Typically this would be populated with 237 * an InstanceType. 238 */ 239 public void setConformanceDate(IPrimitiveType<Date> theConformanceDate) { 240 myConformanceDate = theConformanceDate; 241 } 242 243 public Bindings provideBindings() { 244 IdentityHashMap<SearchMethodBinding, String> namedSearchMethodBindingToName = new IdentityHashMap<>(); 245 HashMap<String, List<SearchMethodBinding>> searchNameToBindings = new HashMap<>(); 246 IdentityHashMap<OperationMethodBinding, String> operationBindingToId = new IdentityHashMap<>(); 247 HashMap<String, List<OperationMethodBinding>> operationIdToBindings = new HashMap<>(); 248 249 Map<String, List<BaseMethodBinding>> resourceToMethods = collectMethodBindings(); 250 List<BaseMethodBinding> methodBindings = resourceToMethods 251 .values() 252 .stream().flatMap(t -> t.stream()) 253 .collect(Collectors.toList()); 254 if (myGlobalBindings != null) { 255 methodBindings.addAll(myGlobalBindings); 256 } 257 258 ListMultimap<String, OperationMethodBinding> nameToOperationMethodBindings = ArrayListMultimap.create(); 259 for (BaseMethodBinding nextMethodBinding : methodBindings) { 260 if (nextMethodBinding instanceof OperationMethodBinding) { 261 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 262 nameToOperationMethodBindings.put(methodBinding.getName(), methodBinding); 263 } else if (nextMethodBinding instanceof SearchMethodBinding) { 264 SearchMethodBinding methodBinding = (SearchMethodBinding) nextMethodBinding; 265 if (namedSearchMethodBindingToName.containsKey(methodBinding)) { 266 continue; 267 } 268 269 String name = createNamedQueryName(methodBinding); 270 ourLog.trace("Detected named query: {}", name); 271 272 namedSearchMethodBindingToName.put(methodBinding, name); 273 if (!searchNameToBindings.containsKey(name)) { 274 searchNameToBindings.put(name, new ArrayList<>()); 275 } 276 searchNameToBindings.get(name).add(methodBinding); 277 } 278 } 279 280 for (String nextName : nameToOperationMethodBindings.keySet()) { 281 List<OperationMethodBinding> nextMethodBindings = nameToOperationMethodBindings.get(nextName); 282 283 boolean global = false; 284 boolean system = false; 285 boolean instance = false; 286 boolean type = false; 287 Set<String> resourceTypes = null; 288 289 for (OperationMethodBinding nextMethodBinding : nextMethodBindings) { 290 global |= nextMethodBinding.isGlobalMethod(); 291 system |= nextMethodBinding.isCanOperateAtServerLevel(); 292 type |= nextMethodBinding.isCanOperateAtTypeLevel(); 293 instance |= nextMethodBinding.isCanOperateAtInstanceLevel(); 294 if (nextMethodBinding.getResourceName() != null) { 295 resourceTypes = resourceTypes != null ? resourceTypes : new TreeSet<>(); 296 resourceTypes.add(nextMethodBinding.getResourceName()); 297 } 298 } 299 300 StringBuilder operationIdBuilder = new StringBuilder(); 301 if (global) { 302 operationIdBuilder.append("Global"); 303 } else if (resourceTypes != null && resourceTypes.size() == 1) { 304 operationIdBuilder.append(resourceTypes.iterator().next()); 305 } else if (resourceTypes != null && resourceTypes.size() == 2) { 306 Iterator<String> iterator = resourceTypes.iterator(); 307 operationIdBuilder.append(iterator.next()); 308 operationIdBuilder.append(iterator.next()); 309 } else if (resourceTypes != null) { 310 operationIdBuilder.append("Multi"); 311 } 312 313 operationIdBuilder.append('-'); 314 if (instance) { 315 operationIdBuilder.append('i'); 316 } 317 if (type) { 318 operationIdBuilder.append('t'); 319 } 320 if (system) { 321 operationIdBuilder.append('s'); 322 } 323 operationIdBuilder.append('-'); 324 325 // Exclude the leading $ 326 operationIdBuilder.append(nextName, 1, nextName.length()); 327 328 String operationId = operationIdBuilder.toString(); 329 operationIdToBindings.put(operationId, nextMethodBindings); 330 nextMethodBindings.forEach(t->operationBindingToId.put(t, operationId)); 331 } 332 333 for (BaseMethodBinding nextMethodBinding : methodBindings) { 334 if (nextMethodBinding instanceof OperationMethodBinding) { 335 OperationMethodBinding methodBinding = (OperationMethodBinding) nextMethodBinding; 336 if (operationBindingToId.containsKey(methodBinding)) { 337 continue; 338 } 339 340 String name = createOperationName(methodBinding); 341 ourLog.debug("Detected operation: {}", name); 342 343 operationBindingToId.put(methodBinding, name); 344 if (operationIdToBindings.containsKey(name) == false) { 345 operationIdToBindings.put(name, new ArrayList<>()); 346 } 347 operationIdToBindings.get(name).add(methodBinding); 348 } 349 } 350 351 return new Bindings(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 = providers.stream() 395 .map(provider -> provider.getResourceType()) 396 .collect(Collectors.toList()); 397 providedResourceClasses.stream() 398 .forEach(resourceClass -> { 399 RuntimeResourceDefinition baseDefinition = getFhirContext().getResourceDefinition(resourceClass).getBaseDefinition(); 400 CommonResourceSupertypeScanner scanner = resourceNameToScanner.computeIfAbsent(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(String theResourceName, String theParamName) { 424 return getActiveSearchParams(theResourceName).get(theParamName); 425 } 426 427 @Override 428 public ResourceSearchParams getActiveSearchParams(@Nonnull String theResourceName) { 429 Validate.notBlank(theResourceName, "theResourceName must not be null or blank"); 430 431 ResourceSearchParams retval = new ResourceSearchParams(theResourceName); 432 433 collectMethodBindings() 434 .getOrDefault(theResourceName, Collections.emptyList()) 435 .stream() 436 .filter(t -> theResourceName.equals(t.getResourceName())) 437 .filter(t -> t instanceof SearchMethodBinding) 438 .map(t -> (SearchMethodBinding) t) 439 .filter(t -> t.getQueryName() == null) 440 .forEach(t -> createRuntimeBinding(retval, t)); 441 442 return retval; 443 } 444 445 @Nullable 446 @Override 447 public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) { 448 throw new UnsupportedOperationException(Msg.code(286)); 449 } 450 451 private void createRuntimeBinding(ResourceSearchParams theMapToPopulate, SearchMethodBinding theSearchMethodBinding) { 452 453 List<SearchParameter> parameters = theSearchMethodBinding 454 .getParameters() 455 .stream() 456 .filter(t -> t instanceof SearchParameter) 457 .map(t -> (SearchParameter) t) 458 .sorted(SearchParameterComparator.INSTANCE) 459 .collect(Collectors.toList()); 460 461 for (SearchParameter nextParameter : parameters) { 462 463 String nextParamName = nextParameter.getName(); 464 465 String nextParamUnchainedName = nextParamName; 466 if (nextParamName.contains(".")) { 467 nextParamUnchainedName = nextParamName.substring(0, nextParamName.indexOf('.')); 468 } 469 470 String nextParamDescription = nextParameter.getDescription(); 471 472 /* 473 * If the parameter has no description, default to the one from the resource 474 */ 475 if (StringUtils.isBlank(nextParamDescription)) { 476 RuntimeResourceDefinition def = getFhirContext().getResourceDefinition(theSearchMethodBinding.getResourceName()); 477 RuntimeSearchParam paramDef = def.getSearchParam(nextParamUnchainedName); 478 if (paramDef != null) { 479 nextParamDescription = paramDef.getDescription(); 480 } 481 } 482 483 if (theMapToPopulate.containsParamName(nextParamUnchainedName)) { 484 continue; 485 } 486 487 IIdType id = getFhirContext().getVersion().newIdType().setValue("SearchParameter/" + theSearchMethodBinding.getResourceName() + "-" + nextParamName); 488 String uri = null; 489 String description = nextParamDescription; 490 String path = null; 491 RestSearchParameterTypeEnum type = nextParameter.getParamType(); 492 Set<String> providesMembershipInCompartments = Collections.emptySet(); 493 Set<String> targets = Collections.emptySet(); 494 RuntimeSearchParam.RuntimeSearchParamStatusEnum status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; 495 Collection<String> base = Collections.singletonList(theSearchMethodBinding.getResourceName()); 496 RuntimeSearchParam param = new RuntimeSearchParam(id, uri, nextParamName, description, path, type, providesMembershipInCompartments, targets, status, null, null, base); 497 theMapToPopulate.put(nextParamName, param); 498 499 } 500 501 } 502 503 private static class SearchParameterComparator implements Comparator<SearchParameter> { 504 private static final SearchParameterComparator INSTANCE = new SearchParameterComparator(); 505 506 @Override 507 public int compare(SearchParameter theO1, SearchParameter theO2) { 508 if (theO1.isRequired() == theO2.isRequired()) { 509 return theO1.getName().compareTo(theO2.getName()); 510 } 511 if (theO1.isRequired()) { 512 return -1; 513 } 514 return 1; 515 } 516 } 517 518 private static String createOperationName(OperationMethodBinding theMethodBinding) { 519 StringBuilder retVal = new StringBuilder(); 520 if (theMethodBinding.getResourceName() != null) { 521 retVal.append(theMethodBinding.getResourceName()); 522 } else if (theMethodBinding.isGlobalMethod()) { 523 retVal.append("Global"); 524 } 525 526 retVal.append('-'); 527 if (theMethodBinding.isCanOperateAtInstanceLevel()) { 528 retVal.append('i'); 529 } 530 if (theMethodBinding.isCanOperateAtServerLevel()) { 531 retVal.append('s'); 532 } 533 retVal.append('-'); 534 535 // Exclude the leading $ 536 retVal.append(theMethodBinding.getName(), 1, theMethodBinding.getName().length()); 537 538 return retVal.toString(); 539 } 540}