385 lines
18 KiB
Java
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;
|
|
}
|
|
}
|