ArchiMateIds.java

package com.taxonomy.archimate.exchange;

import java.nio.ByteBuffer;
import java.io.ByteArrayOutputStream;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.HexFormat;

/** Versioned, domain-separated XML identities, independent of names and layout. */
public final class ArchiMateIds {
    private ArchiMateIds() { }

    public static String id(String kind, String... identities) {
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        for (String part : identities) {
            requireIdentity(part);
            byte[] bytes = part.getBytes(StandardCharsets.UTF_8);
            output.writeBytes(ByteBuffer.allocate(4).putInt(bytes.length).array());
            output.writeBytes(bytes);
        }
        if (!kind.matches("[a-z]+")) throw new IllegalArgumentException("Invalid ID domain");
        return "id-" + kind + "-" + HexFormat.of().formatHex(output.toByteArray());
    }

    /** Decode only this profile's injective ID encoding; arbitrary tool IDs are not guessed. */
    public static List<String> decode(String kind, String xmlId) {
        String prefix = "id-" + kind + "-";
        if (!xmlId.startsWith(prefix)) throw new IllegalArgumentException("Unexpected XML identity profile");
        try {
            ByteBuffer input = ByteBuffer.wrap(HexFormat.of().parseHex(xmlId.substring(prefix.length())));
            List<String> result = new ArrayList<>();
            while (input.hasRemaining()) {
                int length = input.getInt();
                if (length < 1 || length > 16384 || length > input.remaining()) {
                    throw new IllegalArgumentException("Invalid encoded XML identity length");
                }
                ByteBuffer bytes = input.slice(input.position(), length);
                String value = StandardCharsets.UTF_8.newDecoder()
                        .onMalformedInput(CodingErrorAction.REPORT).decode(bytes).toString();
                requireIdentity(value);
                result.add(value);
                input.position(input.position() + length);
            }
            if (result.isEmpty()) throw new IllegalArgumentException("Empty XML identity");
            return List.copyOf(result);
        } catch (java.nio.charset.CharacterCodingException | java.nio.BufferUnderflowException exception) {
            throw new IllegalArgumentException("Invalid encoded XML identity", exception);
        }
    }

    public static void requireIdentity(String value) {
        if (value == null || value.isBlank() || value.length() > 4096) {
            throw new IllegalArgumentException("Missing or oversized architecture identity");
        }
        requireXmlText(value);
    }

    public static String requireXmlText(String value) {
        if (value == null) return "";
        value.codePoints().forEach(code -> {
            if (!(code == 9 || code == 10 || code == 13
                    || code >= 0x20 && code <= 0xD7FF
                    || code >= 0xE000 && code <= 0xFFFD
                    || code >= 0x10000 && code <= 0x10FFFF)) {
                throw new IllegalArgumentException("Invalid XML 1.0 character U+" + Integer.toHexString(code));
            }
        });
        return value;
    }
}