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