
001/*- 002 * #%L 003 * HAPI FHIR JPA - Search Parameters 004 * %% 005 * Copyright (C) 2014 - 2025 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.jpa.searchparam.registry; 021 022import ca.uhn.fhir.context.ComboSearchParamType; 023import ca.uhn.fhir.context.FhirContext; 024import ca.uhn.fhir.context.RuntimeSearchParam; 025import ca.uhn.fhir.context.phonetic.IPhoneticEncoder; 026import ca.uhn.fhir.i18n.Msg; 027import ca.uhn.fhir.model.api.ExtensionDt; 028import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum; 029import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 030import ca.uhn.fhir.util.DatatypeUtil; 031import ca.uhn.fhir.util.ExtensionUtil; 032import ca.uhn.fhir.util.FhirTerser; 033import ca.uhn.fhir.util.HapiExtensions; 034import ca.uhn.fhir.util.PhoneticEncoderUtil; 035import org.apache.commons.lang3.StringUtils; 036import org.apache.commons.lang3.Strings; 037import org.hl7.fhir.dstu3.model.Extension; 038import org.hl7.fhir.dstu3.model.SearchParameter; 039import org.hl7.fhir.instance.model.api.IBase; 040import org.hl7.fhir.instance.model.api.IBaseDatatype; 041import org.hl7.fhir.instance.model.api.IBaseExtension; 042import org.hl7.fhir.instance.model.api.IBaseHasExtensions; 043import org.hl7.fhir.instance.model.api.IBaseResource; 044import org.hl7.fhir.instance.model.api.IIdType; 045import org.hl7.fhir.instance.model.api.IPrimitiveType; 046import org.slf4j.Logger; 047import org.slf4j.LoggerFactory; 048import org.springframework.beans.factory.annotation.Autowired; 049import org.springframework.stereotype.Service; 050 051import java.util.ArrayList; 052import java.util.Collection; 053import java.util.Collections; 054import java.util.HashSet; 055import java.util.List; 056import java.util.Set; 057import java.util.stream.Collectors; 058 059import static org.apache.commons.lang3.StringUtils.isBlank; 060import static org.apache.commons.lang3.StringUtils.isNotBlank; 061 062@Service 063public class SearchParameterCanonicalizer { 064 private static final Logger ourLog = LoggerFactory.getLogger(SearchParameterCanonicalizer.class); 065 066 private final FhirContext myFhirContext; 067 private final FhirTerser myTerser; 068 069 @Autowired 070 public SearchParameterCanonicalizer(FhirContext theFhirContext) { 071 myFhirContext = theFhirContext; 072 myTerser = myFhirContext.newTerser(); 073 } 074 075 private static Collection<String> toStrings(Collection<? extends IPrimitiveType<String>> theBase) { 076 HashSet<String> retVal = new HashSet<>(); 077 for (IPrimitiveType<String> next : theBase) { 078 if (isNotBlank(next.getValueAsString())) { 079 retVal.add(next.getValueAsString()); 080 } 081 } 082 return retVal; 083 } 084 085 public RuntimeSearchParam canonicalizeSearchParameter(IBaseResource theSearchParameter) { 086 RuntimeSearchParam retVal; 087 switch (myFhirContext.getVersion().getVersion()) { 088 case DSTU2: 089 retVal = canonicalizeSearchParameterDstu2( 090 (ca.uhn.fhir.model.dstu2.resource.SearchParameter) theSearchParameter); 091 break; 092 case DSTU3: 093 retVal = 094 canonicalizeSearchParameterDstu3((org.hl7.fhir.dstu3.model.SearchParameter) theSearchParameter); 095 break; 096 case R4: 097 case R4B: 098 case R5: 099 retVal = canonicalizeSearchParameterR4Plus(theSearchParameter); 100 break; 101 case DSTU2_HL7ORG: 102 case DSTU2_1: 103 // Non-supported - these won't happen so just fall through 104 default: 105 throw new InternalErrorException( 106 Msg.code(510) + "SearchParameter canonicalization not supported for FHIR version" 107 + myFhirContext.getVersion().getVersion()); 108 } 109 110 if (retVal != null) { 111 extractExtensions(theSearchParameter, retVal); 112 } 113 114 return retVal; 115 } 116 117 private RuntimeSearchParam canonicalizeSearchParameterDstu2( 118 ca.uhn.fhir.model.dstu2.resource.SearchParameter theNextSp) { 119 String name = theNextSp.getCode(); 120 String description = theNextSp.getDescription(); 121 String path = theNextSp.getXpath(); 122 123 Collection<String> baseResource = toStrings(Collections.singletonList(theNextSp.getBaseElement())); 124 List<String> baseCustomResources = extractDstu2CustomResourcesFromExtensions( 125 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); 126 127 if (!baseCustomResources.isEmpty()) { 128 baseResource = Collections.singleton(baseCustomResources.get(0)); 129 } 130 131 RestSearchParameterTypeEnum paramType = null; 132 RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; 133 if (theNextSp.getTypeElement().getValueAsEnum() != null) { 134 switch (theNextSp.getTypeElement().getValueAsEnum()) { 135 case COMPOSITE: 136 paramType = RestSearchParameterTypeEnum.COMPOSITE; 137 break; 138 case DATE_DATETIME: 139 paramType = RestSearchParameterTypeEnum.DATE; 140 break; 141 case NUMBER: 142 paramType = RestSearchParameterTypeEnum.NUMBER; 143 break; 144 case QUANTITY: 145 paramType = RestSearchParameterTypeEnum.QUANTITY; 146 break; 147 case REFERENCE: 148 paramType = RestSearchParameterTypeEnum.REFERENCE; 149 break; 150 case STRING: 151 paramType = RestSearchParameterTypeEnum.STRING; 152 break; 153 case TOKEN: 154 paramType = RestSearchParameterTypeEnum.TOKEN; 155 break; 156 case URI: 157 paramType = RestSearchParameterTypeEnum.URI; 158 break; 159 } 160 } 161 if (theNextSp.getStatus() != null) { 162 switch (theNextSp.getStatusElement().getValueAsEnum()) { 163 case ACTIVE: 164 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; 165 break; 166 case DRAFT: 167 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; 168 break; 169 case RETIRED: 170 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; 171 break; 172 } 173 } 174 175 Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget()); 176 List<String> targetCustomResources = extractDstu2CustomResourcesFromExtensions( 177 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); 178 179 maybeAddCustomResourcesToResources(targetResources, targetCustomResources); 180 181 if (isBlank(name) || isBlank(path)) { 182 if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { 183 return null; 184 } 185 } 186 187 IIdType id = theNextSp.getIdElement(); 188 String uri = ""; 189 ComboSearchParamType unique = null; 190 191 List<ExtensionDt> uniqueExts = theNextSp.getUndeclaredExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE); 192 if (uniqueExts.size() > 0) { 193 IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); 194 if (uniqueExtsValuePrimitive != null) { 195 if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { 196 unique = ComboSearchParamType.UNIQUE; 197 } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { 198 unique = ComboSearchParamType.NON_UNIQUE; 199 } 200 } 201 } 202 203 List<RuntimeSearchParam.Component> components = Collections.emptyList(); 204 return new RuntimeSearchParam( 205 id, 206 uri, 207 name, 208 description, 209 path, 210 paramType, 211 Collections.emptySet(), 212 targetResources, 213 status, 214 unique, 215 components, 216 baseResource); 217 } 218 219 private RuntimeSearchParam canonicalizeSearchParameterDstu3(org.hl7.fhir.dstu3.model.SearchParameter theNextSp) { 220 String name = theNextSp.getCode(); 221 String description = theNextSp.getDescription(); 222 String path = theNextSp.getExpression(); 223 224 List<String> baseResources = new ArrayList<>(toStrings(theNextSp.getBase())); 225 List<String> baseCustomResources = extractDstu3CustomResourcesFromExtensions( 226 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); 227 228 maybeAddCustomResourcesToResources(baseResources, baseCustomResources); 229 230 RestSearchParameterTypeEnum paramType = null; 231 RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; 232 if (theNextSp.getType() != null) { 233 switch (theNextSp.getType()) { 234 case COMPOSITE: 235 paramType = RestSearchParameterTypeEnum.COMPOSITE; 236 break; 237 case DATE: 238 paramType = RestSearchParameterTypeEnum.DATE; 239 break; 240 case NUMBER: 241 paramType = RestSearchParameterTypeEnum.NUMBER; 242 break; 243 case QUANTITY: 244 paramType = RestSearchParameterTypeEnum.QUANTITY; 245 break; 246 case REFERENCE: 247 paramType = RestSearchParameterTypeEnum.REFERENCE; 248 break; 249 case STRING: 250 paramType = RestSearchParameterTypeEnum.STRING; 251 break; 252 case TOKEN: 253 paramType = RestSearchParameterTypeEnum.TOKEN; 254 break; 255 case URI: 256 paramType = RestSearchParameterTypeEnum.URI; 257 break; 258 case NULL: 259 break; 260 } 261 } 262 if (theNextSp.getStatus() != null) { 263 switch (theNextSp.getStatus()) { 264 case ACTIVE: 265 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; 266 break; 267 case DRAFT: 268 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; 269 break; 270 case RETIRED: 271 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; 272 break; 273 case UNKNOWN: 274 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; 275 break; 276 case NULL: 277 break; 278 } 279 } 280 281 Set<String> targetResources = DatatypeUtil.toStringSet(theNextSp.getTarget()); 282 List<String> targetCustomResources = extractDstu3CustomResourcesFromExtensions( 283 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); 284 285 maybeAddCustomResourcesToResources(targetResources, targetCustomResources); 286 287 if (isBlank(name) || isBlank(path) || paramType == null) { 288 if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { 289 return null; 290 } 291 } 292 293 IIdType id = theNextSp.getIdElement(); 294 String uri = ""; 295 ComboSearchParamType unique = null; 296 297 List<Extension> uniqueExts = theNextSp.getExtensionsByUrl(HapiExtensions.EXT_SP_UNIQUE); 298 if (uniqueExts.size() > 0) { 299 IPrimitiveType<?> uniqueExtsValuePrimitive = uniqueExts.get(0).getValueAsPrimitive(); 300 if (uniqueExtsValuePrimitive != null) { 301 if ("true".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { 302 unique = ComboSearchParamType.UNIQUE; 303 } else if ("false".equalsIgnoreCase(uniqueExtsValuePrimitive.getValueAsString())) { 304 unique = ComboSearchParamType.NON_UNIQUE; 305 } 306 } 307 } 308 309 List<RuntimeSearchParam.Component> components = new ArrayList<>(); 310 for (SearchParameter.SearchParameterComponentComponent next : theNextSp.getComponent()) { 311 components.add(new RuntimeSearchParam.Component( 312 next.getExpression(), 313 next.getDefinition() 314 .getReferenceElement() 315 .toUnqualifiedVersionless() 316 .getValue(), 317 null)); 318 } 319 320 return new RuntimeSearchParam( 321 id, 322 uri, 323 name, 324 description, 325 path, 326 paramType, 327 Collections.emptySet(), 328 targetResources, 329 status, 330 unique, 331 components, 332 baseResources); 333 } 334 335 private RuntimeSearchParam canonicalizeSearchParameterR4Plus(IBaseResource theNextSp) { 336 337 String name = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "code"); 338 String description = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "description"); 339 String path = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "expression"); 340 341 Set<String> baseResources = extractR4PlusResources("base", theNextSp); 342 List<String> baseCustomResources = extractR4PlusCustomResourcesFromExtensions( 343 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE); 344 345 maybeAddCustomResourcesToResources(baseResources, baseCustomResources); 346 347 RestSearchParameterTypeEnum paramType = null; 348 RuntimeSearchParam.RuntimeSearchParamStatusEnum status = null; 349 switch (myTerser.getSinglePrimitiveValue(theNextSp, "type").orElse("")) { 350 case "composite": 351 paramType = RestSearchParameterTypeEnum.COMPOSITE; 352 break; 353 case "date": 354 paramType = RestSearchParameterTypeEnum.DATE; 355 break; 356 case "number": 357 paramType = RestSearchParameterTypeEnum.NUMBER; 358 break; 359 case "quantity": 360 paramType = RestSearchParameterTypeEnum.QUANTITY; 361 break; 362 case "reference": 363 paramType = RestSearchParameterTypeEnum.REFERENCE; 364 break; 365 case "string": 366 paramType = RestSearchParameterTypeEnum.STRING; 367 break; 368 case "token": 369 paramType = RestSearchParameterTypeEnum.TOKEN; 370 break; 371 case "uri": 372 paramType = RestSearchParameterTypeEnum.URI; 373 break; 374 case "special": 375 paramType = RestSearchParameterTypeEnum.SPECIAL; 376 break; 377 } 378 switch (myTerser.getSinglePrimitiveValue(theNextSp, "status").orElse("")) { 379 case "active": 380 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE; 381 break; 382 case "draft": 383 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.DRAFT; 384 break; 385 case "retired": 386 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.RETIRED; 387 break; 388 case "unknown": 389 status = RuntimeSearchParam.RuntimeSearchParamStatusEnum.UNKNOWN; 390 break; 391 } 392 393 Set<String> targetResources = extractR4PlusResources("target", theNextSp); 394 List<String> targetCustomResources = extractR4PlusCustomResourcesFromExtensions( 395 theNextSp, HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE); 396 397 maybeAddCustomResourcesToResources(targetResources, targetCustomResources); 398 399 if (isBlank(name) || isBlank(path) || paramType == null) { 400 if ("_text".equals(name) || "_content".equals(name)) { 401 // ok 402 } else if (paramType != RestSearchParameterTypeEnum.COMPOSITE) { 403 return null; 404 } 405 } 406 407 IIdType id = theNextSp.getIdElement(); 408 String uri = myTerser.getSinglePrimitiveValueOrNull(theNextSp, "url"); 409 ComboSearchParamType unique = null; 410 411 String value = ((IBaseHasExtensions) theNextSp) 412 .getExtension().stream() 413 .filter(e -> HapiExtensions.EXT_SP_UNIQUE.equals(e.getUrl())) 414 .filter(t -> t.getValue() instanceof IPrimitiveType) 415 .map(t -> (IPrimitiveType<?>) t.getValue()) 416 .map(IPrimitiveType::getValueAsString) 417 .findFirst() 418 .orElse(""); 419 if ("true".equalsIgnoreCase(value)) { 420 unique = ComboSearchParamType.UNIQUE; 421 } else if ("false".equalsIgnoreCase(value)) { 422 unique = ComboSearchParamType.NON_UNIQUE; 423 } 424 425 List<RuntimeSearchParam.Component> components = new ArrayList<>(); 426 for (IBase next : myTerser.getValues(theNextSp, "component")) { 427 String expression = myTerser.getSinglePrimitiveValueOrNull(next, "expression"); 428 String definition = myTerser.getSinglePrimitiveValueOrNull(next, "definition"); 429 if (Strings.CS.startsWith(definition, "/SearchParameter/")) { 430 definition = definition.substring(1); 431 } 432 433 String comboUpliftChain = null; 434 List<? extends IBaseExtension<?, ?>> componentExtensions = ((IBaseHasExtensions) next).getExtension(); 435 for (IBaseExtension<?, ?> nextComponentExtension : componentExtensions) { 436 if (HapiExtensions.EXT_SP_COMBO_UPLIFT_CHAIN.equals(nextComponentExtension.getUrl()) 437 && unique == ComboSearchParamType.NON_UNIQUE) { 438 IPrimitiveType<String> upliftChainValue = 439 (IPrimitiveType<String>) nextComponentExtension.getValue(); 440 comboUpliftChain = upliftChainValue.getValueAsString(); 441 } 442 } 443 444 components.add(new RuntimeSearchParam.Component(expression, definition, comboUpliftChain)); 445 } 446 447 return new RuntimeSearchParam( 448 id, 449 uri, 450 name, 451 description, 452 path, 453 paramType, 454 Collections.emptySet(), 455 targetResources, 456 status, 457 unique, 458 components, 459 baseResources); 460 } 461 462 private Set<String> extractR4PlusResources(String thePath, IBaseResource theNextSp) { 463 return myTerser.getValues(theNextSp, thePath, IPrimitiveType.class).stream() 464 .map(IPrimitiveType::getValueAsString) 465 .collect(Collectors.toSet()); 466 } 467 468 /** 469 * Extracts any extensions from the resource and populates an extension field in the 470 */ 471 protected void extractExtensions(IBaseResource theSearchParamResource, RuntimeSearchParam theRuntimeSearchParam) { 472 if (theSearchParamResource instanceof IBaseHasExtensions) { 473 List<? extends IBaseExtension<? extends IBaseExtension, ?>> extensions = 474 (List<? extends IBaseExtension<? extends IBaseExtension, ?>>) 475 ((IBaseHasExtensions) theSearchParamResource).getExtension(); 476 for (IBaseExtension<? extends IBaseExtension, ?> next : extensions) { 477 String nextUrl = next.getUrl(); 478 if (isNotBlank(nextUrl)) { 479 theRuntimeSearchParam.addExtension(nextUrl, next); 480 if (HapiExtensions.EXT_SEARCHPARAM_PHONETIC_ENCODER.equals(nextUrl)) { 481 setEncoder(theRuntimeSearchParam, next.getValue()); 482 } else if (HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN.equals(nextUrl)) { 483 addUpliftRefchain(theRuntimeSearchParam, next); 484 } else if (HapiExtensions.EXT_SEARCHPARAM_ENABLED_FOR_SEARCHING.equals(nextUrl)) { 485 addEnabledForSearching(theRuntimeSearchParam, next.getValue()); 486 } 487 } 488 } 489 } 490 } 491 492 private void addEnabledForSearching(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) { 493 if (theValue instanceof IPrimitiveType) { 494 String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString(); 495 boolean enabledForSearching = Boolean.parseBoolean(stringValue); 496 theRuntimeSearchParam.setEnabledForSearching(enabledForSearching); 497 } 498 } 499 500 @SuppressWarnings("unchecked") 501 private void addUpliftRefchain( 502 RuntimeSearchParam theRuntimeSearchParam, IBaseExtension<? extends IBaseExtension, ?> theExtension) { 503 String code = ExtensionUtil.extractChildPrimitiveExtensionValue( 504 theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_PARAM_CODE); 505 String elementName = ExtensionUtil.extractChildPrimitiveExtensionValue( 506 theExtension, HapiExtensions.EXTENSION_SEARCHPARAM_UPLIFT_REFCHAIN_ELEMENT_NAME); 507 if (isNotBlank(code)) { 508 theRuntimeSearchParam.addUpliftRefchain(code, elementName); 509 } 510 } 511 512 private void setEncoder(RuntimeSearchParam theRuntimeSearchParam, IBaseDatatype theValue) { 513 if (theValue instanceof IPrimitiveType) { 514 String stringValue = ((IPrimitiveType<?>) theValue).getValueAsString(); 515 516 // every string creates a completely new encoder wrapper. 517 // this is fine, because the runtime search parameters are constructed at startup 518 // for every saved value 519 IPhoneticEncoder encoder = PhoneticEncoderUtil.getEncoder(stringValue); 520 if (encoder != null) { 521 theRuntimeSearchParam.setPhoneticEncoder(encoder); 522 } else { 523 ourLog.error("Invalid PhoneticEncoderEnum value '" + stringValue + "'"); 524 } 525 } 526 } 527 528 private List<String> extractDstu2CustomResourcesFromExtensions( 529 ca.uhn.fhir.model.dstu2.resource.SearchParameter theSearchParameter, String theExtensionUrl) { 530 531 List<ExtensionDt> customSpExtensionDt = theSearchParameter.getUndeclaredExtensionsByUrl(theExtensionUrl); 532 533 return customSpExtensionDt.stream() 534 .map(theExtensionDt -> theExtensionDt.getValueAsPrimitive().getValueAsString()) 535 .filter(StringUtils::isNotBlank) 536 .collect(Collectors.toList()); 537 } 538 539 private List<String> extractDstu3CustomResourcesFromExtensions( 540 org.hl7.fhir.dstu3.model.SearchParameter theSearchParameter, String theExtensionUrl) { 541 542 List<Extension> customSpExtensions = theSearchParameter.getExtensionsByUrl(theExtensionUrl); 543 544 return customSpExtensions.stream() 545 .map(theExtension -> theExtension.getValueAsPrimitive().getValueAsString()) 546 .filter(StringUtils::isNotBlank) 547 .collect(Collectors.toList()); 548 } 549 550 private List<String> extractR4PlusCustomResourcesFromExtensions( 551 IBaseResource theSearchParameter, String theExtensionUrl) { 552 553 List<String> retVal = new ArrayList<>(); 554 555 if (theSearchParameter instanceof IBaseHasExtensions) { 556 ((IBaseHasExtensions) theSearchParameter) 557 .getExtension().stream() 558 .filter(t -> theExtensionUrl.equals(t.getUrl())) 559 .filter(t -> t.getValue() instanceof IPrimitiveType) 560 .map(t -> ((IPrimitiveType<?>) t.getValue())) 561 .map(IPrimitiveType::getValueAsString) 562 .filter(StringUtils::isNotBlank) 563 .forEach(retVal::add); 564 } 565 566 return retVal; 567 } 568 569 private <T extends Collection<String>> void maybeAddCustomResourcesToResources( 570 T theResources, List<String> theCustomResources) { 571 // SearchParameter base and target components require strict binding to ResourceType for dstu[2|3], R4, R4B 572 // and to Version Independent Resource Types for R5. 573 // 574 // To handle custom resources, we set a placeholder of type 'Resource' in the base or target component and 575 // define 576 // the custom resource by adding a corresponding extension with url 577 // HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE 578 // or HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE with the name of the custom resource. 579 // 580 // To provide a base/target list that contains both the resources and customResources, we need to remove the 581 // placeholders 582 // from the theResources and add theCustomResources. 583 584 if (!theCustomResources.isEmpty()) { 585 theResources.removeAll(Collections.singleton("Resource")); 586 theResources.addAll(theCustomResources); 587 } 588 } 589}