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