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