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