package p.packer.services; import p.packer.PackerOperationStatus; import p.packer.PackerProjectContext; import p.packer.assets.AssetFamilyCatalog; import p.packer.assets.AssetReference; import p.packer.assets.OutputCodecCatalog; import p.packer.assets.PackerAssetState; import p.packer.assets.PackerBuildParticipation; import p.packer.diagnostics.PackerDiagnosticCategory; import p.packer.diagnostics.PackerDiagnosticSeverity; import p.packer.messages.GetAssetDetailsRequest; import p.packer.messages.GetAssetDetailsResult; import p.packer.models.PackerAssetDeclaration; import p.packer.models.PackerAssetDeclarationParseResult; import p.packer.models.PackerAssetDetails; import p.packer.models.PackerAssetIdentity; import p.packer.models.PackerAssetSummary; import p.packer.models.PackerDiagnostic; import p.packer.models.PackerRegistryEntry; import p.packer.models.PackerRuntimeAsset; import p.packer.models.PackerRuntimeSnapshot; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; public final class PackerAssetDetailsService { private final PackerRuntimeRegistry runtimeRegistry; public PackerAssetDetailsService(PackerRuntimeRegistry runtimeRegistry) { this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); } public GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request) { final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); final ResolvedAssetReference resolved = resolveReference(project, snapshot, request.assetReference()); final List diagnostics = new ArrayList<>(resolved.diagnostics()); if (resolved.runtimeAsset().isEmpty()) { diagnostics.add(new PackerDiagnostic( PackerDiagnosticSeverity.ERROR, PackerDiagnosticCategory.STRUCTURAL, "asset.json was not found for the requested asset root.", resolved.assetRoot().resolve("asset.json"), true)); return failureResult(project, request.assetReference(), resolved, diagnostics); } final PackerRuntimeAsset runtimeAsset = resolved.runtimeAsset().get(); final Path manifestPath = runtimeAsset.manifestPath(); final PackerAssetDeclarationParseResult parsed = runtimeAsset.parsedDeclaration(); diagnostics.addAll(parsed.diagnostics()); if (!parsed.valid()) { return failureResult(project, request.assetReference(), resolved, diagnostics); } final PackerAssetDeclaration declaration = parsed.declaration(); diagnostics.addAll(identityMismatchDiagnostics(resolved.registryEntry(), declaration, manifestPath)); final PackerOutputContractCatalog.OutputContractDefinition outputContract = PackerOutputContractCatalog.definitionFor( declaration.outputFormat(), declaration.outputCodec()); final PackerAssetState state = resolved.registryEntry().isPresent() ? PackerAssetState.REGISTERED : PackerAssetState.UNREGISTERED; final PackerAssetSummary summary = new PackerAssetSummary( canonicalReference(project, resolved.assetRoot(), resolved.registryEntry()), new PackerAssetIdentity( resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null), resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(declaration.assetUuid()), declaration.name(), resolved.assetRoot()), state, resolved.registryEntry().map(entry -> entry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED), declaration.assetFamily(), declaration.preloadEnabled(), !diagnostics.isEmpty()); final PackerAssetDetails details = new PackerAssetDetails( summary, declaration.outputFormat(), declaration.outputCodec(), outputContract.availableCodecs(), outputContract.codecConfigurationFieldsByCodec(), resolveInputs(resolved.assetRoot(), declaration.inputsByRole()), diagnostics); return new GetAssetDetailsResult( diagnostics.stream().anyMatch(PackerDiagnostic::blocking) ? PackerOperationStatus.PARTIAL : PackerOperationStatus.SUCCESS, "Asset details resolved from runtime snapshot.", PackerReadMessageMapper.toAssetDetailsDTO(details), PackerReadMessageMapper.toDiagnosticDTOs(diagnostics)); } private GetAssetDetailsResult failureResult( PackerProjectContext project, AssetReference requestedReference, ResolvedAssetReference resolved, List diagnostics) { final PackerAssetState state = resolved.registryEntry().isPresent() ? PackerAssetState.REGISTERED : PackerAssetState.UNREGISTERED; final PackerAssetSummary summary = new PackerAssetSummary( canonicalReferenceOrRequested(project, requestedReference, resolved), new PackerAssetIdentity( resolved.registryEntry().map(PackerRegistryEntry::assetId).orElse(null), resolved.registryEntry().map(PackerRegistryEntry::assetUuid).orElse(null), resolved.assetRoot().getFileName().toString(), resolved.assetRoot()), state, resolved.registryEntry().map(entry -> entry.includedInBuild() ? PackerBuildParticipation.INCLUDED : PackerBuildParticipation.EXCLUDED).orElse(PackerBuildParticipation.EXCLUDED), AssetFamilyCatalog.UNKNOWN, false, true); final PackerAssetDetails details = new PackerAssetDetails( summary, "unknown", OutputCodecCatalog.UNKNOWN, List.of(OutputCodecCatalog.NONE), Map.of(OutputCodecCatalog.NONE, List.of()), Map.of(), diagnostics); return new GetAssetDetailsResult( PackerOperationStatus.FAILED, "Asset declaration is invalid or unreadable.", PackerReadMessageMapper.toAssetDetailsDTO(details), PackerReadMessageMapper.toDiagnosticDTOs(diagnostics)); } private Map> resolveInputs(Path assetRoot, Map> inputsByRole) { final Map> resolved = new LinkedHashMap<>(); inputsByRole.forEach((role, inputs) -> resolved.put( role, inputs.stream().map(input -> assetRoot.resolve(input).toAbsolutePath().normalize()).toList())); return Map.copyOf(resolved); } private List identityMismatchDiagnostics( Optional registryEntry, PackerAssetDeclaration declaration, Path manifestPath) { if (registryEntry.isEmpty() || registryEntry.get().assetUuid().equals(declaration.assetUuid())) { return List.of(); } return List.of(new PackerDiagnostic( PackerDiagnosticSeverity.ERROR, PackerDiagnosticCategory.STRUCTURAL, "Field 'asset_uuid' does not match the registered asset identity.", manifestPath, true)); } private ResolvedAssetReference resolveReference(PackerProjectContext project, PackerRuntimeSnapshot snapshot, AssetReference assetReference) { final PackerRegistryLookup lookup = new PackerRegistryLookup(); final String reference = Objects.requireNonNull(assetReference, "assetReference").rawValue(); final Optional byId = parseAssetId(reference).flatMap(assetId -> lookup.findByAssetId(snapshot.registry(), assetId)); if (byId.isPresent()) { final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byId.get().root()); return new ResolvedAssetReference(assetRoot, byId, findRuntimeAsset(snapshot, assetRoot), List.of()); } final Optional byUuid = lookup.findByAssetUuid(snapshot.registry(), reference); if (byUuid.isPresent()) { final Path assetRoot = PackerWorkspacePaths.assetRoot(project, byUuid.get().root()); return new ResolvedAssetReference(assetRoot, byUuid, findRuntimeAsset(snapshot, assetRoot), List.of()); } final Path candidateRoot = PackerWorkspacePaths.assetRoot(project, reference); final Optional runtimeAsset = findRuntimeAsset(snapshot, candidateRoot); if (runtimeAsset.isPresent()) { return new ResolvedAssetReference(candidateRoot, lookup.findByRoot(project, snapshot.registry(), candidateRoot), runtimeAsset, List.of()); } final Optional registryEntry = lookup.findByRoot(project, snapshot.registry(), candidateRoot); if (registryEntry.isPresent()) { return new ResolvedAssetReference(candidateRoot, registryEntry, Optional.empty(), List.of()); } return new ResolvedAssetReference( candidateRoot, Optional.empty(), Optional.empty(), List.of(new PackerDiagnostic( PackerDiagnosticSeverity.ERROR, PackerDiagnosticCategory.STRUCTURAL, "Requested asset reference could not be resolved.", candidateRoot, true))); } private AssetReference canonicalReference( PackerProjectContext project, Path assetRoot, Optional registryEntry) { if (registryEntry.isPresent()) { return AssetReference.forAssetId(registryEntry.get().assetId()); } return AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project) .relativize(assetRoot.toAbsolutePath().normalize()) .toString() .replace('\\', '/')); } private AssetReference canonicalReferenceOrRequested( PackerProjectContext project, AssetReference requestedReference, ResolvedAssetReference resolved) { if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) { return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry()); } return requestedReference; } private Optional parseAssetId(String reference) { try { return Optional.of(Integer.parseInt(reference.trim())); } catch (NumberFormatException ignored) { return Optional.empty(); } } private Optional findRuntimeAsset(PackerRuntimeSnapshot snapshot, Path assetRoot) { final Path normalizedRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); return snapshot.assets().stream() .filter(candidate -> candidate.assetRoot().equals(normalizedRoot)) .findFirst(); } private record ResolvedAssetReference( Path assetRoot, Optional registryEntry, Optional runtimeAsset, List diagnostics) { private ResolvedAssetReference { assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize(); registryEntry = Objects.requireNonNull(registryEntry, "registryEntry"); runtimeAsset = Objects.requireNonNull(runtimeAsset, "runtimeAsset"); diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics")); } } }