package p.packer.services; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import p.packer.PackerWorkspacePaths; import p.packer.messages.*; import p.packer.messages.assets.*; import p.packer.messages.diagnostics.PackerDiagnosticCategory; import p.packer.messages.diagnostics.PackerDiagnosticSeverity; import p.packer.models.*; import p.packer.repositories.PackerRuntimeRegistry; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; public final class PackerAssetDetailsService { private static final ObjectMapper GLYPH_MATERIAL_MAPPER = new ObjectMapper(); private final PackerRuntimeRegistry runtimeRegistry; private final PackerAssetReferenceResolver assetReferenceResolver; public PackerAssetDetailsService( final PackerRuntimeRegistry runtimeRegistry, final PackerAssetReferenceResolver assetReferenceResolver) { this.runtimeRegistry = Objects.requireNonNull(runtimeRegistry, "runtimeRegistry"); this.assetReferenceResolver = Objects.requireNonNull(assetReferenceResolver, "assetReferenceResolver"); } public GetAssetDetailsResult getAssetDetails(final GetAssetDetailsRequest request) { final PackerProjectContext project = Objects.requireNonNull(request, "request").project(); final PackerRuntimeSnapshot snapshot = runtimeRegistry.getOrLoad(project).snapshot(); final PackerResolvedAssetReference resolved = assetReferenceResolver.resolve(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 var runtimeAsset = resolved.runtimeAsset().get(); final var manifestPath = runtimeAsset.manifestPath(); final var parsed = runtimeAsset.parsedDeclaration(); diagnostics.addAll(parsed.diagnostics()); diagnostics.addAll(runtimeAsset.walkDiagnostics()); appendWalkFileDiagnostics(runtimeAsset, diagnostics); diagnostics.addAll(new PackerGlyphMaterialSupport(GLYPH_MATERIAL_MAPPER).validate(runtimeAsset)); if (!parsed.valid()) { return failureResult(project, request.assetReference(), resolved, diagnostics); } final var declaration = parsed.declaration(); diagnostics.addAll(identityMismatchDiagnostics(resolved.registryEntry(), declaration, manifestPath)); final var outputContract = PackerOutputContractCatalog.definitionFor( declaration.outputFormat(), declaration.outputCodec()); final var state = resolved.registryEntry().isPresent() ? PackerAssetState.REGISTERED : PackerAssetState.UNREGISTERED; final var 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() .filter(PackerRegistryEntry::includedInBuild) .map(entry -> PackerBuildParticipation.INCLUDED) .orElse(PackerBuildParticipation.EXCLUDED), declaration.assetFamily(), declaration.preloadEnabled(), !diagnostics.isEmpty()); final var details = new PackerAssetDetails( summary, declaration.outputFormat(), declaration.outputCodec(), outputContract.availableCodecs(), outputContract.codecConfigurationFieldsByCodec(), metadataFields(outputContract.metadataFields(), declaration.outputMetadata()), PackerReadMessageMapper.normalizeMetadata(declaration.outputPipelineMetadata()), resolveBankCompositionDetails(snapshot, runtimeAsset, declaration), resolvePipelinePalettes(declaration), 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( final PackerProjectContext project, final AssetReference requestedReference, final PackerResolvedAssetReference resolved, final List diagnostics) { final var state = resolved.registryEntry().isPresent() ? PackerAssetState.REGISTERED : PackerAssetState.UNREGISTERED; final var 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() .filter(PackerRegistryEntry::includedInBuild) .map(entry -> PackerBuildParticipation.INCLUDED) .orElse(PackerBuildParticipation.EXCLUDED), AssetFamilyCatalog.UNKNOWN, false, true); final var details = new PackerAssetDetails( summary, OutputFormatCatalog.UNKNOWN, OutputCodecCatalog.UNKNOWN, List.of(OutputCodecCatalog.NONE), Map.of(OutputCodecCatalog.NONE, List.of()), List.of(), Map.of(), PackerBankCompositionDetails.EMPTY, List.of(), diagnostics); return new GetAssetDetailsResult( PackerOperationStatus.FAILED, "Asset declaration is invalid or unreadable.", PackerReadMessageMapper.toAssetDetailsDTO(details), PackerReadMessageMapper.toDiagnosticDTOs(diagnostics)); } private PackerBankCompositionDetails resolveBankCompositionDetails( final PackerRuntimeSnapshot snapshot, final PackerRuntimeAsset runtimeAsset, final PackerAssetDeclaration declaration) { final var walkProjection = runtimeAsset.walkProjection(); final Map walkFilesByPath = new LinkedHashMap<>(); walkProjection.buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file)); final Optional assetCacheEntry = runtimeAsset.registryEntry() .flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId())); final List availableFiles = walkProjection.buildCandidateFiles().stream() .filter(file -> file.diagnostics().stream().noneMatch(PackerDiagnostic::blocking)) .map(this::toBankCompositionFile) .toList(); final List selectedFiles = declaration.artifacts().stream() .sorted(Comparator.comparingInt(PackerAssetArtifactSelection::index)) .map(artifact -> resolveSelectedBankFile(runtimeAsset.assetRoot(), artifact, walkFilesByPath, assetCacheEntry)) .flatMap(Optional::stream) .toList(); return new PackerBankCompositionDetails( availableFiles, selectedFiles, walkProjection.measuredBankSizeBytes()); } private List> resolvePipelinePalettes(PackerAssetDeclaration declaration) { final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes"); if (!(palettesNode instanceof ArrayNode palettesArray)) { return List.of(); } final List> palettes = new ArrayList<>(); for (JsonNode paletteNode : palettesArray) { final Map normalized = palettePayloadFromNode(paletteNode); if (!normalized.isEmpty()) { palettes.add(normalized); } } palettes.sort(Comparator.comparingInt(palette -> ((Number) palette.get("index")).intValue())); return List.copyOf(palettes); } private Map palettePayloadFromNode(JsonNode paletteNode) { if (!(paletteNode instanceof ObjectNode paletteObject)) { return Map.of(); } final JsonNode indexNode = paletteObject.path("index"); final JsonNode payloadNode = paletteObject.path("palette"); if (!indexNode.isInt() || indexNode.intValue() < 0 || !(payloadNode instanceof ObjectNode payloadObject)) { return Map.of(); } final JsonNode originalNode = payloadObject.path("originalArgb8888"); final JsonNode convertedNode = payloadObject.path("convertedRgb565"); if (!originalNode.isArray() || !convertedNode.isArray()) { return Map.of(); } final List originalArgb8888 = new ArrayList<>(); for (JsonNode colorNode : originalNode) { if (!colorNode.canConvertToInt()) { return Map.of(); } originalArgb8888.add(colorNode.intValue()); } final List convertedRgb565 = new ArrayList<>(); for (JsonNode colorNode : convertedNode) { if (!colorNode.canConvertToInt()) { return Map.of(); } convertedRgb565.add(colorNode.intValue()); } return Map.of( "index", indexNode.intValue(), "originalArgb8888", List.copyOf(originalArgb8888), "convertedRgb565", List.copyOf(convertedRgb565)); } private Optional resolveSelectedBankFile( Path assetRoot, PackerAssetArtifactSelection artifact, Map walkFilesByPath, Optional assetCacheEntry) { final String relativePath = artifact.file(); final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath); if (runtimeWalkFile != null) { return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact)); } final Optional cachedFile = assetCacheEntry.flatMap(entry -> entry.findFile(relativePath)); if (cachedFile.isPresent()) { return Optional.of(toSelectedBankCompositionFile(cachedFile.get(), artifact)); } final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize(); if (!Files.isRegularFile(filePath)) { return Optional.empty(); } try { return Optional.of(new PackerBankCompositionFile( relativePath, filePath.getFileName().toString(), Files.size(filePath), Files.getLastModifiedTime(filePath).toMillis(), artifact.fingerprint(), selectedArtifactMetadata(artifact, Map.of()))); } catch (Exception exception) { return Optional.empty(); } } private PackerBankCompositionFile toBankCompositionFile(PackerRuntimeWalkFile file) { return new PackerBankCompositionFile( file.relativePath(), Path.of(file.relativePath()).getFileName().toString(), file.size(), file.lastModified(), file.fingerprint(), file.metadata()); } private PackerBankCompositionFile toBankCompositionFile(PackerFileCacheEntry file) { return new PackerBankCompositionFile( file.relativePath(), Path.of(file.relativePath()).getFileName().toString(), file.size(), file.lastModified(), file.fingerprint(), file.metadata()); } private PackerBankCompositionFile toSelectedBankCompositionFile( PackerRuntimeWalkFile file, PackerAssetArtifactSelection artifact) { return new PackerBankCompositionFile( file.relativePath(), Path.of(file.relativePath()).getFileName().toString(), file.size(), file.lastModified(), file.fingerprint(), selectedArtifactMetadata(artifact, file.metadata())); } private PackerBankCompositionFile toSelectedBankCompositionFile( PackerFileCacheEntry file, PackerAssetArtifactSelection artifact) { return new PackerBankCompositionFile( file.relativePath(), Path.of(file.relativePath()).getFileName().toString(), file.size(), file.lastModified(), file.fingerprint(), selectedArtifactMetadata(artifact, file.metadata())); } private Map selectedArtifactMetadata( PackerAssetArtifactSelection artifact, Map metadata) { final Map decorated = new LinkedHashMap<>(metadata); if (artifact.artifactId() != null) { decorated.put("artifactId", artifact.artifactId()); } if (artifact.fingerprint() != null) { decorated.put("selectedFingerprint", artifact.fingerprint()); } return Map.copyOf(decorated); } private List metadataFields( List definitions, Map outputMetadata) { final Map valuesByKey = new LinkedHashMap<>(outputMetadata); final List fields = new ArrayList<>(); for (PackerCodecConfigurationField definition : definitions) { final String value = valuesByKey.getOrDefault(definition.key(), definition.value()); fields.add(new PackerCodecConfigurationField( definition.key(), definition.label(), definition.fieldType(), value, definition.required(), definition.options())); valuesByKey.remove(definition.key()); } valuesByKey.forEach((key, value) -> fields.add(new PackerCodecConfigurationField( key, key, PackerCodecConfigurationFieldType.TEXT, value, false, List.of()))); return List.copyOf(fields); } private List identityMismatchDiagnostics( final Optional registryEntry, final PackerAssetDeclaration declaration, final 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 void appendWalkFileDiagnostics( PackerRuntimeAsset runtimeAsset, List diagnostics) { for (PackerRuntimeWalkFile file : runtimeAsset.walkProjection().buildCandidateFiles()) { for (PackerDiagnostic diagnostic : file.diagnostics()) { if (!diagnostics.contains(diagnostic)) { diagnostics.add(diagnostic); } } } } private AssetReference canonicalReference( final PackerProjectContext project, final Path assetRoot, final Optional registryEntry) { return registryEntry .map(packerRegistryEntry -> AssetReference.forAssetId(packerRegistryEntry.assetId())) .orElseGet(() -> AssetReference.forRelativeAssetRoot(PackerWorkspacePaths.assetsRoot(project) .relativize(assetRoot.toAbsolutePath().normalize()) .toString() .replace('\\', '/'))); } private AssetReference canonicalReferenceOrRequested( final PackerProjectContext project, final AssetReference requestedReference, final PackerResolvedAssetReference resolved) { if (resolved.registryEntry().isPresent() || resolved.runtimeAsset().isPresent()) { return canonicalReference(project, resolved.assetRoot(), resolved.registryEntry()); } return requestedReference; } }