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