001/*- 002 * #%L 003 * HAPI FHIR JPA Server 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.jpa.packages; 021 022import ca.uhn.fhir.context.BaseRuntimeChildDefinition; 023import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; 024import ca.uhn.fhir.context.BaseRuntimeElementDefinition; 025import ca.uhn.fhir.context.FhirContext; 026import ca.uhn.fhir.context.FhirVersionEnum; 027import ca.uhn.fhir.context.support.IValidationSupport; 028import ca.uhn.fhir.context.support.ValidationSupportContext; 029import ca.uhn.fhir.i18n.Msg; 030import ca.uhn.fhir.interceptor.model.RequestPartitionId; 031import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 032import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 033import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome; 034import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; 035import ca.uhn.fhir.jpa.dao.tx.IHapiTransactionService; 036import ca.uhn.fhir.jpa.model.config.PartitionSettings; 037import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; 038import ca.uhn.fhir.jpa.packages.loader.PackageResourceParsingSvc; 039import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; 040import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistryController; 041import ca.uhn.fhir.jpa.searchparam.util.SearchParameterHelper; 042import ca.uhn.fhir.rest.api.server.IBundleProvider; 043import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 044import ca.uhn.fhir.rest.param.StringParam; 045import ca.uhn.fhir.rest.param.TokenParam; 046import ca.uhn.fhir.rest.param.UriParam; 047import ca.uhn.fhir.rest.server.exceptions.ResourceVersionConflictException; 048import ca.uhn.fhir.util.FhirTerser; 049import ca.uhn.fhir.util.SearchParameterUtil; 050import com.google.common.annotations.VisibleForTesting; 051import org.apache.commons.lang3.Validate; 052import org.hl7.fhir.instance.model.api.IBase; 053import org.hl7.fhir.instance.model.api.IBaseResource; 054import org.hl7.fhir.instance.model.api.IIdType; 055import org.hl7.fhir.instance.model.api.IPrimitiveType; 056import org.hl7.fhir.r4.model.Identifier; 057import org.hl7.fhir.r4.model.MetadataResource; 058import org.hl7.fhir.utilities.json.model.JsonObject; 059import org.hl7.fhir.utilities.npm.IPackageCacheManager; 060import org.hl7.fhir.utilities.npm.NpmPackage; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063import org.springframework.beans.factory.annotation.Autowired; 064 065import java.io.IOException; 066import java.util.Collection; 067import java.util.List; 068import java.util.Optional; 069import javax.annotation.Nonnull; 070import javax.annotation.PostConstruct; 071 072import static ca.uhn.fhir.jpa.packages.util.PackageUtils.DEFAULT_INSTALL_TYPES; 073import static org.apache.commons.lang3.StringUtils.defaultString; 074import static org.apache.commons.lang3.StringUtils.isBlank; 075 076/** 077 * @since 5.1.0 078 */ 079public class PackageInstallerSvcImpl implements IPackageInstallerSvc { 080 081 private static final Logger ourLog = LoggerFactory.getLogger(PackageInstallerSvcImpl.class); 082 083 boolean enabled = true; 084 085 @Autowired 086 private FhirContext myFhirContext; 087 088 @Autowired 089 private DaoRegistry myDaoRegistry; 090 091 @Autowired 092 private IValidationSupport validationSupport; 093 094 @Autowired 095 private IHapiPackageCacheManager myPackageCacheManager; 096 097 @Autowired 098 private IHapiTransactionService myTxService; 099 100 @Autowired 101 private INpmPackageVersionDao myPackageVersionDao; 102 103 @Autowired 104 private ISearchParamRegistryController mySearchParamRegistryController; 105 106 @Autowired 107 private PartitionSettings myPartitionSettings; 108 109 @Autowired 110 private SearchParameterHelper mySearchParameterHelper; 111 112 @Autowired 113 private PackageResourceParsingSvc myPackageResourceParsingSvc; 114 115 /** 116 * Constructor 117 */ 118 public PackageInstallerSvcImpl() { 119 super(); 120 } 121 122 @PostConstruct 123 public void initialize() { 124 switch (myFhirContext.getVersion().getVersion()) { 125 case R5: 126 case R4B: 127 case R4: 128 case DSTU3: 129 break; 130 131 case DSTU2: 132 case DSTU2_HL7ORG: 133 case DSTU2_1: 134 default: { 135 ourLog.info( 136 "IG installation not supported for version: {}", 137 myFhirContext.getVersion().getVersion()); 138 enabled = false; 139 } 140 } 141 } 142 143 @Override 144 public PackageDeleteOutcomeJson uninstall(PackageInstallationSpec theInstallationSpec) { 145 PackageDeleteOutcomeJson outcome = 146 myPackageCacheManager.uninstallPackage(theInstallationSpec.getName(), theInstallationSpec.getVersion()); 147 validationSupport.invalidateCaches(); 148 return outcome; 149 } 150 151 /** 152 * Loads and installs an IG from a file on disk or the Simplifier repo using 153 * the {@link IPackageCacheManager}. 154 * <p> 155 * Installs the IG by persisting instances of the following types of resources: 156 * <p> 157 * - NamingSystem, CodeSystem, ValueSet, StructureDefinition (with snapshots), 158 * ConceptMap, SearchParameter, Subscription 159 * <p> 160 * Creates the resources if non-existent, updates them otherwise. 161 * 162 * @param theInstallationSpec The details about what should be installed 163 */ 164 @SuppressWarnings("ConstantConditions") 165 @Override 166 public PackageInstallOutcomeJson install(PackageInstallationSpec theInstallationSpec) 167 throws ImplementationGuideInstallationException { 168 PackageInstallOutcomeJson retVal = new PackageInstallOutcomeJson(); 169 if (enabled) { 170 try { 171 172 boolean exists = myTxService 173 .withSystemRequest() 174 .withRequestPartitionId(RequestPartitionId.defaultPartition()) 175 .execute(() -> { 176 Optional<NpmPackageVersionEntity> existing = myPackageVersionDao.findByPackageIdAndVersion( 177 theInstallationSpec.getName(), theInstallationSpec.getVersion()); 178 return existing.isPresent(); 179 }); 180 if (exists) { 181 ourLog.info( 182 "Package {}#{} is already installed", 183 theInstallationSpec.getName(), 184 theInstallationSpec.getVersion()); 185 } 186 187 NpmPackage npmPackage = myPackageCacheManager.installPackage(theInstallationSpec); 188 if (npmPackage == null) { 189 throw new IOException(Msg.code(1284) + "Package not found"); 190 } 191 192 retVal.getMessage().addAll(JpaPackageCache.getProcessingMessages(npmPackage)); 193 194 if (theInstallationSpec.isFetchDependencies()) { 195 fetchAndInstallDependencies(npmPackage, theInstallationSpec, retVal); 196 } 197 198 if (theInstallationSpec.getInstallMode() == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { 199 install(npmPackage, theInstallationSpec, retVal); 200 201 // If any SearchParameters were installed, let's load them right away 202 mySearchParamRegistryController.refreshCacheIfNecessary(); 203 } 204 205 validationSupport.invalidateCaches(); 206 207 } catch (IOException e) { 208 throw new ImplementationGuideInstallationException( 209 Msg.code(1285) + "Could not load NPM package " + theInstallationSpec.getName() + "#" 210 + theInstallationSpec.getVersion(), 211 e); 212 } 213 } 214 215 return retVal; 216 } 217 218 /** 219 * Installs a package and its dependencies. 220 * <p> 221 * Fails fast if one of its dependencies could not be installed. 222 * 223 * @throws ImplementationGuideInstallationException if installation fails 224 */ 225 private void install( 226 NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome) 227 throws ImplementationGuideInstallationException { 228 String name = npmPackage.getNpm().get("name").asJsonString().getValue(); 229 String version = npmPackage.getNpm().get("version").asJsonString().getValue(); 230 231 String fhirVersion = npmPackage.fhirVersion(); 232 String currentFhirVersion = myFhirContext.getVersion().getVersion().getFhirVersionString(); 233 assertFhirVersionsAreCompatible(fhirVersion, currentFhirVersion); 234 235 List<String> installTypes; 236 if (!theInstallationSpec.getInstallResourceTypes().isEmpty()) { 237 installTypes = theInstallationSpec.getInstallResourceTypes(); 238 } else { 239 installTypes = DEFAULT_INSTALL_TYPES; 240 } 241 242 ourLog.info("Installing package: {}#{}", name, version); 243 int[] count = new int[installTypes.size()]; 244 245 for (int i = 0; i < installTypes.size(); i++) { 246 String type = installTypes.get(i); 247 248 Collection<IBaseResource> resources = myPackageResourceParsingSvc.parseResourcesOfType(type, npmPackage); 249 count[i] = resources.size(); 250 251 for (IBaseResource next : resources) { 252 try { 253 next = isStructureDefinitionWithoutSnapshot(next) ? generateSnapshot(next) : next; 254 create(next, theInstallationSpec, theOutcome); 255 } catch (Exception e) { 256 ourLog.warn( 257 "Failed to upload resource of type {} with ID {} - Error: {}", 258 myFhirContext.getResourceType(next), 259 next.getIdElement().getValue(), 260 e.toString()); 261 throw new ImplementationGuideInstallationException( 262 Msg.code(1286) + String.format("Error installing IG %s#%s: %s", name, version, e), e); 263 } 264 } 265 } 266 ourLog.info(String.format("Finished installation of package %s#%s:", name, version)); 267 268 for (int i = 0; i < count.length; i++) { 269 ourLog.info(String.format("-- Created or updated %s resources of type %s", count[i], installTypes.get(i))); 270 } 271 } 272 273 private void fetchAndInstallDependencies( 274 NpmPackage npmPackage, PackageInstallationSpec theInstallationSpec, PackageInstallOutcomeJson theOutcome) 275 throws ImplementationGuideInstallationException { 276 if (npmPackage.getNpm().has("dependencies")) { 277 JsonObject dependenciesElement = 278 npmPackage.getNpm().get("dependencies").asJsonObject(); 279 for (String id : dependenciesElement.getNames()) { 280 String ver = dependenciesElement.getJsonString(id).asString(); 281 try { 282 theOutcome 283 .getMessage() 284 .add("Package " + npmPackage.id() + "#" + npmPackage.version() + " depends on package " + id 285 + "#" + ver); 286 287 boolean skip = false; 288 for (String next : theInstallationSpec.getDependencyExcludes()) { 289 if (id.matches(next)) { 290 theOutcome 291 .getMessage() 292 .add("Not installing dependency " + id + " because it matches exclude criteria: " 293 + next); 294 skip = true; 295 break; 296 } 297 } 298 if (skip) { 299 continue; 300 } 301 302 // resolve in local cache or on packages.fhir.org 303 NpmPackage dependency = myPackageCacheManager.loadPackage(id, ver); 304 // recursive call to install dependencies of a package before 305 // installing the package 306 fetchAndInstallDependencies(dependency, theInstallationSpec, theOutcome); 307 308 if (theInstallationSpec.getInstallMode() 309 == PackageInstallationSpec.InstallModeEnum.STORE_AND_INSTALL) { 310 install(dependency, theInstallationSpec, theOutcome); 311 } 312 313 } catch (IOException e) { 314 throw new ImplementationGuideInstallationException( 315 Msg.code(1287) + String.format("Cannot resolve dependency %s#%s", id, ver), e); 316 } 317 } 318 } 319 } 320 321 /** 322 * Asserts if package FHIR version is compatible with current FHIR version 323 * by using semantic versioning rules. 324 */ 325 protected void assertFhirVersionsAreCompatible(String fhirVersion, String currentFhirVersion) 326 throws ImplementationGuideInstallationException { 327 328 FhirVersionEnum fhirVersionEnum = FhirVersionEnum.forVersionString(fhirVersion); 329 FhirVersionEnum currentFhirVersionEnum = FhirVersionEnum.forVersionString(currentFhirVersion); 330 Validate.notNull(fhirVersionEnum, "Invalid FHIR version string: %s", fhirVersion); 331 Validate.notNull(currentFhirVersionEnum, "Invalid FHIR version string: %s", currentFhirVersion); 332 boolean compatible = fhirVersionEnum.equals(currentFhirVersionEnum); 333 if (!compatible && fhirVersion.startsWith("R4") && currentFhirVersion.startsWith("R4")) { 334 compatible = true; 335 } 336 if (!compatible) { 337 throw new ImplementationGuideInstallationException(Msg.code(1288) 338 + String.format( 339 "Cannot install implementation guide: FHIR versions mismatch (expected <=%s, package uses %s)", 340 currentFhirVersion, fhirVersion)); 341 } 342 } 343 344 /** 345 * ============================= Utility methods =============================== 346 */ 347 @VisibleForTesting 348 void create( 349 IBaseResource theResource, 350 PackageInstallationSpec theInstallationSpec, 351 PackageInstallOutcomeJson theOutcome) { 352 IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass()); 353 SearchParameterMap map = createSearchParameterMapFor(theResource); 354 IBundleProvider searchResult = searchResource(dao, map); 355 if (validForUpload(theResource)) { 356 if (searchResult.isEmpty()) { 357 358 ourLog.info("Creating new resource matching {}", map.toNormalizedQueryString(myFhirContext)); 359 theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource)); 360 361 IIdType id = theResource.getIdElement(); 362 363 if (id.isEmpty()) { 364 createResource(dao, theResource); 365 ourLog.info("Created resource with new id"); 366 } else { 367 if (id.isIdPartValidLong()) { 368 String newIdPart = "npm-" + id.getIdPart(); 369 id.setParts(id.getBaseUrl(), id.getResourceType(), newIdPart, id.getVersionIdPart()); 370 } 371 372 try { 373 updateResource(dao, theResource); 374 375 ourLog.info("Created resource with existing id"); 376 } catch (ResourceVersionConflictException exception) { 377 final Optional<IBaseResource> optResource = readResourceById(dao, id); 378 379 final String existingResourceUrlOrNull = optResource 380 .filter(MetadataResource.class::isInstance) 381 .map(MetadataResource.class::cast) 382 .map(MetadataResource::getUrl) 383 .orElse(null); 384 final String newResourceUrlOrNull = (theResource instanceof MetadataResource) 385 ? ((MetadataResource) theResource).getUrl() 386 : null; 387 388 ourLog.error( 389 "Version conflict error: This is possibly due to a collision between ValueSets from different IGs that are coincidentally using the same resource ID: [{}] and new resource URL: [{}], with the exisitng resource having URL: [{}]. Ignoring this update and continuing: The first IG wins. ", 390 id.getIdPart(), 391 newResourceUrlOrNull, 392 existingResourceUrlOrNull, 393 exception); 394 } 395 } 396 } else { 397 if (theInstallationSpec.isReloadExisting()) { 398 ourLog.info("Updating existing resource matching {}", map.toNormalizedQueryString(myFhirContext)); 399 theResource.setId(searchResult 400 .getResources(0, 1) 401 .get(0) 402 .getIdElement() 403 .toUnqualifiedVersionless()); 404 DaoMethodOutcome outcome = updateResource(dao, theResource); 405 if (!outcome.isNop()) { 406 theOutcome.incrementResourcesInstalled(myFhirContext.getResourceType(theResource)); 407 } 408 } else { 409 ourLog.info( 410 "Skipping update of existing resource matching {}", 411 map.toNormalizedQueryString(myFhirContext)); 412 } 413 } 414 } else { 415 ourLog.warn( 416 "Failed to upload resource of type {} with ID {} - Error: Resource failed validation", 417 theResource.fhirType(), 418 theResource.getIdElement().getValue()); 419 } 420 } 421 422 private Optional<IBaseResource> readResourceById(IFhirResourceDao dao, IIdType id) { 423 try { 424 return Optional.ofNullable(dao.read(id.toUnqualifiedVersionless(), newSystemRequestDetails())); 425 426 } catch (Exception exception) { 427 // ignore because we're running this query to help build the log 428 ourLog.warn("Exception when trying to read resource with ID: {}, message: {}", id, exception.getMessage()); 429 } 430 431 return Optional.empty(); 432 } 433 434 private IBundleProvider searchResource(IFhirResourceDao theDao, SearchParameterMap theMap) { 435 return theDao.search(theMap, newSystemRequestDetails()); 436 } 437 438 @Nonnull 439 private SystemRequestDetails newSystemRequestDetails() { 440 return new SystemRequestDetails().setRequestPartitionId(RequestPartitionId.defaultPartition()); 441 } 442 443 private void createResource(IFhirResourceDao theDao, IBaseResource theResource) { 444 if (myPartitionSettings.isPartitioningEnabled()) { 445 SystemRequestDetails requestDetails = newSystemRequestDetails(); 446 theDao.create(theResource, requestDetails); 447 } else { 448 theDao.create(theResource); 449 } 450 } 451 452 DaoMethodOutcome updateResource(IFhirResourceDao theDao, IBaseResource theResource) { 453 if (myPartitionSettings.isPartitioningEnabled()) { 454 SystemRequestDetails requestDetails = newSystemRequestDetails(); 455 return theDao.update(theResource, requestDetails); 456 } else { 457 return theDao.update(theResource, new SystemRequestDetails()); 458 } 459 } 460 461 boolean validForUpload(IBaseResource theResource) { 462 String resourceType = myFhirContext.getResourceType(theResource); 463 if ("SearchParameter".equals(resourceType)) { 464 465 String code = SearchParameterUtil.getCode(myFhirContext, theResource); 466 if (defaultString(code).startsWith("_")) { 467 ourLog.warn( 468 "Failed to validate resource of type {} with url {} - Error: Resource code starts with \"_\"", 469 theResource.fhirType(), 470 SearchParameterUtil.getURL(myFhirContext, theResource)); 471 return false; 472 } 473 474 String expression = SearchParameterUtil.getExpression(myFhirContext, theResource); 475 if (isBlank(expression)) { 476 ourLog.warn( 477 "Failed to validate resource of type {} with url {} - Error: Resource expression is blank", 478 theResource.fhirType(), 479 SearchParameterUtil.getURL(myFhirContext, theResource)); 480 return false; 481 } 482 483 if (SearchParameterUtil.getBaseAsStrings(myFhirContext, theResource).isEmpty()) { 484 ourLog.warn( 485 "Failed to validate resource of type {} with url {} - Error: Resource base is empty", 486 theResource.fhirType(), 487 SearchParameterUtil.getURL(myFhirContext, theResource)); 488 return false; 489 } 490 } 491 492 if (!isValidResourceStatusForPackageUpload(theResource)) { 493 ourLog.warn( 494 "Failed to validate resource of type {} with ID {} - Error: Resource status not accepted value.", 495 theResource.fhirType(), 496 theResource.getIdElement().getValue()); 497 return false; 498 } 499 500 return true; 501 } 502 503 /** 504 * For resources like {@link org.hl7.fhir.r4.model.Subscription}, {@link org.hl7.fhir.r4.model.DocumentReference}, 505 * and {@link org.hl7.fhir.r4.model.Communication}, the status field doesn't necessarily need to be set to 'active' 506 * for that resource to be eligible for upload via packages. For example, all {@link org.hl7.fhir.r4.model.Subscription} 507 * have a status of {@link org.hl7.fhir.r4.model.Subscription.SubscriptionStatus#REQUESTED} when they are originally 508 * inserted into the database, so we accept that value for {@link org.hl7.fhir.r4.model.Subscription} isntead. 509 * Furthermore, {@link org.hl7.fhir.r4.model.DocumentReference} and {@link org.hl7.fhir.r4.model.Communication} can 510 * exist with a wide variety of values for status that include ones such as 511 * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#ENTEREDINERROR}, 512 * {@link org.hl7.fhir.r4.model.Communication.CommunicationStatus#UNKNOWN}, 513 * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#ENTEREDINERROR}, 514 * {@link org.hl7.fhir.r4.model.DocumentReference.ReferredDocumentStatus#PRELIMINARY}, and others, which while not considered 515 * 'final' values, should still be uploaded for reference. 516 * 517 * @return {@link Boolean#TRUE} if the status value of this resource is acceptable for package upload. 518 */ 519 private boolean isValidResourceStatusForPackageUpload(IBaseResource theResource) { 520 List<IPrimitiveType> statusTypes = 521 myFhirContext.newFhirPath().evaluate(theResource, "status", IPrimitiveType.class); 522 // Resource does not have a status field 523 if (statusTypes.isEmpty()) return true; 524 // Resource has a null status field 525 if (statusTypes.get(0).getValue() == null) return false; 526 // Resource has a status, and we need to check based on type 527 switch (theResource.fhirType()) { 528 case "Subscription": 529 return (statusTypes.get(0).getValueAsString().equals("requested")); 530 case "DocumentReference": 531 case "Communication": 532 return (!statusTypes.get(0).getValueAsString().equals("?")); 533 default: 534 return (statusTypes.get(0).getValueAsString().equals("active")); 535 } 536 } 537 538 private boolean isStructureDefinitionWithoutSnapshot(IBaseResource r) { 539 boolean retVal = false; 540 FhirTerser terser = myFhirContext.newTerser(); 541 if (r.getClass().getSimpleName().equals("StructureDefinition")) { 542 Optional<String> kind = terser.getSinglePrimitiveValue(r, "kind"); 543 if (kind.isPresent() && !(kind.get().equals("logical"))) { 544 retVal = terser.getSingleValueOrNull(r, "snapshot") == null; 545 } 546 } 547 return retVal; 548 } 549 550 private IBaseResource generateSnapshot(IBaseResource sd) { 551 try { 552 return validationSupport.generateSnapshot( 553 new ValidationSupportContext(validationSupport), sd, null, null, null); 554 } catch (Exception e) { 555 throw new ImplementationGuideInstallationException( 556 Msg.code(1290) 557 + String.format( 558 "Failure when generating snapshot of StructureDefinition: %s", sd.getIdElement()), 559 e); 560 } 561 } 562 563 private SearchParameterMap createSearchParameterMapFor(IBaseResource resource) { 564 if (resource.getClass().getSimpleName().equals("NamingSystem")) { 565 String uniqueId = extractUniqeIdFromNamingSystem(resource); 566 return SearchParameterMap.newSynchronous().add("value", new StringParam(uniqueId).setExact(true)); 567 } else if (resource.getClass().getSimpleName().equals("Subscription")) { 568 String id = extractIdFromSubscription(resource); 569 return SearchParameterMap.newSynchronous().add("_id", new TokenParam(id)); 570 } else if (resource.getClass().getSimpleName().equals("SearchParameter")) { 571 return buildSearchParameterMapForSearchParameter(resource); 572 } else if (resourceHasUrlElement(resource)) { 573 String url = extractUniqueUrlFromMetadataResource(resource); 574 return SearchParameterMap.newSynchronous().add("url", new UriParam(url)); 575 } else { 576 TokenParam identifierToken = extractIdentifierFromOtherResourceTypes(resource); 577 return SearchParameterMap.newSynchronous().add("identifier", identifierToken); 578 } 579 } 580 581 /** 582 * Strategy is to build a SearchParameterMap same way the SearchParamValidatingInterceptor does, to make sure that 583 * the loader search detects existing resources and routes process to 'update' path, to avoid treating it as a new 584 * upload which validator later rejects as duplicated. 585 * To achieve this, we try canonicalizing the SearchParameter first (as the validator does) and if that is not possible 586 * we cascade to building the map from 'url' or 'identifier'. 587 */ 588 private SearchParameterMap buildSearchParameterMapForSearchParameter(IBaseResource theResource) { 589 Optional<SearchParameterMap> spmFromCanonicalized = 590 mySearchParameterHelper.buildSearchParameterMapFromCanonical(theResource); 591 if (spmFromCanonicalized.isPresent()) { 592 return spmFromCanonicalized.get(); 593 } 594 595 if (resourceHasUrlElement(theResource)) { 596 String url = extractUniqueUrlFromMetadataResource(theResource); 597 return SearchParameterMap.newSynchronous().add("url", new UriParam(url)); 598 } else { 599 TokenParam identifierToken = extractIdentifierFromOtherResourceTypes(theResource); 600 return SearchParameterMap.newSynchronous().add("identifier", identifierToken); 601 } 602 } 603 604 private String extractUniqeIdFromNamingSystem(IBaseResource resource) { 605 FhirTerser terser = myFhirContext.newTerser(); 606 IBase uniqueIdComponent = (IBase) terser.getSingleValueOrNull(resource, "uniqueId"); 607 if (uniqueIdComponent == null) { 608 throw new ImplementationGuideInstallationException( 609 Msg.code(1291) + "NamingSystem does not have uniqueId component."); 610 } 611 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(uniqueIdComponent, "value"); 612 return (String) asPrimitiveType.getValue(); 613 } 614 615 private String extractIdFromSubscription(IBaseResource resource) { 616 FhirTerser terser = myFhirContext.newTerser(); 617 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(resource, "id"); 618 return (String) asPrimitiveType.getValue(); 619 } 620 621 private String extractUniqueUrlFromMetadataResource(IBaseResource resource) { 622 FhirTerser terser = myFhirContext.newTerser(); 623 IPrimitiveType<?> asPrimitiveType = (IPrimitiveType<?>) terser.getSingleValueOrNull(resource, "url"); 624 return (String) asPrimitiveType.getValue(); 625 } 626 627 private TokenParam extractIdentifierFromOtherResourceTypes(IBaseResource resource) { 628 FhirTerser terser = myFhirContext.newTerser(); 629 Identifier identifier = (Identifier) terser.getSingleValueOrNull(resource, "identifier"); 630 if (identifier != null) { 631 return new TokenParam(identifier.getSystem(), identifier.getValue()); 632 } else { 633 throw new UnsupportedOperationException(Msg.code(1292) 634 + "Resources in a package must have a url or identifier to be loaded by the package installer."); 635 } 636 } 637 638 private boolean resourceHasUrlElement(IBaseResource resource) { 639 BaseRuntimeElementDefinition<?> def = myFhirContext.getElementDefinition(resource.getClass()); 640 if (!(def instanceof BaseRuntimeElementCompositeDefinition)) { 641 throw new IllegalArgumentException(Msg.code(1293) + "Resource is not a composite type: " 642 + resource.getClass().getName()); 643 } 644 BaseRuntimeElementCompositeDefinition<?> currentDef = (BaseRuntimeElementCompositeDefinition<?>) def; 645 BaseRuntimeChildDefinition nextDef = currentDef.getChildByName("url"); 646 return nextDef != null; 647 } 648 649 @VisibleForTesting 650 void setFhirContextForUnitTest(FhirContext theCtx) { 651 myFhirContext = theCtx; 652 } 653}