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