
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.FhirContext; 024import ca.uhn.fhir.context.FhirVersionEnum; 025import ca.uhn.fhir.i18n.Msg; 026import ca.uhn.fhir.interceptor.model.RequestPartitionId; 027import ca.uhn.fhir.jpa.api.dao.DaoRegistry; 028import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao; 029import ca.uhn.fhir.jpa.api.model.ExpungeOptions; 030import ca.uhn.fhir.jpa.binary.api.IBinaryStorageSvc; 031import ca.uhn.fhir.jpa.binary.svc.NullBinaryStorageSvcImpl; 032import ca.uhn.fhir.jpa.dao.data.INpmPackageDao; 033import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionDao; 034import ca.uhn.fhir.jpa.dao.data.INpmPackageVersionResourceDao; 035import ca.uhn.fhir.jpa.model.config.PartitionSettings; 036import ca.uhn.fhir.jpa.model.dao.JpaPid; 037import ca.uhn.fhir.jpa.model.entity.NpmPackageEntity; 038import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionEntity; 039import ca.uhn.fhir.jpa.model.entity.NpmPackageVersionResourceEntity; 040import ca.uhn.fhir.jpa.model.entity.ResourceTable; 041import ca.uhn.fhir.jpa.model.util.JpaConstants; 042import ca.uhn.fhir.jpa.packages.loader.NpmPackageData; 043import ca.uhn.fhir.jpa.packages.loader.PackageLoaderSvc; 044import ca.uhn.fhir.rest.api.Constants; 045import ca.uhn.fhir.rest.api.EncodingEnum; 046import ca.uhn.fhir.rest.api.server.SystemRequestDetails; 047import ca.uhn.fhir.rest.server.exceptions.InternalErrorException; 048import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; 049import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; 050import ca.uhn.fhir.util.BinaryUtil; 051import ca.uhn.fhir.util.ResourceUtil; 052import ca.uhn.fhir.util.StringUtil; 053import jakarta.annotation.Nonnull; 054import jakarta.annotation.Nullable; 055import jakarta.persistence.EntityManager; 056import jakarta.persistence.PersistenceContext; 057import jakarta.persistence.TypedQuery; 058import jakarta.persistence.criteria.CriteriaBuilder; 059import jakarta.persistence.criteria.CriteriaQuery; 060import jakarta.persistence.criteria.Join; 061import jakarta.persistence.criteria.JoinType; 062import jakarta.persistence.criteria.Predicate; 063import jakarta.persistence.criteria.Root; 064import org.apache.commons.collections4.comparators.ReverseComparator; 065import org.apache.commons.lang3.Validate; 066import org.hl7.fhir.exceptions.FHIRException; 067import org.hl7.fhir.instance.model.api.IBase; 068import org.hl7.fhir.instance.model.api.IBaseBackboneElement; 069import org.hl7.fhir.instance.model.api.IBaseBinary; 070import org.hl7.fhir.instance.model.api.IBaseResource; 071import org.hl7.fhir.instance.model.api.IIdType; 072import org.hl7.fhir.instance.model.api.IPrimitiveType; 073import org.hl7.fhir.utilities.npm.BasePackageCacheManager; 074import org.hl7.fhir.utilities.npm.NpmPackage; 075import org.hl7.fhir.utilities.npm.PackageServer; 076import org.slf4j.Logger; 077import org.slf4j.LoggerFactory; 078import org.springframework.beans.factory.annotation.Autowired; 079import org.springframework.data.domain.PageRequest; 080import org.springframework.data.domain.Slice; 081import org.springframework.transaction.PlatformTransactionManager; 082import org.springframework.transaction.annotation.Propagation; 083import org.springframework.transaction.annotation.Transactional; 084import org.springframework.transaction.support.TransactionTemplate; 085 086import java.io.ByteArrayInputStream; 087import java.io.IOException; 088import java.io.InputStream; 089import java.nio.charset.StandardCharsets; 090import java.util.ArrayList; 091import java.util.Collection; 092import java.util.Collections; 093import java.util.Date; 094import java.util.HashMap; 095import java.util.List; 096import java.util.Map; 097import java.util.Optional; 098import java.util.regex.Pattern; 099import java.util.stream.Collectors; 100 101import static ca.uhn.fhir.jpa.util.QueryParameterUtils.toPredicateArray; 102import static ca.uhn.fhir.util.StringUtil.toUtf8String; 103import static org.apache.commons.lang3.StringUtils.defaultString; 104import static org.apache.commons.lang3.StringUtils.isNotBlank; 105 106public class JpaPackageCache extends BasePackageCacheManager implements IHapiPackageCacheManager { 107 108 private static final Logger ourLog = LoggerFactory.getLogger(JpaPackageCache.class); 109 private static final Pattern PATTERN_FHIR_VERSION = Pattern.compile("^[0-9]+\\.[0-9]+$"); 110 111 private final Map<FhirVersionEnum, FhirContext> myVersionToContext = Collections.synchronizedMap(new HashMap<>()); 112 113 @PersistenceContext 114 protected EntityManager myEntityManager; 115 116 @Autowired 117 private INpmPackageDao myPackageDao; 118 119 @Autowired 120 private INpmPackageVersionDao myPackageVersionDao; 121 122 @Autowired 123 private INpmPackageVersionResourceDao myPackageVersionResourceDao; 124 125 @Autowired 126 private DaoRegistry myDaoRegistry; 127 128 @Autowired 129 private FhirContext myCtx; 130 131 @Autowired 132 private PlatformTransactionManager myTxManager; 133 134 @Autowired 135 private PartitionSettings myPartitionSettings; 136 137 @Autowired 138 private PackageLoaderSvc myPackageLoaderSvc; 139 140 @Autowired(required = false) // It is possible that some implementers will not create such a bean. 141 private IBinaryStorageSvc myBinaryStorageSvc; 142 143 @Override 144 public void addPackageServer(@Nonnull PackageServer thePackageServer) { 145 assert myPackageLoaderSvc != null; 146 myPackageLoaderSvc.addPackageServer(thePackageServer); 147 } 148 149 @Override 150 public String getPackageId(String theS) throws IOException { 151 return myPackageLoaderSvc.getPackageId(theS); 152 } 153 154 @Override 155 public void setSilent(boolean silent) { 156 myPackageLoaderSvc.setSilent(silent); 157 } 158 159 @Override 160 public String getPackageUrl(String theS) throws IOException { 161 return myPackageLoaderSvc.getPackageUrl(theS); 162 } 163 164 @Override 165 public List<PackageServer> getPackageServers() { 166 return myPackageLoaderSvc.getPackageServers(); 167 } 168 169 @Override 170 protected BasePackageCacheManager.InputStreamWithSrc loadFromPackageServer(String id, String version) { 171 throw new UnsupportedOperationException(Msg.code(2220) + "Use PackageLoaderSvc for loading packages."); 172 } 173 174 @Override 175 @Transactional 176 public NpmPackage loadPackageFromCacheOnly(String theId, @Nullable String theVersion) { 177 return loadPackageFromCacheOnlyInner(theId, theVersion); 178 } 179 180 @Nullable 181 private NpmPackage loadPackageFromCacheOnlyInner(String theId, @Nullable String theVersion) { 182 Optional<NpmPackageVersionEntity> packageVersion = loadPackageVersionEntity(theId, theVersion); 183 if (theVersion != null && packageVersion.isEmpty() && theVersion.endsWith(".x")) { 184 String lookupVersion = theVersion; 185 do { 186 lookupVersion = lookupVersion.substring(0, lookupVersion.length() - 2); 187 } while (lookupVersion.endsWith(".x")); 188 189 List<String> candidateVersionIds = 190 myPackageVersionDao.findVersionIdsByPackageIdAndLikeVersion(theId, lookupVersion + ".%"); 191 if (!candidateVersionIds.isEmpty()) { 192 candidateVersionIds.sort(PackageVersionComparator.INSTANCE); 193 packageVersion = 194 loadPackageVersionEntity(theId, candidateVersionIds.get(candidateVersionIds.size() - 1)); 195 } 196 } 197 198 return packageVersion.map(this::loadPackage).orElse(null); 199 } 200 201 private Optional<NpmPackageVersionEntity> loadPackageVersionEntity(String theId, @Nullable String theVersion) { 202 Validate.notBlank(theId, "theId must be populated"); 203 204 Optional<NpmPackageVersionEntity> packageVersion = Optional.empty(); 205 if (isNotBlank(theVersion) && !"latest".equals(theVersion)) { 206 packageVersion = myPackageVersionDao.findByPackageIdAndVersion(theId, theVersion); 207 } else { 208 Optional<NpmPackageEntity> pkg = myPackageDao.findByPackageId(theId); 209 if (pkg.isPresent()) { 210 packageVersion = myPackageVersionDao.findByPackageIdAndVersion( 211 theId, pkg.get().getCurrentVersionId()); 212 } 213 } 214 return packageVersion; 215 } 216 217 private NpmPackage loadPackage(NpmPackageVersionEntity thePackageVersion) { 218 PackageContents content = loadPackageContents(thePackageVersion); 219 ByteArrayInputStream inputStream = new ByteArrayInputStream(content.getBytes()); 220 try { 221 return NpmPackage.fromPackage(inputStream); 222 } catch (IOException e) { 223 throw new InternalErrorException(Msg.code(1294) + e); 224 } 225 } 226 227 private IHapiPackageCacheManager.PackageContents loadPackageContents(NpmPackageVersionEntity thePackageVersion) { 228 IFhirResourceDao<? extends IBaseBinary> binaryDao = getBinaryDao(); 229 IBaseBinary binary = 230 binaryDao.readByPid(thePackageVersion.getPackageBinary().getId()); 231 try { 232 byte[] content = fetchBlobFromBinary(binary); 233 return new PackageContents() 234 .setBytes(content) 235 .setPackageId(thePackageVersion.getPackageId()) 236 .setVersion(thePackageVersion.getVersionId()) 237 .setLastModified(thePackageVersion.getUpdatedTime()); 238 } catch (IOException e) { 239 throw new InternalErrorException( 240 Msg.code(1295) + "Failed to load package. There was a problem reading binaries", e); 241 } 242 } 243 244 /** 245 * Helper method which will attempt to use the IBinaryStorageSvc to resolve the binary blob if available. If 246 * the bean is unavailable, fallback to assuming we are using an embedded base64 in the data element. 247 * @param theBinary the Binary who's `data` blob you want to retrieve 248 * @return a byte array containing the blob. 249 * 250 * @throws IOException 251 */ 252 private byte[] fetchBlobFromBinary(IBaseBinary theBinary) throws IOException { 253 if (myBinaryStorageSvc != null && !(myBinaryStorageSvc instanceof NullBinaryStorageSvcImpl)) { 254 return myBinaryStorageSvc.fetchDataByteArrayFromBinary(theBinary); 255 } else { 256 byte[] value = BinaryUtil.getOrCreateData(myCtx, theBinary).getValue(); 257 if (value == null) { 258 throw new InternalErrorException( 259 Msg.code(1296) + "Failed to fetch blob from Binary/" + theBinary.getIdElement()); 260 } 261 return value; 262 } 263 } 264 265 @SuppressWarnings("unchecked") 266 private IFhirResourceDao<IBaseBinary> getBinaryDao() { 267 return myDaoRegistry.getResourceDao("Binary"); 268 } 269 270 private NpmPackage addPackageToCacheInternal(NpmPackageData thePackageData) { 271 NpmPackage npmPackage = thePackageData.getPackage(); 272 String packageId = thePackageData.getPackageId(); 273 String initialPackageVersionId = thePackageData.getPackageVersionId(); 274 byte[] bytes = thePackageData.getBytes(); 275 276 if (!npmPackage.id().equalsIgnoreCase(packageId)) { 277 throw new InvalidRequestException( 278 Msg.code(1297) + "Package ID " + npmPackage.id() + " doesn't match expected: " + packageId); 279 } 280 if (!PackageVersionComparator.isEquivalent(initialPackageVersionId, npmPackage.version())) { 281 throw new InvalidRequestException(Msg.code(1298) + "Package ID " + npmPackage.version() 282 + " doesn't match expected: " + initialPackageVersionId); 283 } 284 285 String packageVersionId = npmPackage.version(); 286 FhirVersionEnum fhirVersion = FhirVersionEnum.forVersionString(npmPackage.fhirVersion()); 287 if (fhirVersion == null) { 288 throw new InvalidRequestException(Msg.code(1299) + "Unknown FHIR version: " + npmPackage.fhirVersion()); 289 } 290 FhirContext packageContext = getFhirContext(fhirVersion); 291 292 IBaseBinary binary = createPackageBinary(bytes); 293 294 return newTxTemplate().execute(tx -> { 295 ResourceTable persistedPackage = createResourceBinary(binary); 296 NpmPackageEntity pkg = myPackageDao.findByPackageId(packageId).orElseGet(() -> createPackage(npmPackage)); 297 NpmPackageVersionEntity packageVersion = myPackageVersionDao 298 .findByPackageIdAndVersion(packageId, packageVersionId) 299 .orElse(null); 300 if (packageVersion != null) { 301 NpmPackage existingPackage = 302 loadPackageFromCacheOnlyInner(packageVersion.getPackageId(), packageVersion.getVersionId()); 303 if (existingPackage == null) { 304 return null; 305 } 306 String msg = "Package version already exists in local storage, no action taken: " + packageId + "#" 307 + packageVersionId; 308 getProcessingMessages(existingPackage).add(msg); 309 ourLog.info(msg); 310 return existingPackage; 311 } 312 313 boolean currentVersion = 314 updateCurrentVersionFlagForAllPackagesBasedOnNewIncomingVersion(packageId, packageVersionId); 315 316 String packageDesc = truncateStorageString(npmPackage.description()); 317 String packageAuthor = truncateStorageString(npmPackage.getNpm().asString("author")); 318 319 if (currentVersion) { 320 getProcessingMessages(npmPackage) 321 .add("Marking package " + packageId + "#" + initialPackageVersionId + " as current version"); 322 pkg.setCurrentVersionId(packageVersionId); 323 pkg.setDescription(packageDesc); 324 myPackageDao.save(pkg); 325 } else { 326 getProcessingMessages(npmPackage) 327 .add("Package " + packageId + "#" + initialPackageVersionId + " is not the newest version"); 328 } 329 330 packageVersion = new NpmPackageVersionEntity(); 331 packageVersion.setPackageId(packageId); 332 packageVersion.setVersionId(packageVersionId); 333 packageVersion.setPackage(pkg); 334 packageVersion.setPackageBinary(persistedPackage); 335 packageVersion.setSavedTime(new Date()); 336 packageVersion.setAuthor(packageAuthor); 337 packageVersion.setDescription(packageDesc); 338 packageVersion.setFhirVersionId(npmPackage.fhirVersion()); 339 packageVersion.setFhirVersion(fhirVersion); 340 packageVersion.setCurrentVersion(currentVersion); 341 packageVersion.setPackageSizeBytes(bytes.length); 342 packageVersion = myPackageVersionDao.save(packageVersion); 343 344 String dirName = "package"; 345 NpmPackage.NpmPackageFolder packageFolder = npmPackage.getFolders().get(dirName); 346 Map<String, List<String>> packageFolderTypes = null; 347 try { 348 packageFolderTypes = packageFolder.getTypes(); 349 } catch (IOException e) { 350 throw new InternalErrorException(Msg.code(2371) + e); 351 } 352 for (Map.Entry<String, List<String>> nextTypeToFiles : packageFolderTypes.entrySet()) { 353 String nextType = nextTypeToFiles.getKey(); 354 for (String nextFile : nextTypeToFiles.getValue()) { 355 356 byte[] contents; 357 String contentsString; 358 try { 359 contents = packageFolder.fetchFile(nextFile); 360 contentsString = toUtf8String(contents); 361 } catch (IOException e) { 362 throw new InternalErrorException(Msg.code(1300) + e); 363 } 364 365 IBaseResource resource; 366 if (nextFile.toLowerCase().endsWith(".xml")) { 367 resource = packageContext.newXmlParser().parseResource(contentsString); 368 } else if (nextFile.toLowerCase().endsWith(".json")) { 369 resource = packageContext.newJsonParser().parseResource(contentsString); 370 } else { 371 getProcessingMessages(npmPackage).add("Not indexing file: " + nextFile); 372 continue; 373 } 374 375 /* 376 * Re-encode the resource as JSON with the narrative removed in order to reduce the footprint. 377 * This is useful since we'll be loading these resources back and hopefully keeping lots of 378 * them in memory in order to speed up validation activities. 379 */ 380 String contentType = Constants.CT_FHIR_JSON_NEW; 381 ResourceUtil.removeNarrative(packageContext, resource); 382 byte[] minimizedContents = packageContext 383 .newJsonParser() 384 .encodeResourceToString(resource) 385 .getBytes(StandardCharsets.UTF_8); 386 387 IBaseBinary resourceBinary = createPackageResourceBinary(minimizedContents, contentType); 388 ResourceTable persistedResource = createResourceBinary(resourceBinary); 389 390 NpmPackageVersionResourceEntity resourceEntity = new NpmPackageVersionResourceEntity(); 391 resourceEntity.setPackageVersion(packageVersion); 392 resourceEntity.setResourceBinary(persistedResource); 393 resourceEntity.setDirectory(dirName); 394 resourceEntity.setFhirVersionId(npmPackage.fhirVersion()); 395 resourceEntity.setFhirVersion(fhirVersion); 396 resourceEntity.setFilename(nextFile); 397 resourceEntity.setResourceType(nextType); 398 resourceEntity.setResSizeBytes(contents.length); 399 BaseRuntimeChildDefinition urlChild = 400 packageContext.getResourceDefinition(nextType).getChildByName("url"); 401 BaseRuntimeChildDefinition versionChild = 402 packageContext.getResourceDefinition(nextType).getChildByName("version"); 403 String url = null; 404 String version = null; 405 if (urlChild != null) { 406 url = urlChild.getAccessor() 407 .getFirstValueOrNull(resource) 408 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 409 .orElse(null); 410 resourceEntity.setCanonicalUrl(url); 411 412 Optional<IBase> resourceVersion = 413 versionChild.getAccessor().getFirstValueOrNull(resource); 414 if (resourceVersion.isPresent() && resourceVersion.get() instanceof IPrimitiveType) { 415 version = ((IPrimitiveType<?>) resourceVersion.get()).getValueAsString(); 416 } else if (resourceVersion.isPresent() 417 && resourceVersion.get() instanceof IBaseBackboneElement) { 418 version = String.valueOf(myCtx.newFhirPath() 419 .evaluateFirst(resourceVersion.get(), "value", IPrimitiveType.class) 420 .orElse(null)); 421 } else { 422 version = null; 423 } 424 resourceEntity.setCanonicalVersion(version); 425 } 426 myPackageVersionResourceDao.save(resourceEntity); 427 428 String resType = packageContext.getResourceType(resource); 429 String msg = "Indexing " + resType + " Resource[" + dirName + '/' + nextFile + "] with URL: " 430 + defaultString(url) + "|" + defaultString(version); 431 getProcessingMessages(npmPackage).add(msg); 432 ourLog.info("{}: Package[{}#{}] ", msg, packageId, packageVersionId); 433 } 434 } 435 436 getProcessingMessages(npmPackage) 437 .add("Successfully added package " + npmPackage.id() + "#" + npmPackage.version() + " to registry"); 438 439 return npmPackage; 440 }); 441 } 442 443 @Override 444 public NpmPackage addPackageToCache( 445 String thePackageId, String thePackageVersionId, InputStream thePackageTgzInputStream, String theSourceDesc) 446 throws IOException { 447 NpmPackageData npmData = myPackageLoaderSvc.createNpmPackageDataFromData( 448 thePackageId, thePackageVersionId, theSourceDesc, thePackageTgzInputStream); 449 450 return addPackageToCacheInternal(npmData); 451 } 452 453 private ResourceTable createResourceBinary(IBaseBinary theResourceBinary) { 454 455 if (myPartitionSettings.isPartitioningEnabled()) { 456 SystemRequestDetails requestDetails = new SystemRequestDetails(); 457 if (myPartitionSettings.isUnnamedPartitionMode() && myPartitionSettings.getDefaultPartitionId() != null) { 458 requestDetails.setRequestPartitionId( 459 RequestPartitionId.fromPartitionId(myPartitionSettings.getDefaultPartitionId())); 460 } else { 461 requestDetails.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); 462 } 463 return (ResourceTable) 464 getBinaryDao().create(theResourceBinary, requestDetails).getEntity(); 465 } else { 466 return (ResourceTable) getBinaryDao() 467 .create(theResourceBinary, new SystemRequestDetails()) 468 .getEntity(); 469 } 470 } 471 472 private boolean updateCurrentVersionFlagForAllPackagesBasedOnNewIncomingVersion( 473 String thePackageId, String thePackageVersion) { 474 Collection<NpmPackageVersionEntity> existingVersions = myPackageVersionDao.findByPackageId(thePackageId); 475 boolean retVal = true; 476 477 for (NpmPackageVersionEntity next : existingVersions) { 478 int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), thePackageVersion); 479 assert cmp != 0; 480 if (cmp < 0) { 481 if (next.isCurrentVersion()) { 482 next.setCurrentVersion(false); 483 myPackageVersionDao.save(next); 484 } 485 } else { 486 retVal = false; 487 } 488 } 489 490 return retVal; 491 } 492 493 @Nonnull 494 public FhirContext getFhirContext(FhirVersionEnum theFhirVersion) { 495 return myVersionToContext.computeIfAbsent(theFhirVersion, FhirContext::new); 496 } 497 498 private IBaseBinary createPackageBinary(byte[] theBytes) { 499 IBaseBinary binary = BinaryUtil.newBinary(myCtx); 500 BinaryUtil.setData(myCtx, binary, theBytes, Constants.CT_APPLICATION_GZIP); 501 return binary; 502 } 503 504 private IBaseBinary createPackageResourceBinary(byte[] theBytes, String theContentType) { 505 IBaseBinary binary = BinaryUtil.newBinary(myCtx); 506 BinaryUtil.setData(myCtx, binary, theBytes, theContentType); 507 return binary; 508 } 509 510 private NpmPackageEntity createPackage(NpmPackage theNpmPackage) { 511 NpmPackageEntity entity = new NpmPackageEntity(); 512 entity.setPackageId(theNpmPackage.id()); 513 entity.setCurrentVersionId(theNpmPackage.version()); 514 return myPackageDao.save(entity); 515 } 516 517 @Override 518 @Transactional 519 public NpmPackage loadPackage(String thePackageId, String thePackageVersion) throws FHIRException, IOException { 520 return loadPackageInner(thePackageId, thePackageVersion); 521 } 522 523 @Nonnull 524 private NpmPackage loadPackageInner(String thePackageId, String thePackageVersion) throws IOException { 525 // check package cache 526 NpmPackage cachedPackage = loadPackageFromCacheOnlyInner(thePackageId, thePackageVersion); 527 if (cachedPackage != null) { 528 return cachedPackage; 529 } 530 531 // otherwise we have to load it from packageloader 532 NpmPackageData pkgData = myPackageLoaderSvc.fetchPackageFromPackageSpec(thePackageId, thePackageVersion); 533 534 try { 535 // and add it to the cache 536 NpmPackage retVal = addPackageToCacheInternal(pkgData); 537 getProcessingMessages(retVal) 538 .add( 539 0, 540 "Package fetched from server at: " 541 + pkgData.getPackage().url()); 542 return retVal; 543 } finally { 544 pkgData.getInputStream().close(); 545 } 546 } 547 548 @Override 549 @Transactional 550 public NpmPackage loadPackage(String theS) throws FHIRException, IOException { 551 return loadPackageInner(theS, null); 552 } 553 554 private TransactionTemplate newTxTemplate() { 555 return new TransactionTemplate(myTxManager); 556 } 557 558 @Override 559 @Transactional(propagation = Propagation.NEVER) 560 public NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException { 561 Validate.notBlank(theInstallationSpec.getName(), "thePackageId must not be blank"); 562 Validate.notBlank(theInstallationSpec.getVersion(), "thePackageVersion must not be blank"); 563 564 String sourceDescription = "Embedded content"; 565 if (isNotBlank(theInstallationSpec.getPackageUrl())) { 566 byte[] contents = myPackageLoaderSvc.loadPackageUrlContents(theInstallationSpec.getPackageUrl()); 567 theInstallationSpec.setPackageContents(contents); 568 sourceDescription = theInstallationSpec.getPackageUrl(); 569 } 570 571 if (theInstallationSpec.getPackageContents() != null) { 572 return addPackageToCache( 573 theInstallationSpec.getName(), 574 theInstallationSpec.getVersion(), 575 new ByteArrayInputStream(theInstallationSpec.getPackageContents()), 576 sourceDescription); 577 } 578 579 return newTxTemplate().execute(tx -> { 580 try { 581 return loadPackageInner(theInstallationSpec.getName(), theInstallationSpec.getVersion()); 582 } catch (IOException e) { 583 throw new InternalErrorException(Msg.code(1302) + e); 584 } 585 }); 586 } 587 588 @Override 589 @Transactional(readOnly = true) 590 public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl) { 591 592 final List<NpmPackageVersionResourceEntity> npmPackageVersionResourceEntities = 593 loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, 2, null, null); 594 595 if (npmPackageVersionResourceEntities.isEmpty()) { 596 return null; 597 } else { 598 if (npmPackageVersionResourceEntities.size() > 1) { 599 ourLog.warn( 600 "Found multiple package versions for FHIR version: {} and canonical URL: {}", 601 theFhirVersion, 602 theCanonicalUrl); 603 } 604 final NpmPackageVersionResourceEntity contents = npmPackageVersionResourceEntities.get(0); 605 return loadPackageEntity(contents); 606 } 607 } 608 609 @Override 610 @Transactional(readOnly = true) 611 public IBaseResource findPackageAsset(FindPackageAssetRequest theRequest) { 612 613 final List<NpmPackageVersionResourceEntity> npmPackageVersionResourceEntities = loadPackageInfoByCanonicalUrl( 614 theRequest.getFhirVersion(), 615 theRequest.getCanonicalUrl(), 616 2, // We set it to 2 so that if we get more than one we can warn 617 theRequest.getPackageId(), 618 theRequest.getVersion()); 619 620 if (npmPackageVersionResourceEntities.isEmpty()) { 621 throw new ResourceNotFoundException( 622 "%s Could not find asset for FHIR version: %s, canonical URL: %s, package ID: %s and package version: %s" 623 .formatted( 624 Msg.code(2644), 625 theRequest.getFhirVersion(), 626 theRequest.getCanonicalUrl(), 627 theRequest.getPackageId(), 628 Optional.ofNullable(theRequest.getVersion()).orElse("[none]"))); 629 } else { 630 if (npmPackageVersionResourceEntities.size() > 1) { 631 ourLog.warn( 632 "Found multiple package versions for FHIR version: {} and canonical URL: {}", 633 theRequest.getFhirVersion(), 634 theRequest.getCanonicalUrl()); 635 } 636 final NpmPackageVersionResourceEntity contents = npmPackageVersionResourceEntities.get(0); 637 return loadPackageEntity(contents); 638 } 639 } 640 641 @Override 642 @Transactional(readOnly = true) 643 public List<NpmPackageAssetInfoJson> findPackageAssetInfoByUrl( 644 FhirVersionEnum theFhirVersion, String theCanonicalUrl) { 645 final List<NpmPackageVersionResourceEntity> npmPackageVersionResourceEntities = 646 loadPackageInfoByCanonicalUrl(theFhirVersion, theCanonicalUrl, 20, null, null); 647 648 return npmPackageVersionResourceEntities.stream() 649 .map(entity -> new NpmPackageAssetInfoJson( 650 entity.getResourceBinary().asTypedFhirResourceId(), 651 entity.getCanonicalUrl(), 652 entity.getFhirVersion(), 653 entity.getPackageId(), 654 entity.getPackageVersion())) 655 .collect(Collectors.toUnmodifiableList()); 656 } 657 658 private List<NpmPackageVersionResourceEntity> loadPackageInfoByCanonicalUrl( 659 FhirVersionEnum theFhirVersion, 660 String theCanonicalUrl, 661 int thePageSize, 662 @Nullable String thePackageId, 663 @Nullable String theVersionId) { 664 String canonicalUrl = theCanonicalUrl; 665 666 int versionSeparator = canonicalUrl.lastIndexOf('|'); 667 Slice<NpmPackageVersionResourceEntity> slice; 668 669 final PageRequest pageRequest = PageRequest.of(0, thePageSize); 670 671 if (versionSeparator != -1) { 672 String canonicalVersion = canonicalUrl.substring(versionSeparator + 1); 673 canonicalUrl = canonicalUrl.substring(0, versionSeparator); 674 675 if (thePackageId != null) { 676 if (theVersionId != null) { 677 slice = 678 myPackageVersionResourceDao 679 .findCurrentVersionByCanonicalUrlAndVersionAndPackageIdAndVersion( 680 pageRequest, 681 theFhirVersion, 682 canonicalUrl, 683 canonicalVersion, 684 thePackageId, 685 theVersionId); 686 } else { 687 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersionAndPackageId( 688 pageRequest, theFhirVersion, canonicalUrl, canonicalVersion, thePackageId); 689 } 690 } else { 691 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersion( 692 pageRequest, theFhirVersion, canonicalUrl, canonicalVersion); 693 } 694 695 } else { 696 if (thePackageId != null) { 697 if (theVersionId != null) { 698 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageIdAndVersion( 699 pageRequest, theFhirVersion, canonicalUrl, thePackageId, theVersionId); 700 } else { 701 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndPackageId( 702 pageRequest, theFhirVersion, canonicalUrl, thePackageId); 703 } 704 } else { 705 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl( 706 pageRequest, theFhirVersion, canonicalUrl); 707 } 708 } 709 710 if (slice.isEmpty()) { 711 return List.of(); 712 } else { 713 return slice.getContent(); 714 } 715 } 716 717 private IBaseResource loadPackageEntity(NpmPackageVersionResourceEntity contents) { 718 return loadPackageAssetByVersionAndId( 719 contents.getFhirVersion(), contents.getResourceBinary().getResourceId()); 720 } 721 722 private IBaseResource loadPackageAssetByVersionAndId(FhirVersionEnum theFhirVersion, JpaPid theBinaryPid) { 723 try { 724 IBaseBinary binary = getBinaryDao().readByPid(theBinaryPid); 725 byte[] resourceContentsBytes = fetchBlobFromBinary(binary); 726 String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); 727 FhirContext packageContext = getFhirContext(theFhirVersion); 728 return EncodingEnum.detectEncoding(resourceContents) 729 .newParser(packageContext) 730 .parseResource(resourceContents); 731 } catch (Exception exception) { 732 throw new InvalidRequestException( 733 String.format( 734 "%sFailed to load package resource for FHIR version: %s and binary PID: %s", 735 Msg.code(1305), theFhirVersion, theBinaryPid), 736 exception); 737 } 738 } 739 740 @Override 741 @Transactional 742 public NpmPackageMetadataJson loadPackageMetadata(String thePackageId) { 743 NpmPackageMetadataJson retVal = new NpmPackageMetadataJson(); 744 745 Optional<NpmPackageEntity> pkg = myPackageDao.findByPackageId(thePackageId); 746 if (!pkg.isPresent()) { 747 throw new ResourceNotFoundException(Msg.code(1306) + "Unknown package ID: " + thePackageId); 748 } 749 750 List<NpmPackageVersionEntity> packageVersions = 751 new ArrayList<>(myPackageVersionDao.findByPackageId(thePackageId)); 752 packageVersions.sort(new ReverseComparator<>( 753 (o1, o2) -> PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId()))); 754 755 for (NpmPackageVersionEntity next : packageVersions) { 756 if (next.isCurrentVersion()) { 757 retVal.setDistTags(new NpmPackageMetadataJson.DistTags().setLatest(next.getVersionId())); 758 } 759 760 NpmPackageMetadataJson.Version version = new NpmPackageMetadataJson.Version(); 761 version.setFhirVersion(next.getFhirVersionId()); 762 version.setAuthor(next.getAuthor()); 763 version.setDescription(next.getDescription()); 764 version.setName(next.getPackageId()); 765 version.setVersion(next.getVersionId()); 766 version.setBytes(next.getPackageSizeBytes()); 767 retVal.addVersion(version); 768 } 769 770 return retVal; 771 } 772 773 @Override 774 @Transactional 775 public PackageContents loadPackageContents(String thePackageId, @Nullable String theVersion) { 776 Optional<NpmPackageVersionEntity> entity = loadPackageVersionEntity(thePackageId, theVersion); 777 return entity.map(this::loadPackageContents).orElse(null); 778 } 779 780 @Override 781 @Transactional 782 public NpmPackageSearchResultJson search(PackageSearchSpec thePackageSearchSpec) { 783 NpmPackageSearchResultJson retVal = new NpmPackageSearchResultJson(); 784 785 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 786 787 // Query for total 788 queryForTool(thePackageSearchSpec, cb, retVal); 789 790 // Query for results 791 queryForResults(thePackageSearchSpec, cb, retVal); 792 793 return retVal; 794 } 795 796 private void queryForTool( 797 PackageSearchSpec thePackageSearchSpec, CriteriaBuilder cb, NpmPackageSearchResultJson retVal) { 798 CriteriaQuery<Long> countCriteriaQuery = cb.createQuery(Long.class); 799 Root<NpmPackageVersionEntity> countCriteriaRoot = countCriteriaQuery.from(NpmPackageVersionEntity.class); 800 countCriteriaQuery.multiselect(cb.countDistinct(countCriteriaRoot.get("myPackageId"))); 801 802 List<Predicate> predicates = createSearchPredicates(thePackageSearchSpec, cb, countCriteriaRoot); 803 804 countCriteriaQuery.where(toPredicateArray(predicates)); 805 Long total = myEntityManager.createQuery(countCriteriaQuery).getSingleResult(); 806 retVal.setTotal(Math.toIntExact(total)); 807 } 808 809 private void queryForResults( 810 PackageSearchSpec thePackageSearchSpec, CriteriaBuilder cb, NpmPackageSearchResultJson retVal) { 811 CriteriaQuery<NpmPackageVersionEntity> criteriaQuery = cb.createQuery(NpmPackageVersionEntity.class); 812 Root<NpmPackageVersionEntity> root = criteriaQuery.from(NpmPackageVersionEntity.class); 813 814 List<Predicate> predicates = createSearchPredicates(thePackageSearchSpec, cb, root); 815 816 criteriaQuery.where(toPredicateArray(predicates)); 817 criteriaQuery.orderBy(cb.asc(root.get("myPackageId"))); 818 TypedQuery<NpmPackageVersionEntity> query = myEntityManager.createQuery(criteriaQuery); 819 query.setFirstResult(thePackageSearchSpec.getStart()); 820 query.setMaxResults(thePackageSearchSpec.getSize()); 821 822 List<NpmPackageVersionEntity> resultList = query.getResultList(); 823 for (NpmPackageVersionEntity next : resultList) { 824 825 if (!retVal.hasPackageWithId(next.getPackageId())) { 826 retVal.addObject() 827 .getPackage() 828 .setName(next.getPackageId()) 829 .setAuthor(next.getAuthor()) 830 .setDescription(next.getDescription()) 831 .setVersion(next.getVersionId()) 832 .addFhirVersion(next.getFhirVersionId()) 833 .setBytes(next.getPackageSizeBytes()); 834 } else { 835 NpmPackageSearchResultJson.Package retPackage = retVal.getPackageWithId(next.getPackageId()); 836 retPackage.addFhirVersion(next.getFhirVersionId()); 837 838 int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), retPackage.getVersion()); 839 if (cmp > 0) { 840 retPackage.setVersion(next.getVersionId()); 841 } 842 } 843 } 844 } 845 846 @Override 847 @Transactional 848 public PackageDeleteOutcomeJson uninstallPackage(String thePackageId, String theVersion) { 849 PackageDeleteOutcomeJson retVal = new PackageDeleteOutcomeJson(); 850 851 Optional<NpmPackageVersionEntity> packageVersion = 852 myPackageVersionDao.findByPackageIdAndVersion(thePackageId, theVersion); 853 if (packageVersion.isPresent()) { 854 855 String msg = "Deleting package " + thePackageId + "#" + theVersion; 856 ourLog.info(msg); 857 retVal.getMessage().add(msg); 858 859 for (NpmPackageVersionResourceEntity next : packageVersion.get().getResources()) { 860 msg = "Deleting package +" + thePackageId + "#" + theVersion + "resource: " + next.getCanonicalUrl(); 861 ourLog.info(msg); 862 retVal.getMessage().add(msg); 863 864 myPackageVersionResourceDao.delete(next); 865 866 ExpungeOptions options = new ExpungeOptions(); 867 options.setExpungeDeletedResources(true).setExpungeOldVersions(true); 868 deleteAndExpungeResourceBinary( 869 next.getResourceBinary().getIdDt().toVersionless(), options); 870 } 871 872 myPackageVersionDao.delete(packageVersion.get()); 873 874 ExpungeOptions options = new ExpungeOptions(); 875 options.setExpungeDeletedResources(true).setExpungeOldVersions(true); 876 deleteAndExpungeResourceBinary( 877 packageVersion.get().getPackageBinary().getIdDt().toVersionless(), options); 878 879 Collection<NpmPackageVersionEntity> remainingVersions = myPackageVersionDao.findByPackageId(thePackageId); 880 if (remainingVersions.isEmpty()) { 881 msg = "No versions of package " + thePackageId + " remain"; 882 ourLog.info(msg); 883 retVal.getMessage().add(msg); 884 Optional<NpmPackageEntity> pkgEntity = myPackageDao.findByPackageId(thePackageId); 885 pkgEntity.ifPresent(pkgEntityPresent -> myPackageDao.delete(pkgEntityPresent)); 886 } else { 887 888 List<NpmPackageVersionEntity> versions = remainingVersions.stream() 889 .sorted((o1, o2) -> 890 PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId())) 891 .collect(Collectors.toList()); 892 for (int i = 0; i < versions.size(); i++) { 893 boolean isCurrent = i == versions.size() - 1; 894 if (isCurrent != versions.get(i).isCurrentVersion()) { 895 versions.get(i).setCurrentVersion(isCurrent); 896 myPackageVersionDao.save(versions.get(i)); 897 } 898 } 899 } 900 901 } else { 902 903 String msg = "No package found with the given ID"; 904 retVal.getMessage().add(msg); 905 } 906 907 return retVal; 908 } 909 910 @Override 911 @Transactional 912 public List<IBaseResource> loadPackageAssetsByType(FhirVersionEnum theFhirVersion, String theResourceType) { 913 Slice<NpmPackageVersionResourceEntity> outcome = myPackageVersionResourceDao.findCurrentVersionByResourceType( 914 PageRequest.of(0, 1000), theFhirVersion, theResourceType); 915 return outcome.stream().map(this::loadPackageEntity).collect(Collectors.toList()); 916 } 917 918 private void deleteAndExpungeResourceBinary(IIdType theResourceBinaryId, ExpungeOptions theOptions) { 919 getBinaryDao().delete(theResourceBinaryId, new SystemRequestDetails()).getEntity(); 920 getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, new SystemRequestDetails()); 921 } 922 923 @Nonnull 924 public List<Predicate> createSearchPredicates( 925 PackageSearchSpec thePackageSearchSpec, CriteriaBuilder theCb, Root<NpmPackageVersionEntity> theRoot) { 926 List<Predicate> predicates = new ArrayList<>(); 927 928 if (isNotBlank(thePackageSearchSpec.getResourceUrl())) { 929 Join<NpmPackageVersionEntity, NpmPackageVersionResourceEntity> resources = 930 theRoot.join("myResources", JoinType.LEFT); 931 932 predicates.add(theCb.equal(resources.get("myCanonicalUrl"), thePackageSearchSpec.getResourceUrl())); 933 } 934 935 if (isNotBlank(thePackageSearchSpec.getVersion())) { 936 String searchTerm = thePackageSearchSpec.getVersion() + "%"; 937 predicates.add(theCb.like(theRoot.get("myVersionId"), searchTerm)); 938 } 939 940 if (isNotBlank(thePackageSearchSpec.getDescription())) { 941 String searchTerm = "%" + thePackageSearchSpec.getDescription() + "%"; 942 searchTerm = StringUtil.normalizeStringForSearchIndexing(searchTerm); 943 predicates.add(theCb.like(theCb.upper(theRoot.get("myDescriptionUpper")), searchTerm)); 944 } 945 946 if (isNotBlank(thePackageSearchSpec.getAuthor())) { 947 String searchTerm = "%" + thePackageSearchSpec.getAuthor() + "%"; 948 searchTerm = StringUtil.normalizeStringForSearchIndexing(searchTerm); 949 predicates.add(theCb.like(theRoot.get("myAuthorUpper"), searchTerm)); 950 } 951 952 if (isNotBlank(thePackageSearchSpec.getFhirVersion())) { 953 if (!PATTERN_FHIR_VERSION 954 .matcher(thePackageSearchSpec.getFhirVersion()) 955 .matches()) { 956 FhirVersionEnum versionEnum = FhirVersionEnum.forVersionString(thePackageSearchSpec.getFhirVersion()); 957 if (versionEnum != null) { 958 predicates.add(theCb.equal(theRoot.get("myFhirVersion").as(String.class), versionEnum.name())); 959 } 960 } else { 961 predicates.add(theCb.like(theRoot.get("myFhirVersionId"), thePackageSearchSpec.getFhirVersion() + "%")); 962 } 963 } 964 965 return predicates; 966 } 967 968 @SuppressWarnings("unchecked") 969 public static List<String> getProcessingMessages(NpmPackage thePackage) { 970 return (List<String>) 971 thePackage.getUserData().computeIfAbsent("JpPackageCache_ProcessingMessages", t -> new ArrayList<>()); 972 } 973 974 /** 975 * Truncates a string to {@link NpmPackageVersionEntity#PACKAGE_DESC_LENGTH} which is 976 * the maximum length used on several columns in {@link NpmPackageVersionEntity}. If the 977 * string is longer than the maximum allowed, the last 3 characters are replaced with "..." 978 */ 979 private static String truncateStorageString(String theInput) { 980 String retVal = null; 981 if (theInput != null) { 982 if (theInput.length() > NpmPackageVersionEntity.PACKAGE_DESC_LENGTH) { 983 retVal = theInput.substring(0, NpmPackageVersionEntity.PACKAGE_DESC_LENGTH - 4) + "..."; 984 } else { 985 retVal = theInput; 986 } 987 } 988 return retVal; 989 } 990}