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