Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/main/java/dev/ayagmar/quarkusforge/api/CatalogData.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ public record CatalogData(
MetadataDto metadata,
List<ExtensionDto> extensions,
CatalogSource source,
MetadataSource metadataSource,
boolean stale,
String detailMessage) {
public CatalogData {
metadata = Objects.requireNonNull(metadata);
source = Objects.requireNonNull(source);
metadataSource = Objects.requireNonNull(metadataSource);
extensions = List.copyOf(Objects.requireNonNull(extensions));
detailMessage = detailMessage == null ? "" : detailMessage.strip();

Expand All @@ -21,10 +23,38 @@ public record CatalogData(
if (source != CatalogSource.CACHE && stale) {
throw new IllegalArgumentException("Only cache-sourced catalog data can be stale");
}
if (source == CatalogSource.CACHE && metadataSource != MetadataSource.CACHE) {
throw new IllegalArgumentException("Cache-sourced catalog data must use cache metadata");
}
if (source == CatalogSource.LIVE && metadataSource == MetadataSource.CACHE) {
throw new IllegalArgumentException("Live catalog data cannot use cache metadata");
}
}

public CatalogData(
MetadataDto metadata,
List<ExtensionDto> extensions,
CatalogSource source,
boolean stale,
String detailMessage) {
this(
metadata,
extensions,
source,
source == CatalogSource.CACHE ? MetadataSource.CACHE : MetadataSource.LIVE,
stale,
detailMessage);
}

/** Returns the source label with a "[stale]" suffix when applicable. */
public String sourceLabel() {
return stale ? source.label() + " [stale]" : source.label();
}

/** Returns the metadata source label with a "[stale]" suffix when applicable. */
public String metadataSourceLabel() {
return metadataSource == MetadataSource.CACHE && stale
? metadataSource.label() + " [stale]"
: metadataSource.label();
}
}
31 changes: 21 additions & 10 deletions src/main/java/dev/ayagmar/quarkusforge/api/CatalogDataService.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,34 @@ private CatalogData toLiveCatalogData(
throw new ApiContractException("Catalog load returned no extensions");
}

CacheWriteOutcome writeOutcome = snapshotCache.write(metadataSelection.metadata(), extensions);
String detailMessage = metadataSelection.detailMessage();
if (!writeOutcome.written()) {
String cacheWriteDetail =
writeOutcome.rejected()
? "Live catalog loaded; cache update skipped (%s)".formatted(writeOutcome.detail())
: "Live catalog loaded; cache update failed (%s)".formatted(writeOutcome.detail());
detailMessage =
detailMessage.isBlank() ? cacheWriteDetail : detailMessage + " | " + cacheWriteDetail;
MetadataSource metadataSource =
metadataSelection.liveMetadata() ? MetadataSource.LIVE : MetadataSource.SNAPSHOT;
if (metadataSelection.liveMetadata()) {
CacheWriteOutcome writeOutcome =
snapshotCache.write(metadataSelection.metadata(), extensions);
if (!writeOutcome.written()) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
String cacheWriteDetail =
writeOutcome.rejected()
? "Live catalog loaded; cache update skipped (%s)".formatted(writeOutcome.detail())
: "Live catalog loaded; cache update failed (%s)".formatted(writeOutcome.detail());
detailMessage =
detailMessage.isBlank() ? cacheWriteDetail : detailMessage + " | " + cacheWriteDetail;
}
}
return new CatalogData(
metadataSelection.metadata(), extensions, CatalogSource.LIVE, false, detailMessage);
metadataSelection.metadata(),
extensions,
CatalogSource.LIVE,
metadataSource,
false,
detailMessage);
}

private CompletableFuture<MetadataSelection> loadMetadataSelection() {
return apiClient
.fetchMetadata()
.thenApply(metadata -> new MetadataSelection(metadata, ""))
.thenApply(metadata -> new MetadataSelection(metadata, true, ""))
.exceptionally(this::fallbackMetadataSelection);
}

