|
28 | 28 | import java.util.Map; |
29 | 29 | import java.util.Set; |
30 | 30 | import java.util.UUID; |
| 31 | +import java.util.function.Function; |
31 | 32 | import java.util.logging.Level; |
32 | 33 | import java.util.logging.Logger; |
| 34 | +import java.util.stream.Collectors; |
| 35 | + |
33 | 36 | import jakarta.ejb.EJB; |
34 | 37 | import jakarta.ejb.Stateless; |
35 | 38 | import jakarta.ejb.TransactionAttribute; |
36 | 39 | import jakarta.ejb.TransactionAttributeType; |
37 | 40 | import jakarta.inject.Named; |
38 | | -import jakarta.persistence.EntityManager; |
39 | | -import jakarta.persistence.NoResultException; |
40 | | -import jakarta.persistence.PersistenceContext; |
41 | | -import jakarta.persistence.Query; |
42 | | -import jakarta.persistence.TypedQuery; |
| 41 | +import jakarta.persistence.*; |
| 42 | +import jakarta.persistence.criteria.*; |
43 | 43 |
|
44 | 44 | /** |
45 | 45 | * |
@@ -376,6 +376,133 @@ public FileMetadata findFileMetadataByDatasetVersionIdAndDataFileId(Long dataset |
376 | 376 | } |
377 | 377 | } |
378 | 378 |
|
| 379 | + /** |
| 380 | + * Finds the complete history of a file's presence across all dataset versions. |
| 381 | + * <p> |
| 382 | + * This method returns a {@link VersionedFileMetadata} entry for every version |
| 383 | + * of the specified dataset. If a version does not contain the file, the |
| 384 | + * {@code fileMetadata} field in the corresponding DTO will be {@code null}. |
| 385 | + * It correctly handles file replacements by searching for all files sharing the |
| 386 | + * same {@code rootDataFileId}. |
| 387 | + * |
| 388 | + * @param datasetId The ID of the parent dataset. |
| 389 | + * @param dataFile The DataFile entity to find the history for. |
| 390 | + * @param canViewUnpublishedVersions A boolean indicating if the user has permission to view non-released versions. |
| 391 | + * @param limit (Optional) The maximum number of results to return. |
| 392 | + * @param offset (Optional) The starting point of the result list. |
| 393 | + * @return A chronologically sorted, paginated list of the file's version history, including versions where the file is absent. |
| 394 | + */ |
| 395 | + public List<VersionedFileMetadata> findFileMetadataHistory(Long datasetId, |
| 396 | + DataFile dataFile, |
| 397 | + boolean canViewUnpublishedVersions, |
| 398 | + Integer limit, |
| 399 | + Integer offset) { |
| 400 | + if (dataFile == null) { |
| 401 | + return Collections.emptyList(); |
| 402 | + } |
| 403 | + |
| 404 | + // Query 1: Get the paginated list of relevant DatasetVersions |
| 405 | + CriteriaBuilder cb = em.getCriteriaBuilder(); |
| 406 | + CriteriaQuery<DatasetVersion> versionQuery = cb.createQuery(DatasetVersion.class); |
| 407 | + Root<DatasetVersion> versionRoot = versionQuery.from(DatasetVersion.class); |
| 408 | + |
| 409 | + List<Predicate> versionPredicates = new ArrayList<>(); |
| 410 | + versionPredicates.add(cb.equal(versionRoot.join("dataset").get("id"), datasetId)); |
| 411 | + if (!canViewUnpublishedVersions) { |
| 412 | + versionPredicates.add(versionRoot.get("versionState").in( |
| 413 | + VersionState.RELEASED, VersionState.DEACCESSIONED)); |
| 414 | + } |
| 415 | + versionQuery.where(versionPredicates.toArray(new Predicate[0])); |
| 416 | + versionQuery.orderBy( |
| 417 | + cb.desc(versionRoot.get("versionNumber")), |
| 418 | + cb.desc(versionRoot.get("minorVersionNumber")) |
| 419 | + ); |
| 420 | + |
| 421 | + TypedQuery<DatasetVersion> typedVersionQuery = em.createQuery(versionQuery); |
| 422 | + if (limit != null) { |
| 423 | + typedVersionQuery.setMaxResults(limit); |
| 424 | + } |
| 425 | + if (offset != null) { |
| 426 | + typedVersionQuery.setFirstResult(offset); |
| 427 | + } |
| 428 | + List<DatasetVersion> datasetVersions = typedVersionQuery.getResultList(); |
| 429 | + |
| 430 | + if (datasetVersions.isEmpty()) { |
| 431 | + return Collections.emptyList(); |
| 432 | + } |
| 433 | + |
| 434 | + // Query 2: Get all FileMetadata for this file's history in this dataset |
| 435 | + CriteriaQuery<FileMetadata> fmQuery = cb.createQuery(FileMetadata.class); |
| 436 | + Root<FileMetadata> fmRoot = fmQuery.from(FileMetadata.class); |
| 437 | + |
| 438 | + List<Predicate> fmPredicates = new ArrayList<>(); |
| 439 | + fmPredicates.add(cb.equal(fmRoot.get("datasetVersion").get("dataset").get("id"), datasetId)); |
| 440 | + |
| 441 | + // Find the file by its entire lineage |
| 442 | + if (dataFile.getRootDataFileId() < 0) { |
| 443 | + fmPredicates.add(cb.equal(fmRoot.get("dataFile").get("id"), dataFile.getId())); |
| 444 | + } else { |
| 445 | + fmPredicates.add(cb.equal(fmRoot.get("dataFile").get("rootDataFileId"), dataFile.getRootDataFileId())); |
| 446 | + } |
| 447 | + fmQuery.where(fmPredicates.toArray(new Predicate[0])); |
| 448 | + |
| 449 | + List<FileMetadata> fileHistory = em.createQuery(fmQuery).getResultList(); |
| 450 | + |
| 451 | + // Combine results |
| 452 | + Map<Long, FileMetadata> fmMap = fileHistory.stream() |
| 453 | + .collect(Collectors.toMap( |
| 454 | + fm -> fm.getDatasetVersion().getId(), |
| 455 | + Function.identity() |
| 456 | + )); |
| 457 | + |
| 458 | + // Create the final list, looking up the FileMetadata for each version |
| 459 | + return datasetVersions.stream() |
| 460 | + .map(version -> new VersionedFileMetadata( |
| 461 | + version, |
| 462 | + fmMap.get(version.getId()) // This will be null if no entry exists for that version ID |
| 463 | + )) |
| 464 | + .collect(Collectors.toList()); |
| 465 | + } |
| 466 | + |
| 467 | + /** |
| 468 | + * Finds the FileMetadata for a given file in the version immediately preceding a specified version. |
| 469 | + * |
| 470 | + * @param fileMetadata The FileMetadata instance from the current version, used to identify the file's lineage. |
| 471 | + * @return The FileMetadata from the immediately prior version, or {@code null} if this is the first version of the file. |
| 472 | + */ |
| 473 | + public FileMetadata getPreviousFileMetadata(FileMetadata fileMetadata) { |
| 474 | + if (fileMetadata == null || fileMetadata.getDataFile() == null) { |
| 475 | + return null; |
| 476 | + } |
| 477 | + |
| 478 | + // 1. Get the ID of the file that was replaced. |
| 479 | + Long previousId = fileMetadata.getDataFile().getPreviousDataFileId(); |
| 480 | + |
| 481 | + // If there's no previous ID, this is the first version of the file. |
| 482 | + if (previousId == null) { |
| 483 | + return null; |
| 484 | + } |
| 485 | + |
| 486 | + CriteriaBuilder cb = em.getCriteriaBuilder(); |
| 487 | + CriteriaQuery<FileMetadata> cq = cb.createQuery(FileMetadata.class); |
| 488 | + Root<FileMetadata> fileMetadataRoot = cq.from(FileMetadata.class); |
| 489 | + |
| 490 | + // 2. Join FileMetadata to DataFile to access the ID. |
| 491 | + Join<FileMetadata, DataFile> dataFileJoin = fileMetadataRoot.join("dataFile"); |
| 492 | + |
| 493 | + // 3. Find the FileMetadata whose DataFile ID matches the previousId. |
| 494 | + cq.where(cb.equal(dataFileJoin.get("id"), previousId)); |
| 495 | + |
| 496 | + // --- Execution --- |
| 497 | + TypedQuery<FileMetadata> query = em.createQuery(cq); |
| 498 | + try { |
| 499 | + return query.getSingleResult(); |
| 500 | + } catch (NoResultException e) { |
| 501 | + // If no result is found, return null. |
| 502 | + return null; |
| 503 | + } |
| 504 | + } |
| 505 | + |
379 | 506 | public FileMetadata findMostRecentVersionFileIsIn(DataFile file) { |
380 | 507 | if (file == null) { |
381 | 508 | return null; |
|
0 commit comments