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.fetchDataBlobFromBinary(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 String packageDesc = null; 305 if (npmPackage.description() != null) { 306 if (npmPackage.description().length() > NpmPackageVersionEntity.PACKAGE_DESC_LENGTH) { 307 packageDesc = npmPackage.description().substring(0, NpmPackageVersionEntity.PACKAGE_DESC_LENGTH - 4) 308 + "..."; 309 } else { 310 packageDesc = npmPackage.description(); 311 } 312 } 313 if (currentVersion) { 314 getProcessingMessages(npmPackage) 315 .add("Marking package " + packageId + "#" + initialPackageVersionId + " as current version"); 316 pkg.setCurrentVersionId(packageVersionId); 317 pkg.setDescription(packageDesc); 318 myPackageDao.save(pkg); 319 } else { 320 getProcessingMessages(npmPackage) 321 .add("Package " + packageId + "#" + initialPackageVersionId + " is not the newest version"); 322 } 323 324 packageVersion = new NpmPackageVersionEntity(); 325 packageVersion.setPackageId(packageId); 326 packageVersion.setVersionId(packageVersionId); 327 packageVersion.setPackage(pkg); 328 packageVersion.setPackageBinary(persistedPackage); 329 packageVersion.setSavedTime(new Date()); 330 packageVersion.setDescription(packageDesc); 331 packageVersion.setFhirVersionId(npmPackage.fhirVersion()); 332 packageVersion.setFhirVersion(fhirVersion); 333 packageVersion.setCurrentVersion(currentVersion); 334 packageVersion.setPackageSizeBytes(bytes.length); 335 packageVersion = myPackageVersionDao.save(packageVersion); 336 337 String dirName = "package"; 338 NpmPackage.NpmPackageFolder packageFolder = npmPackage.getFolders().get(dirName); 339 Map<String, List<String>> packageFolderTypes = null; 340 try { 341 packageFolderTypes = packageFolder.getTypes(); 342 } catch (IOException e) { 343 throw new InternalErrorException(Msg.code(2371) + e); 344 } 345 for (Map.Entry<String, List<String>> nextTypeToFiles : packageFolderTypes.entrySet()) { 346 String nextType = nextTypeToFiles.getKey(); 347 for (String nextFile : nextTypeToFiles.getValue()) { 348 349 byte[] contents; 350 String contentsString; 351 try { 352 contents = packageFolder.fetchFile(nextFile); 353 contentsString = toUtf8String(contents); 354 } catch (IOException e) { 355 throw new InternalErrorException(Msg.code(1300) + e); 356 } 357 358 IBaseResource resource; 359 if (nextFile.toLowerCase().endsWith(".xml")) { 360 resource = packageContext.newXmlParser().parseResource(contentsString); 361 } else if (nextFile.toLowerCase().endsWith(".json")) { 362 resource = packageContext.newJsonParser().parseResource(contentsString); 363 } else { 364 getProcessingMessages(npmPackage).add("Not indexing file: " + nextFile); 365 continue; 366 } 367 368 /* 369 * Re-encode the resource as JSON with the narrative removed in order to reduce the footprint. 370 * This is useful since we'll be loading these resources back and hopefully keeping lots of 371 * them in memory in order to speed up validation activities. 372 */ 373 String contentType = Constants.CT_FHIR_JSON_NEW; 374 ResourceUtil.removeNarrative(packageContext, resource); 375 byte[] minimizedContents = packageContext 376 .newJsonParser() 377 .encodeResourceToString(resource) 378 .getBytes(StandardCharsets.UTF_8); 379 380 IBaseBinary resourceBinary = createPackageResourceBinary(nextFile, minimizedContents, contentType); 381 ResourceTable persistedResource = createResourceBinary(resourceBinary); 382 383 NpmPackageVersionResourceEntity resourceEntity = new NpmPackageVersionResourceEntity(); 384 resourceEntity.setPackageVersion(packageVersion); 385 resourceEntity.setResourceBinary(persistedResource); 386 resourceEntity.setDirectory(dirName); 387 resourceEntity.setFhirVersionId(npmPackage.fhirVersion()); 388 resourceEntity.setFhirVersion(fhirVersion); 389 resourceEntity.setFilename(nextFile); 390 resourceEntity.setResourceType(nextType); 391 resourceEntity.setResSizeBytes(contents.length); 392 BaseRuntimeChildDefinition urlChild = 393 packageContext.getResourceDefinition(nextType).getChildByName("url"); 394 BaseRuntimeChildDefinition versionChild = 395 packageContext.getResourceDefinition(nextType).getChildByName("version"); 396 String url = null; 397 String version = null; 398 if (urlChild != null) { 399 url = urlChild.getAccessor() 400 .getFirstValueOrNull(resource) 401 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 402 .orElse(null); 403 resourceEntity.setCanonicalUrl(url); 404 version = versionChild 405 .getAccessor() 406 .getFirstValueOrNull(resource) 407 .map(t -> ((IPrimitiveType<?>) t).getValueAsString()) 408 .orElse(null); 409 resourceEntity.setCanonicalVersion(version); 410 } 411 myPackageVersionResourceDao.save(resourceEntity); 412 413 String resType = packageContext.getResourceType(resource); 414 String msg = "Indexing " + resType + " Resource[" + dirName + '/' + nextFile + "] with URL: " 415 + defaultString(url) + "|" + defaultString(version); 416 getProcessingMessages(npmPackage).add(msg); 417 ourLog.info("Package[{}#{}] " + msg, packageId, packageVersionId); 418 } 419 } 420 421 getProcessingMessages(npmPackage) 422 .add("Successfully added package " + npmPackage.id() + "#" + npmPackage.version() + " to registry"); 423 424 return npmPackage; 425 }); 426 } 427 428 @Override 429 public NpmPackage addPackageToCache( 430 String thePackageId, String thePackageVersionId, InputStream thePackageTgzInputStream, String theSourceDesc) 431 throws IOException { 432 NpmPackageData npmData = myPackageLoaderSvc.createNpmPackageDataFromData( 433 thePackageId, thePackageVersionId, theSourceDesc, thePackageTgzInputStream); 434 435 return addPackageToCacheInternal(npmData); 436 } 437 438 private ResourceTable createResourceBinary(IBaseBinary theResourceBinary) { 439 440 if (myPartitionSettings.isPartitioningEnabled()) { 441 SystemRequestDetails requestDetails = new SystemRequestDetails(); 442 if (myPartitionSettings.isUnnamedPartitionMode() && myPartitionSettings.getDefaultPartitionId() != null) { 443 requestDetails.setRequestPartitionId( 444 RequestPartitionId.fromPartitionId(myPartitionSettings.getDefaultPartitionId())); 445 } else { 446 requestDetails.setTenantId(JpaConstants.DEFAULT_PARTITION_NAME); 447 } 448 return (ResourceTable) 449 getBinaryDao().create(theResourceBinary, requestDetails).getEntity(); 450 } else { 451 return (ResourceTable) getBinaryDao().create(theResourceBinary).getEntity(); 452 } 453 } 454 455 private boolean updateCurrentVersionFlagForAllPackagesBasedOnNewIncomingVersion( 456 String thePackageId, String thePackageVersion) { 457 Collection<NpmPackageVersionEntity> existingVersions = myPackageVersionDao.findByPackageId(thePackageId); 458 boolean retVal = true; 459 460 for (NpmPackageVersionEntity next : existingVersions) { 461 int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), thePackageVersion); 462 assert cmp != 0; 463 if (cmp < 0) { 464 if (next.isCurrentVersion()) { 465 next.setCurrentVersion(false); 466 myPackageVersionDao.save(next); 467 } 468 } else { 469 retVal = false; 470 } 471 } 472 473 return retVal; 474 } 475 476 @Nonnull 477 public FhirContext getFhirContext(FhirVersionEnum theFhirVersion) { 478 return myVersionToContext.computeIfAbsent(theFhirVersion, v -> new FhirContext(v)); 479 } 480 481 private IBaseBinary createPackageBinary(byte[] theBytes) { 482 IBaseBinary binary = BinaryUtil.newBinary(myCtx); 483 BinaryUtil.setData(myCtx, binary, theBytes, Constants.CT_APPLICATION_GZIP); 484 return binary; 485 } 486 487 private IBaseBinary createPackageResourceBinary(String theFileName, byte[] theBytes, String theContentType) { 488 IBaseBinary binary = BinaryUtil.newBinary(myCtx); 489 BinaryUtil.setData(myCtx, binary, theBytes, theContentType); 490 return binary; 491 } 492 493 private NpmPackageEntity createPackage(NpmPackage theNpmPackage) { 494 NpmPackageEntity entity = new NpmPackageEntity(); 495 entity.setPackageId(theNpmPackage.id()); 496 entity.setCurrentVersionId(theNpmPackage.version()); 497 return myPackageDao.save(entity); 498 } 499 500 @Override 501 @Transactional 502 public NpmPackage loadPackage(String thePackageId, String thePackageVersion) throws FHIRException, IOException { 503 // check package cache 504 NpmPackage cachedPackage = loadPackageFromCacheOnly(thePackageId, thePackageVersion); 505 if (cachedPackage != null) { 506 return cachedPackage; 507 } 508 509 // otherwise we have to load it from packageloader 510 NpmPackageData pkgData = myPackageLoaderSvc.fetchPackageFromPackageSpec(thePackageId, thePackageVersion); 511 512 try { 513 // and add it to the cache 514 NpmPackage retVal = addPackageToCacheInternal(pkgData); 515 getProcessingMessages(retVal) 516 .add( 517 0, 518 "Package fetched from server at: " 519 + pkgData.getPackage().url()); 520 return retVal; 521 } finally { 522 pkgData.getInputStream().close(); 523 } 524 } 525 526 @Override 527 public NpmPackage loadPackage(String theS) throws FHIRException, IOException { 528 return loadPackage(theS, null); 529 } 530 531 private TransactionTemplate newTxTemplate() { 532 return new TransactionTemplate(myTxManager); 533 } 534 535 @Override 536 @Transactional(propagation = Propagation.NEVER) 537 public NpmPackage installPackage(PackageInstallationSpec theInstallationSpec) throws IOException { 538 Validate.notBlank(theInstallationSpec.getName(), "thePackageId must not be blank"); 539 Validate.notBlank(theInstallationSpec.getVersion(), "thePackageVersion must not be blank"); 540 541 String sourceDescription = "Embedded content"; 542 if (isNotBlank(theInstallationSpec.getPackageUrl())) { 543 byte[] contents = myPackageLoaderSvc.loadPackageUrlContents(theInstallationSpec.getPackageUrl()); 544 theInstallationSpec.setPackageContents(contents); 545 sourceDescription = theInstallationSpec.getPackageUrl(); 546 } 547 548 if (theInstallationSpec.getPackageContents() != null) { 549 return addPackageToCache( 550 theInstallationSpec.getName(), 551 theInstallationSpec.getVersion(), 552 new ByteArrayInputStream(theInstallationSpec.getPackageContents()), 553 sourceDescription); 554 } 555 556 return newTxTemplate().execute(tx -> { 557 try { 558 return loadPackage(theInstallationSpec.getName(), theInstallationSpec.getVersion()); 559 } catch (IOException e) { 560 throw new InternalErrorException(Msg.code(1302) + e); 561 } 562 }); 563 } 564 565 @Override 566 @Transactional 567 public IBaseResource loadPackageAssetByUrl(FhirVersionEnum theFhirVersion, String theCanonicalUrl) { 568 569 String canonicalUrl = theCanonicalUrl; 570 571 int versionSeparator = canonicalUrl.lastIndexOf('|'); 572 Slice<NpmPackageVersionResourceEntity> slice; 573 if (versionSeparator != -1) { 574 String canonicalVersion = canonicalUrl.substring(versionSeparator + 1); 575 canonicalUrl = canonicalUrl.substring(0, versionSeparator); 576 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrlAndVersion( 577 PageRequest.of(0, 1), theFhirVersion, canonicalUrl, canonicalVersion); 578 } else { 579 slice = myPackageVersionResourceDao.findCurrentVersionByCanonicalUrl( 580 PageRequest.of(0, 1), theFhirVersion, canonicalUrl); 581 } 582 583 if (slice.isEmpty()) { 584 return null; 585 } else { 586 NpmPackageVersionResourceEntity contents = slice.getContent().get(0); 587 return loadPackageEntity(contents); 588 } 589 } 590 591 private IBaseResource loadPackageEntity(NpmPackageVersionResourceEntity contents) { 592 try { 593 JpaPid binaryPid = JpaPid.fromId(contents.getResourceBinary().getId()); 594 IBaseBinary binary = getBinaryDao().readByPid(binaryPid); 595 byte[] resourceContentsBytes = fetchBlobFromBinary(binary); 596 String resourceContents = new String(resourceContentsBytes, StandardCharsets.UTF_8); 597 FhirContext packageContext = getFhirContext(contents.getFhirVersion()); 598 return EncodingEnum.detectEncoding(resourceContents) 599 .newParser(packageContext) 600 .parseResource(resourceContents); 601 } catch (Exception e) { 602 throw new RuntimeException(Msg.code(1305) + "Failed to load package resource " + contents, e); 603 } 604 } 605 606 @Override 607 @Transactional 608 public NpmPackageMetadataJson loadPackageMetadata(String thePackageId) { 609 NpmPackageMetadataJson retVal = new NpmPackageMetadataJson(); 610 611 Optional<NpmPackageEntity> pkg = myPackageDao.findByPackageId(thePackageId); 612 if (!pkg.isPresent()) { 613 throw new ResourceNotFoundException(Msg.code(1306) + "Unknown package ID: " + thePackageId); 614 } 615 616 List<NpmPackageVersionEntity> packageVersions = 617 new ArrayList<>(myPackageVersionDao.findByPackageId(thePackageId)); 618 packageVersions.sort(new ReverseComparator<>( 619 (o1, o2) -> PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId()))); 620 621 for (NpmPackageVersionEntity next : packageVersions) { 622 if (next.isCurrentVersion()) { 623 retVal.setDistTags(new NpmPackageMetadataJson.DistTags().setLatest(next.getVersionId())); 624 } 625 626 NpmPackageMetadataJson.Version version = new NpmPackageMetadataJson.Version(); 627 version.setFhirVersion(next.getFhirVersionId()); 628 version.setDescription(next.getDescription()); 629 version.setName(next.getPackageId()); 630 version.setVersion(next.getVersionId()); 631 version.setBytes(next.getPackageSizeBytes()); 632 retVal.addVersion(version); 633 } 634 635 return retVal; 636 } 637 638 @Override 639 @Transactional 640 public PackageContents loadPackageContents(String thePackageId, String theVersion) { 641 Optional<NpmPackageVersionEntity> entity = loadPackageVersionEntity(thePackageId, theVersion); 642 return entity.map(t -> loadPackageContents(t)).orElse(null); 643 } 644 645 @Override 646 @Transactional 647 public NpmPackageSearchResultJson search(PackageSearchSpec thePackageSearchSpec) { 648 NpmPackageSearchResultJson retVal = new NpmPackageSearchResultJson(); 649 650 CriteriaBuilder cb = myEntityManager.getCriteriaBuilder(); 651 652 // Query for total 653 { 654 CriteriaQuery<Long> countCriteriaQuery = cb.createQuery(Long.class); 655 Root<NpmPackageVersionEntity> countCriteriaRoot = countCriteriaQuery.from(NpmPackageVersionEntity.class); 656 countCriteriaQuery.multiselect(cb.countDistinct(countCriteriaRoot.get("myPackageId"))); 657 658 List<Predicate> predicates = createSearchPredicates(thePackageSearchSpec, cb, countCriteriaRoot); 659 660 countCriteriaQuery.where(toPredicateArray(predicates)); 661 Long total = myEntityManager.createQuery(countCriteriaQuery).getSingleResult(); 662 retVal.setTotal(Math.toIntExact(total)); 663 } 664 665 // Query for results 666 { 667 CriteriaQuery<NpmPackageVersionEntity> criteriaQuery = cb.createQuery(NpmPackageVersionEntity.class); 668 Root<NpmPackageVersionEntity> root = criteriaQuery.from(NpmPackageVersionEntity.class); 669 670 List<Predicate> predicates = createSearchPredicates(thePackageSearchSpec, cb, root); 671 672 criteriaQuery.where(toPredicateArray(predicates)); 673 criteriaQuery.orderBy(cb.asc(root.get("myPackageId"))); 674 TypedQuery<NpmPackageVersionEntity> query = myEntityManager.createQuery(criteriaQuery); 675 query.setFirstResult(thePackageSearchSpec.getStart()); 676 query.setMaxResults(thePackageSearchSpec.getSize()); 677 678 List<NpmPackageVersionEntity> resultList = query.getResultList(); 679 for (NpmPackageVersionEntity next : resultList) { 680 681 if (!retVal.hasPackageWithId(next.getPackageId())) { 682 retVal.addObject() 683 .getPackage() 684 .setName(next.getPackageId()) 685 .setDescription(next.getPackage().getDescription()) 686 .setVersion(next.getVersionId()) 687 .addFhirVersion(next.getFhirVersionId()) 688 .setBytes(next.getPackageSizeBytes()); 689 } else { 690 NpmPackageSearchResultJson.Package retPackage = retVal.getPackageWithId(next.getPackageId()); 691 retPackage.addFhirVersion(next.getFhirVersionId()); 692 693 int cmp = PackageVersionComparator.INSTANCE.compare(next.getVersionId(), retPackage.getVersion()); 694 if (cmp > 0) { 695 retPackage.setVersion(next.getVersionId()); 696 } 697 } 698 } 699 } 700 701 return retVal; 702 } 703 704 @Override 705 @Transactional 706 public PackageDeleteOutcomeJson uninstallPackage(String thePackageId, String theVersion) { 707 PackageDeleteOutcomeJson retVal = new PackageDeleteOutcomeJson(); 708 709 Optional<NpmPackageVersionEntity> packageVersion = 710 myPackageVersionDao.findByPackageIdAndVersion(thePackageId, theVersion); 711 if (packageVersion.isPresent()) { 712 713 String msg = "Deleting package " + thePackageId + "#" + theVersion; 714 ourLog.info(msg); 715 retVal.getMessage().add(msg); 716 717 for (NpmPackageVersionResourceEntity next : packageVersion.get().getResources()) { 718 msg = "Deleting package +" + thePackageId + "#" + theVersion + "resource: " + next.getCanonicalUrl(); 719 ourLog.info(msg); 720 retVal.getMessage().add(msg); 721 722 myPackageVersionResourceDao.delete(next); 723 724 ExpungeOptions options = new ExpungeOptions(); 725 options.setExpungeDeletedResources(true).setExpungeOldVersions(true); 726 deleteAndExpungeResourceBinary( 727 next.getResourceBinary().getIdDt().toVersionless(), options); 728 } 729 730 myPackageVersionDao.delete(packageVersion.get()); 731 732 ExpungeOptions options = new ExpungeOptions(); 733 options.setExpungeDeletedResources(true).setExpungeOldVersions(true); 734 deleteAndExpungeResourceBinary( 735 packageVersion.get().getPackageBinary().getIdDt().toVersionless(), options); 736 737 Collection<NpmPackageVersionEntity> remainingVersions = myPackageVersionDao.findByPackageId(thePackageId); 738 if (remainingVersions.size() == 0) { 739 msg = "No versions of package " + thePackageId + " remain"; 740 ourLog.info(msg); 741 retVal.getMessage().add(msg); 742 Optional<NpmPackageEntity> pkgEntity = myPackageDao.findByPackageId(thePackageId); 743 myPackageDao.delete(pkgEntity.get()); 744 } else { 745 746 List<NpmPackageVersionEntity> versions = remainingVersions.stream() 747 .sorted((o1, o2) -> 748 PackageVersionComparator.INSTANCE.compare(o1.getVersionId(), o2.getVersionId())) 749 .collect(Collectors.toList()); 750 for (int i = 0; i < versions.size(); i++) { 751 boolean isCurrent = i == versions.size() - 1; 752 if (isCurrent != versions.get(i).isCurrentVersion()) { 753 versions.get(i).setCurrentVersion(isCurrent); 754 myPackageVersionDao.save(versions.get(i)); 755 } 756 } 757 } 758 759 } else { 760 761 String msg = "No package found with the given ID"; 762 retVal.getMessage().add(msg); 763 } 764 765 return retVal; 766 } 767 768 @Override 769 @Transactional 770 public List<IBaseResource> loadPackageAssetsByType(FhirVersionEnum theFhirVersion, String theResourceType) { 771 // List<NpmPackageVersionResourceEntity> outcome = myPackageVersionResourceDao.findAll(); 772 Slice<NpmPackageVersionResourceEntity> outcome = myPackageVersionResourceDao.findCurrentVersionByResourceType( 773 PageRequest.of(0, 1000), theFhirVersion, theResourceType); 774 return outcome.stream().map(t -> loadPackageEntity(t)).collect(Collectors.toList()); 775 } 776 777 private void deleteAndExpungeResourceBinary(IIdType theResourceBinaryId, ExpungeOptions theOptions) { 778 getBinaryDao().delete(theResourceBinaryId, new SystemRequestDetails()).getEntity(); 779 getBinaryDao().forceExpungeInExistingTransaction(theResourceBinaryId, theOptions, new SystemRequestDetails()); 780 } 781 782 @Nonnull 783 public List<Predicate> createSearchPredicates( 784 PackageSearchSpec thePackageSearchSpec, CriteriaBuilder theCb, Root<NpmPackageVersionEntity> theRoot) { 785 List<Predicate> predicates = new ArrayList<>(); 786 787 if (isNotBlank(thePackageSearchSpec.getResourceUrl())) { 788 Join<NpmPackageVersionEntity, NpmPackageVersionResourceEntity> resources = 789 theRoot.join("myResources", JoinType.LEFT); 790 791 predicates.add(theCb.equal(resources.get("myCanonicalUrl"), thePackageSearchSpec.getResourceUrl())); 792 } 793 794 if (isNotBlank(thePackageSearchSpec.getDescription())) { 795 String searchTerm = "%" + thePackageSearchSpec.getDescription() + "%"; 796 searchTerm = StringUtil.normalizeStringForSearchIndexing(searchTerm); 797 predicates.add(theCb.like(theRoot.get("myDescriptionUpper"), searchTerm)); 798 } 799 800 if (isNotBlank(thePackageSearchSpec.getFhirVersion())) { 801 if (!thePackageSearchSpec.getFhirVersion().matches("([0-9]+\\.)+[0-9]+")) { 802 FhirVersionEnum versionEnum = FhirVersionEnum.forVersionString(thePackageSearchSpec.getFhirVersion()); 803 if (versionEnum != null) { 804 predicates.add(theCb.equal(theRoot.get("myFhirVersion").as(String.class), versionEnum.name())); 805 } 806 } else { 807 predicates.add(theCb.like(theRoot.get("myFhirVersionId"), thePackageSearchSpec.getFhirVersion() + "%")); 808 } 809 } 810 811 return predicates; 812 } 813 814 @SuppressWarnings("unchecked") 815 public static List<String> getProcessingMessages(NpmPackage thePackage) { 816 return (List<String>) 817 thePackage.getUserData().computeIfAbsent("JpPackageCache_ProcessingMessages", t -> new ArrayList<>()); 818 } 819}