385 lines
18 KiB
Java

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<PackerDiagnostic> 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<PackerDiagnostic> 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<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
walkProjection.buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
final Optional<PackerAssetCacheEntry> assetCacheEntry = runtimeAsset.registryEntry()
.flatMap(entry -> snapshot.cacheState().findAsset(entry.assetId()));
final List<PackerBankCompositionFile> availableFiles = walkProjection.buildCandidateFiles().stream()
.filter(file -> file.diagnostics().stream().noneMatch(PackerDiagnostic::blocking))
.map(this::toBankCompositionFile)
.toList();
final List<PackerBankCompositionFile> 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<Map<String, Object>> resolvePipelinePalettes(PackerAssetDeclaration declaration) {
final JsonNode palettesNode = declaration.outputPipelineMetadata().get("palettes");
if (!(palettesNode instanceof ArrayNode palettesArray)) {
return List.of();
}
final List<Map<String, Object>> palettes = new ArrayList<>();
for (JsonNode paletteNode : palettesArray) {
final Map<String, Object> 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<String, Object> 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<Integer> originalArgb8888 = new ArrayList<>();
for (JsonNode colorNode : originalNode) {
if (!colorNode.canConvertToInt()) {
return Map.of();
}
originalArgb8888.add(colorNode.intValue());
}
final List<Integer> 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<PackerBankCompositionFile> resolveSelectedBankFile(
Path assetRoot,
PackerAssetArtifactSelection artifact,
Map<String, PackerRuntimeWalkFile> walkFilesByPath,
Optional<PackerAssetCacheEntry> assetCacheEntry) {
final String relativePath = artifact.file();
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
if (runtimeWalkFile != null) {
return Optional.of(toSelectedBankCompositionFile(runtimeWalkFile, artifact));
}
final Optional<PackerFileCacheEntry> 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<String, Object> selectedArtifactMetadata(
PackerAssetArtifactSelection artifact,
Map<String, Object> metadata) {
final Map<String, Object> 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<PackerCodecConfigurationField> metadataFields(
List<PackerCodecConfigurationField> definitions,
Map<String, String> outputMetadata) {
final Map<String, String> valuesByKey = new LinkedHashMap<>(outputMetadata);
final List<PackerCodecConfigurationField> 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<PackerDiagnostic> identityMismatchDiagnostics(
final Optional<PackerRegistryEntry> 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<PackerDiagnostic> 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<PackerRegistryEntry> 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;
}
}