VisioHandoffProfile.java

package com.taxonomy.export;

import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.taxonomy.visio.VisioDocument;
import com.taxonomy.visio.VisioLoss;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;

/** Executable metadata/loss contract shared by the OPC package and downloadable handoff bundle. */
public final class VisioHandoffProfile {
    public static final String ID = "visio-2012-opc-supported-subset-v2";
    static final int MAX_XML_BYTES = 32 * 1024 * 1024;
    private static final JsonMapper JSON = JsonMapper.builder()
            .enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS).build();
    private static final String PROFILE = loadProfile();

    private VisioHandoffProfile() { }

    public static String profileJson() { return PROFILE; }

    public static String manifest(VisioDocument document) {
        List<Object> pages = new ArrayList<>();
        List<VisioLoss> losses = new ArrayList<>(document.getLosses());
        for (var page : document.getPages()) {
            List<Object> shapes = new ArrayList<>();
            for (var shape : page.getShapes()) {
                shapes.add(Map.of("shapeId", shape.getId(), "label", shape.getText(), "properties", shape.getProperties()));
                if (!shape.getProperties().containsKey("taxonomy.id")) losses.add(new VisioLoss("shape", page.getId() + ":" + shape.getId(),
                        "semanticIdentity", "OMITTED", "Direct Visio model contains no original Taxonomy identity."));
            }
            List<Object> relationships = new ArrayList<>();
            long connectorId = page.getShapes().stream().mapToLong(s -> Long.parseLong(s.getId())).max().orElse(0);
            for (var connector : page.getConnects()) {
                relationships.add(Map.of("shapeId", Long.toString(++connectorId), "sourceShapeId", connector.getFromShape(),
                        "targetShapeId", connector.getToShape(), "label", connector.getRelationType(), "properties", connector.getProperties()));
                if (!connector.getProperties().containsKey("taxonomy.id")) losses.add(new VisioLoss("connector", page.getId() + ":" + connectorId,
                        "semanticIdentity", "OMITTED", "Direct Visio model contains no original Taxonomy relationship identity."));
            }
            pages.add(Map.of("pageId", page.getId(), "name", page.getName(), "shapes", shapes, "relationships", relationships));
        }
        losses.sort(Comparator.comparing(VisioLoss::scope).thenComparing(VisioLoss::id)
                .thenComparing(VisioLoss::field).thenComparing(VisioLoss::kind).thenComparing(VisioLoss::rationale));
        return json(Map.of("schemaVersion", 1, "exportProfile", ID,
                "profileSha256", sha256(PROFILE.getBytes(StandardCharsets.UTF_8)),
                "authority", document.getProperties(), "pages", pages, "losses", losses,
                "certification", "EXPERIMENTAL_NOT_MICROSOFT_VISIO_CERTIFIED"));
    }

    static String bundleManifest(byte[] vsdx, String handoff) {
        try {
            return json(Map.of("schemaVersion", 1, "artifact", Map.of("file", "diagram.vsdx", "sha256", sha256(vsdx)),
                    "mappingProfile", Map.of("file", "mapping-profile.json", "sha256", sha256(PROFILE.getBytes(StandardCharsets.UTF_8))),
                    "handoff", JSON.readTree(handoff)));
        } catch (IOException e) {
            throw new IllegalStateException("Could not construct Visio manifest", e);
        }
    }

    public static String json(Object value) {
        try { return JSON.writeValueAsString(value); }
        catch (IOException e) { throw new IllegalArgumentException("Could not serialize Visio metadata", e); }
    }

    public static String sha256(byte[] value) {
        try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(value)); }
        catch (NoSuchAlgorithmException e) { throw new IllegalStateException(e); }
    }

    private static String loadProfile() {
        try (var input = VisioHandoffProfile.class.getResourceAsStream("/visio/handoff-profile-v2.json")) {
            if (input == null) throw new IllegalStateException("Missing Visio profile");
            String profile = new String(input.readAllBytes(), StandardCharsets.UTF_8);
            if (!ID.equals(JSON.readTree(profile).path("profileVersion").asText())) throw new IllegalStateException("Visio profile version mismatch");
            return profile;
        } catch (IOException e) { throw new IllegalStateException("Could not load Visio profile", e); }
    }
}