Expand All @@ -81,6 +91,7 @@ private MetadataSelection fallbackMetadataSelection(Throwable throwable) {
MetadataDto snapshotMetadata = MetadataSnapshotLoader.loadDefault();
return new MetadataSelection(
snapshotMetadata,
false,
"Live metadata unavailable (%s); using bundled metadata snapshot"
.formatted(ErrorMessageMapper.simpleError(cause)));
} catch (RuntimeException snapshotFailure) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.util.Objects;

record MetadataSelection(MetadataDto metadata, String detailMessage) {
record MetadataSelection(MetadataDto metadata, boolean liveMetadata, String detailMessage) {
MetadataSelection {
metadata = Objects.requireNonNull(metadata);
detailMessage = detailMessage == null ? "" : detailMessage.strip();
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/dev/ayagmar/quarkusforge/api/MetadataSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.ayagmar.quarkusforge.api;

public enum MetadataSource {
LIVE("live"),
SNAPSHOT("bundled snapshot"),
CACHE("cache");

private final String label;

MetadataSource(String label) {
this.label = label;
}

public String label() {
return label;
}
}
13 changes: 1 addition & 12 deletions src/main/java/dev/ayagmar/quarkusforge/api/QuarkusApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
import java.util.function.Supplier;

public final class QuarkusApiClient implements AutoCloseable {
private static final List<String> FALLBACK_BUILD_TOOLS =
List.of("maven", "gradle", "gradle-kotlin-dsl");
private static final Duration MAX_RETRY_DELAY = Duration.ofSeconds(30);

private final ApiTransport transport;
Expand Down Expand Up @@ -100,8 +98,7 @@ public CompletableFuture<MetadataDto> fetchMetadata() {
sendWithRetry(() -> transport.sendStringAsync(openApiRequest), 1)
.thenApply(this::assertSuccessful)
.thenApply(ApiStringResponse::body)
.thenApply(ApiPayloadParser::parseBuildToolsFromOpenApiPayload)
.exceptionally(ignored -> fallbackBuildTools());
.thenApply(ApiPayloadParser::parseBuildToolsFromOpenApiPayload);

return streamsMetadataFuture.thenCombine(buildToolsFuture, QuarkusApiClient::toMetadata);
}
Expand Down Expand Up @@ -138,14 +135,6 @@ private static MetadataDto toMetadata(StreamsMetadata streamsMetadata, List<Stri
javaVersions, buildTools, compatibility, streamsMetadata.platformStreams());
}

private static List<String> fallbackBuildTools() {
try {
return MetadataSnapshotLoader.loadDefault().buildTools();
} catch (RuntimeException ignored) {
return FALLBACK_BUILD_TOOLS;
}
}

public CompletableFuture<Path> downloadProjectZipToFile(
GenerationRequest generationRequest, Path destinationFile) {
URI uri = GenerationQueryBuilder.build(baseUri, generationRequest);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,27 @@ private static CliPrefill mergePrefill(
CliPrefill requestedPrefill, CliPrefill storedPrefill, CliPrefill defaults) {
CliPrefill safeRequestedPrefill =
Objects.requireNonNull(requestedPrefill, "requestedPrefill must not be null");
boolean coordinatesOverridden =
overridesStoredOrDefault(
safeRequestedPrefill.groupId(), storedPrefill, CliPrefill::groupId, defaults)
|| overridesStoredOrDefault(
safeRequestedPrefill.artifactId(), storedPrefill, CliPrefill::artifactId, defaults);
String packageName =
coordinatesOverridden && !hasText(safeRequestedPrefill.packageName())
? defaults.packageName()
: preferRequested(
safeRequestedPrefill.packageName(),
storedPrefill,
CliPrefill::packageName,
defaults);
return new CliPrefill(
preferRequested(
safeRequestedPrefill.groupId(), storedPrefill, CliPrefill::groupId, defaults),
preferRequested(
safeRequestedPrefill.artifactId(), storedPrefill, CliPrefill::artifactId, defaults),
preferRequested(
safeRequestedPrefill.version(), storedPrefill, CliPrefill::version, defaults),
preferRequested(
safeRequestedPrefill.packageName(), storedPrefill, CliPrefill::packageName, defaults),
packageName,
preferRequested(
safeRequestedPrefill.outputDirectory(),
storedPrefill,
Expand Down Expand Up @@ -72,6 +84,16 @@ private static String preferRequested(
return field.read(defaults);
}

private static boolean overridesStoredOrDefault(
String requestedValue, CliPrefill storedPrefill, PrefillField field, CliPrefill defaults) {
if (!hasText(requestedValue)) {
return false;
}
String requested = requestedValue.strip();
String baseline = preferRequested(null, storedPrefill, field, defaults);
return !requested.equals(baseline);
}

private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,8 @@ public CompletableFuture<Path> downloadAndExtract(
}
progressListener.accept(ProgressStep.EXTRACTING_ARCHIVE);
ExtractionResult result =
zipExtractor.extract(archivePath, outputDirectory, overwritePolicy);
if (cancelled.getAsBoolean()) {
throw new CancellationException("Generation cancelled during extraction");
}
zipExtractor.extract(
archivePath, outputDirectory, overwritePolicy, cancelled);
return result.extractedRoot();
},
extractionExecutor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CancellationException;
import java.util.function.BooleanSupplier;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

Expand All @@ -37,9 +39,19 @@ public SafeZipExtractor(ArchiveSafetyPolicy safetyPolicy) {

public ExtractionResult extract(
Path zipFile, Path outputDirectory, OverwritePolicy overwritePolicy) {
return extract(zipFile, outputDirectory, overwritePolicy, () -> false);
}

public ExtractionResult extract(
Path zipFile,
Path outputDirectory,
OverwritePolicy overwritePolicy,
BooleanSupplier cancelled) {
Objects.requireNonNull(zipFile);
Objects.requireNonNull(outputDirectory);
Objects.requireNonNull(overwritePolicy);
Objects.requireNonNull(cancelled);
Comment on lines +45 to +53

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fail fast when cancelled is already set.

ProjectArchiveService.java, Lines 111-114 can call this overload after the EXTRACTING_ARCHIVE callback flips the flag, but the first actual check here isn't until Line 160. In that case you still parse the central directory and may create parent/staging directories before aborting.

💡 Minimal fix
   Objects.requireNonNull(zipFile);
   Objects.requireNonNull(outputDirectory);
   Objects.requireNonNull(overwritePolicy);
   Objects.requireNonNull(cancelled);
+  throwIfCancelled(cancelled);

   Map<String, ZipEntryMetadata> metadataByEntry = ZipCentralDirectoryReader.read(zipFile);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/ayagmar/quarkusforge/archive/SafeZipExtractor.java` around
lines 45 - 53, The extract(...) method in SafeZipExtractor should fail fast when
the provided BooleanSupplier cancelled is already set: after the existing
Objects.requireNonNull checks (inside the extract method) call
cancelled.getAsBoolean() and immediately return an appropriate cancelled
ExtractionResult (e.g., ExtractionResult.CANCELLED or a new cancelled result)
instead of proceeding to parse the central directory or create directories;
update any code paths that assume work has started to use this early-return so
callers like ProjectArchiveService don't trigger work after EXTRACTING_ARCHIVE
flips the flag.

throwIfCancelled(cancelled);

Map<String, ZipEntryMetadata> metadataByEntry = ZipCentralDirectoryReader.read(zipFile);
validateEntryMetadata(metadataByEntry);
Expand All @@ -65,7 +77,11 @@ public ExtractionResult extract(
Path stagingContent = stagingRoot.resolve("content");
try {
Files.createDirectories(stagingContent);
ExtractionResult stagedResult = extractIntoStaging(zipFile, stagingContent, metadataByEntry);
ExtractionResult stagedResult =
extractIntoStaging(zipFile, stagingContent, metadataByEntry, cancelled);
if (cancelled.getAsBoolean()) {
throw new CancellationException("Generation cancelled during extraction");
}
Path extractedRoot = stagedResult.extractedRoot();
applyOverwritePolicy(extractedRoot, absoluteTarget, overwritePolicy);
return new ExtractionResult(
Expand Down Expand Up @@ -129,7 +145,10 @@ private void validateEntryMetadata(Map<String, ZipEntryMetadata> metadataByEntry
}

private ExtractionResult extractIntoStaging(
Path zipFile, Path stagingContent, Map<String, ZipEntryMetadata> metadataByEntry)
Path zipFile,
Path stagingContent,
Map<String, ZipEntryMetadata> metadataByEntry,
BooleanSupplier cancelled)
throws IOException {
Set<String> seenEntries = new LinkedHashSet<>();
Set<String> topLevelSegments = new LinkedHashSet<>();
Expand All @@ -139,6 +158,7 @@ private ExtractionResult extractIntoStaging(
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(zipFile))) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
throwIfCancelled(cancelled);
String normalizedName = normalizeEntryName(zipEntry.getName());
ZipEntryMetadata metadata = metadataByEntry.get(normalizedName);
if (metadata == null) {
Expand Down Expand Up @@ -176,7 +196,8 @@ private ExtractionResult extractIntoStaging(
destination,
normalizedName,
metadata.uncompressedSize(),
extractedBytes);
extractedBytes,
cancelled);
long copied = copyResult.entryBytes();
extractedBytes = copyResult.totalExtractedBytes();

Expand Down Expand Up @@ -255,7 +276,8 @@ private CopyResult copyEntry(
Path destination,
String entryName,
long expectedUncompressedSize,
long extractedBytesSoFar)
long extractedBytesSoFar,
BooleanSupplier cancelled)
throws IOException {
long copied = 0L;
long totalExtractedBytes = extractedBytesSoFar;
Expand All @@ -265,6 +287,7 @@ private CopyResult copyEntry(
destination, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
int read;
while ((read = inputStream.read(buffer)) != -1) {
throwIfCancelled(cancelled);
long nextCopied = safeAdd(copied, read);
if (nextCopied > expectedUncompressedSize) {
throw new ArchiveException(
Expand All @@ -290,6 +313,12 @@ private CopyResult copyEntry(
return new CopyResult(copied, totalExtractedBytes);
}

private static void throwIfCancelled(BooleanSupplier cancelled) {
if (cancelled.getAsBoolean()) {
throw new CancellationException("Generation cancelled during extraction");
}
}

static String normalizeEntryName(String rawName) {
if (rawName == null || rawName.isBlank()) {
throw new ArchiveException("ZIP entry name must not be blank");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public int run(GenerateCommand command, boolean globalDryRun, boolean verbose) {
if (!validatedState.canSubmit()) {
return validationFailure(
validatedState.validation(),
catalogData.sourceLabel(),
catalogData.metadataSourceLabel(),
catalogData.detailMessage(),
diagnostics);
}
Expand Down Expand Up @@ -157,6 +157,7 @@ private CatalogData loadCatalogData(Duration catalogTimeout, DiagnosticLogger di
diagnostics.info(
"catalog.load.success",
of("source", catalogData.sourceLabel()),
of("metadataSource", catalogData.metadataSourceLabel()),
of("stale", catalogData.stale()),
of("detail", catalogData.detailMessage()));
return catalogData;
Expand Down Expand Up @@ -188,7 +189,7 @@ private static int extensionValidationFailure(
of("errorCount", validationException.errors().size()));
HeadlessOutputPrinter.printValidationErrors(
new ValidationReport(validationException.errors()),
catalogData.sourceLabel(),
catalogData.metadataSourceLabel(),
catalogData.detailMessage());
return ExitCodes.VALIDATION;
}
Expand All @@ -203,8 +204,14 @@ private int handleDryRun(
"generate.dry-run.validated",
of("extensionCount", extensionIds.size()),
of("catalogSource", catalogData.sourceLabel()),
of("metadataSource", catalogData.metadataSourceLabel()),
of("stale", catalogData.stale()));
HeadlessOutputPrinter.printDryRunSummary(request, extensionIds, catalogData.sourceLabel());
HeadlessOutputPrinter.printDryRunSummary(
request,
extensionIds,
catalogData.sourceLabel(),
catalogData.metadataSourceLabel(),
catalogData.detailMessage());
return persistForgefileAndReturn(inputs, request, extensionIds, diagnostics, ExitCodes.OK);
}

Expand Down
Loading
Loading