001package gudusoft.gsqlparser.slpc.identity;
002
003import gudusoft.gsqlparser.slpc.model.SlpcObject;
004
005import java.io.ByteArrayOutputStream;
006import java.math.BigInteger;
007import java.nio.ByteBuffer;
008import java.nio.CharBuffer;
009import java.nio.charset.CharacterCodingException;
010import java.nio.charset.CodingErrorAction;
011import java.nio.charset.StandardCharsets;
012import java.security.MessageDigest;
013import java.security.NoSuchAlgorithmException;
014import java.util.ArrayList;
015import java.util.Collections;
016import java.util.Comparator;
017import java.util.HashMap;
018import java.util.LinkedHashMap;
019import java.util.List;
020import java.util.Map;
021
022/** Normative SLPC identity/fingerprint V1 implementation (node fingerprint V2). */
023public final class SlpcIdentity {
024
025    private SlpcIdentity() {
026    }
027
028    public static byte[] u64(long value) {
029        if (value < 0) {
030            throw new IllegalArgumentException("negative framed integer: " + value);
031        }
032        return ByteBuffer.allocate(8).putLong(value).array();
033    }
034
035    public static byte[] string(String value) {
036        if (value == null) {
037            return new byte[] {0};
038        }
039        byte[] raw = strictUtf8(value);
040        return concat(new byte[] {1}, u64(raw.length), raw);
041    }
042
043    private static byte[] strictUtf8(String value) {
044        try {
045            ByteBuffer encoded = StandardCharsets.UTF_8.newEncoder()
046                    .onMalformedInput(CodingErrorAction.REPORT)
047                    .onUnmappableCharacter(CodingErrorAction.REPORT)
048                    .encode(CharBuffer.wrap(value));
049            byte[] result = new byte[encoded.remaining()];
050            encoded.get(result);
051            return result;
052        } catch (CharacterCodingException invalid) {
053            throw new IllegalArgumentException("SLPC_MALFORMED_IDENTIFIER: string is not encodable as strict UTF-8", invalid);
054        }
055    }
056
057    public static byte[] integer(long value) {
058        return concat(new byte[] {2}, u64(value));
059    }
060
061    public static byte[] framedList(List<byte[]> values) {
062        ByteArrayOutputStream output = new ByteArrayOutputStream();
063        write(output, new byte[] {3});
064        write(output, u64(values.size()));
065        for (byte[] value : values) {
066            write(output, value);
067        }
068        return output.toByteArray();
069    }
070
071    public static String artifactId(SlpcObject artifact) {
072        SlpcObject contentHash = artifact.object("contentHash");
073        return digest("slart1-", "SLPC-ARTIFACT-ID-V1", concat(
074                string(requiredString(artifact, "sourceKind")),
075                string(requiredString(artifact, "logicalLocator")),
076                string(requiredString(artifact, "sqlDialect")),
077                string(contentHash == null ? null : contentHash.string("algorithm")),
078                string(contentHash == null ? null : contentHash.string("value"))));
079    }
080
081    public static String statementId(SlpcObject statement) {
082        SlpcObject fingerprint = statement.object("queryFingerprint");
083        return digest("slstmt1-", "SLPC-STATEMENT-ID-V1", concat(
084                string(requiredString(statement, "artifactId")),
085                integer(requiredLong(statement, "statementIndex")),
086                string(fingerprint == null ? null : fingerprint.string("algorithm")),
087                string(fingerprint == null ? null : fingerprint.string("value"))));
088    }
089
090    public static String endpointId(SlpcObject endpoint) {
091        return digest("sle1-", "SLPC-ENDPOINT-ID-V1", concat(
092                string(requiredString(endpoint, "endpointLevel")),
093                string(requiredString(endpoint, "objectKind")),
094                endpointIdentityBytes(requiredObject(endpoint, "identity")),
095                endpointScopeBytes(endpoint)));
096    }
097
098    public static String candidateEndpointKey(SlpcObject endpoint) {
099        String canonical = endpoint.string("canonicalEndpointId");
100        if (canonical != null && !canonical.isEmpty()) {
101            return canonical;
102        }
103        List<byte[]> display = new ArrayList<byte[]>();
104        for (Object item : requiredArray(endpoint, "displaySegments")) {
105            SlpcObject segment = asObject(item);
106            display.add(concat(
107                    string(requiredString(segment, "kind")),
108                    string(requiredString(segment, "display")),
109                    string(requiredString(segment, "inputForm")),
110                    string(requiredString(segment, "provenance"))));
111        }
112        return digest("slcefp1-", "SLPC-CANDIDATE-ENDPOINT-FP-V1", concat(
113                string(requiredString(endpoint, "endpointLevel")),
114                string(requiredString(endpoint, "objectKind")),
115                framedList(display),
116                endpointIdentityBytes(requiredObject(endpoint, "identity")),
117                endpointScopeBytes(endpoint)));
118    }
119
120    public static String factId(SlpcObject fact, Map<String, SlpcObject> endpoints) {
121        String sourceRef = fact.string("sourceEndpointRef");
122        String sourceId = sourceRef == null ? null : requiredEndpoint(endpoints, sourceRef).string("canonicalEndpointId");
123        String targetId = requiredString(requiredEndpoint(endpoints, requiredString(fact, "targetEndpointRef")), "canonicalEndpointId");
124        return digest("slf1-", "SLPC-FACT-ID-V1", concat(
125                string(sourceId), string(targetId), string(requiredString(fact, "axis")),
126                string(requiredString(fact, "role")), string(requiredString(fact, "operationKind")),
127                string(requiredString(fact, "operationInputKind"))));
128    }
129
130    public static String factCandidateFingerprint(SlpcObject fact, Map<String, SlpcObject> endpoints) {
131        String sourceRef = fact.string("sourceEndpointRef");
132        String sourceKey = sourceRef == null ? null : candidateEndpointKey(requiredEndpoint(endpoints, sourceRef));
133        String targetKey = candidateEndpointKey(requiredEndpoint(endpoints, requiredString(fact, "targetEndpointRef")));
134        return digest("slfcfp1-", "SLPC-FACT-CANDIDATE-FP-V1", concat(
135                string(sourceKey), string(targetKey), string(requiredString(fact, "axis")),
136                string(requiredString(fact, "role")), string(requiredString(fact, "operationKind")),
137                string(requiredString(fact, "operationInputKind"))));
138    }
139
140    public static String datasetOperationInfluenceId(SlpcObject record, Map<String, SlpcObject> endpoints) {
141        return digest("slop1-", "SLPC-DATASET-OP-ID-V1", concat(
142                string(requiredString(requiredEndpoint(endpoints, requiredString(record, "sourceEndpointRef")), "canonicalEndpointId")),
143                string(requiredString(requiredEndpoint(endpoints, requiredString(record, "targetEndpointRef")), "canonicalEndpointId")),
144                string("SORT")));
145    }
146
147    public static String datasetOperationCandidateFingerprint(SlpcObject record, Map<String, SlpcObject> endpoints) {
148        return digest("slopcfp1-", "SLPC-DATASET-OP-CANDIDATE-FP-V1", concat(
149                string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(record, "sourceEndpointRef")))),
150                string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(record, "targetEndpointRef")))),
151                string("SORT")));
152    }
153
154    public static String objectRelationshipId(SlpcObject record, Map<String, SlpcObject> endpoints) {
155        return digest("slor1-", "SLPC-OBJECT-RELATIONSHIP-ID-V1",
156                relationshipSemanticPayload(record, endpoints, false));
157    }
158
159    public static String objectRelationshipCandidateFingerprint(SlpcObject record, Map<String, SlpcObject> endpoints) {
160        return digest("slorcfp1-", "SLPC-OBJECT-RELATIONSHIP-CANDIDATE-FP-V1",
161                relationshipSemanticPayload(record, endpoints, true));
162    }
163
164    public static String statementOperationId(SlpcObject record, Map<String, SlpcObject> endpoints) {
165        Map<String, List<String>> byRole = new HashMap<String, List<String>>();
166        for (Object item : requiredArray(record, "participants")) {
167            SlpcObject participant = asObject(item);
168            String role = requiredString(participant, "role");
169            if (!byRole.containsKey(role)) byRole.put(role, new ArrayList<String>());
170            byRole.get(role).add(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(participant, "endpointRef"))));
171        }
172        List<OperationRole> roles = operationRoles(requiredString(record, "operationKind"));
173        List<byte[]> groups = new ArrayList<byte[]>();
174        for (OperationRole role : roles) {
175            List<String> values = byRole.containsKey(role.name)
176                    ? new ArrayList<String>(byRole.get(role.name)) : new ArrayList<String>();
177            if (role.setLike) {
178                Collections.sort(values, UTF8_STRING_COMPARATOR);
179                ArrayList<String> distinct = new ArrayList<String>();
180                for (String value : values) {
181                    if (distinct.isEmpty() || !distinct.get(distinct.size() - 1).equals(value)) distinct.add(value);
182                }
183                values = distinct;
184            }
185            List<byte[]> framed = new ArrayList<byte[]>();
186            for (String value : values) framed.add(string(value));
187            groups.add(concat(string(role.name), framedList(framed)));
188        }
189        return digest("slso1-", "SLPC-STATEMENT-OPERATION-ID-V1", concat(
190                string(requiredString(record, "statementId")), integer(requiredLong(record, "operationOrdinal")),
191                string(requiredString(record, "operationKind")), string(requiredString(record, "contentSemantics")),
192                framedList(groups)));
193    }
194
195    public static String joinSemanticsId(SlpcObject record, Map<String, SlpcObject> endpoints) {
196        List<byte[]> pairs = new ArrayList<byte[]>();
197        for (Object item : requiredArray(record, "keyPairs")) {
198            SlpcObject pair = asObject(item);
199            pairs.add(concat(integer(requiredLong(pair, "ordinal")),
200                    string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(pair, "leftFieldEndpointRef")))),
201                    string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(pair, "rightFieldEndpointRef")))),
202                    string(requiredString(pair, "operator"))));
203        }
204        SlpcObject predicate = record.object("predicate");
205        SlpcObject fingerprint = predicate == null ? null : predicate.object("fingerprint");
206        return digest("sljoin1-", "SLPC-JOIN-SEMANTICS-ID-V1", concat(
207                string(requiredString(record, "statementId")), integer(requiredLong(record, "joinOrdinal")),
208                string(requiredString(record, "joinType")), string(requiredString(record, "conditionForm")),
209                string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(record, "leftRelationEndpointRef")))),
210                string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(record, "rightRelationEndpointRef")))),
211                string(requiredString(record, "leftSideSemantics")), string(requiredString(record, "rightSideSemantics")),
212                framedList(pairs), string(fingerprint == null ? null : fingerprint.string("algorithm")),
213                string(fingerprint == null ? null : fingerprint.string("value"))));
214    }
215
216    public static String intermediateNodeFingerprint(SlpcObject node, SlpcObject graph,
217            Map<String, SlpcObject> endpoints) {
218        SlpcObject evidence = node.object("nodeEvidence");
219        SlpcObject fingerprint = evidence == null ? null : evidence.object("fingerprint");
220        String parentRef = node.string("parentNodeRef");
221        Long parentOrdinal = null;
222        if (parentRef != null) {
223            for (Object item : requiredArray(graph, "nodes")) {
224                SlpcObject candidate = asObject(item);
225                if (parentRef.equals(candidate.string("nodeRef"))) {
226                    parentOrdinal = requiredLong(candidate, "nodeOrdinal");
227                    break;
228                }
229            }
230            if (parentOrdinal == null) throw new IllegalArgumentException("intermediate node references missing parent: " + parentRef);
231        }
232        String boundaryRef = node.string("boundaryEndpointRef");
233        return digest("slignfp2-", "SLPC-INTERMEDIATE-NODE-FP-V2", concat(
234                string(requiredString(graph, "statementId")), integer(requiredLong(graph, "graphOrdinal")),
235                integer(requiredLong(node, "nodeOrdinal")), string(requiredString(node, "nodeKind")),
236                string(node.string("localName")), parentOrdinal == null ? string(null) : integer(parentOrdinal.longValue()),
237                string(boundaryRef == null ? null : candidateEndpointKey(requiredEndpoint(endpoints, boundaryRef))),
238                string(fingerprint == null ? null : fingerprint.string("algorithm")),
239                string(fingerprint == null ? null : fingerprint.string("value"))));
240    }
241
242    public static String intermediateEdgeFingerprint(SlpcObject edge, Map<String, SlpcObject> endpoints,
243            Map<String, SlpcObject> nodes) {
244        String[] transform = evidenceFingerprint(edge.object("transformation"));
245        String[] predicate = evidenceFingerprint(edge.object("predicate"));
246        return digest("sligefp1-", "SLPC-INTERMEDIATE-EDGE-FP-V1", concat(
247                anchorBytes(requiredObject(edge, "source"), endpoints, nodes),
248                anchorBytes(requiredObject(edge, "target"), endpoints, nodes),
249                string(requiredString(edge, "axis")), string(requiredString(edge, "role")),
250                string(requiredString(edge, "operationKind")), string(requiredString(edge, "operationInputKind")),
251                string(transform[0]), string(transform[1]), string(predicate[0]), string(predicate[1])));
252    }
253
254    public static String intermediatePathFingerprint(SlpcObject path, Map<String, SlpcObject> edges) {
255        List<byte[]> fingerprints = new ArrayList<byte[]>();
256        for (Object item : requiredArray(path, "edgeRefs")) {
257            fingerprints.add(string(requiredString(requiredRecord(edges, (String) item, "edge"), "edgeFingerprint")));
258        }
259        return digest("sligpfp1-", "SLPC-INTERMEDIATE-PATH-FP-V1", framedList(fingerprints));
260    }
261
262    public static String intermediateGraphFingerprint(SlpcObject graph) {
263        List<String> nodes = fieldValues(requiredArray(graph, "nodes"), "nodeFingerprint");
264        List<String> edges = fieldValues(requiredArray(graph, "edges"), "edgeFingerprint");
265        List<String> paths = fieldValues(requiredArray(graph, "paths"), "pathFingerprint");
266        Collections.sort(nodes); Collections.sort(edges); Collections.sort(paths);
267        return digest("sliggfp1-", "SLPC-INTERMEDIATE-GRAPH-FP-V1", concat(
268                string(requiredString(graph, "statementId")), integer(requiredLong(graph, "graphOrdinal")),
269                string(requiredString(graph, "graphKind")), framedStrings(nodes), framedStrings(edges), framedStrings(paths)));
270    }
271
272    public static String interactiveStepFingerprint(SlpcObject step, SlpcObject graph,
273            Map<String, SlpcObject> endpoints, Map<String, SlpcObject> nodes, Map<String, SlpcObject> edges) {
274        String[] evidence = evidenceFingerprint(step.object("expressionEvidence"));
275        List<byte[]> bindings = new ArrayList<byte[]>();
276        for (Object item : requiredArray(step, "inputBindings")) {
277            SlpcObject binding = asObject(item);
278            SlpcObject edge = requiredRecord(edges, requiredString(binding, "edgeRef"), "edge");
279            bindings.add(concat(integer(requiredLong(binding, "ordinal")), string(requiredString(binding, "roleCode")),
280                    string(requiredString(edge, "edgeFingerprint"))));
281        }
282        return digest("sligsfp1-", "SLPC-INTERACTIVE-EXPLAIN-STEP-FP-V1", concat(
283                string(requiredString(graph, "graphFingerprint")), integer(requiredLong(step, "stepOrdinal")),
284                string(requiredString(step, "operationFamily")), string(requiredString(step, "operationCode")),
285                framedList(bindings), anchorBytes(requiredObject(step, "resultAnchor"), endpoints, nodes),
286                string(evidence[0]), string(evidence[1])));
287    }
288
289    public static String occurrenceId(SlpcObject occurrence, Map<String, String> ownerSemanticKeys) {
290        SlpcObject owner = requiredObject(occurrence, "ownerRef");
291        String ownerKey = requiredString(owner, "kind") + "\u0000" + requiredString(owner, "id");
292        String semanticKey = ownerSemanticKeys.get(ownerKey);
293        if (semanticKey == null) throw new IllegalArgumentException("missing occurrence owner semantic key: " + ownerKey);
294        String[] transformation = evidenceFingerprint(occurrence.object("transformation"));
295        String[] predicate = evidenceFingerprint(occurrence.object("predicate"));
296        SlpcObject origin = requiredObject(occurrence, "origin");
297        return digest("slocc1-", "SLPC-OCCURRENCE-ID-V1", concat(
298                string(requiredString(owner, "kind")), string(semanticKey), string(requiredString(occurrence, "artifactId")),
299                string(occurrence.string("statementId")), string(requiredString(occurrence, "evidenceKind")),
300                string(requiredString(origin, "engineKind")), string(requiredString(origin, "adapterName")),
301                string(occurrence.string("procedureCanonicalEndpointId")), string(transformation[0]), string(transformation[1]),
302                string(predicate[0]), string(predicate[1])));
303    }
304
305    public static String literalTextHash(String sqlDialect, String literalKind, String literalText) {
306        return digest("", "SLPC-LITERAL-TEXT-V1", concat(string(sqlDialect), string(literalKind), string(literalText)));
307    }
308
309    public static String redactedLiteralHash(String sqlDialect, String literalKind, long redactionOrdinal) {
310        return digest("", "SLPC-REDACTED-LITERAL-V1", concat(string(sqlDialect), string(literalKind), integer(redactionOrdinal)));
311    }
312
313    public static Map<String, SlpcObject> index(List<Object> records, String key) {
314        LinkedHashMap<String, SlpcObject> result = new LinkedHashMap<String, SlpcObject>();
315        for (Object item : records) {
316            SlpcObject record = asObject(item);
317            String value = requiredString(record, key);
318            if (result.put(value, record) != null) throw new IllegalArgumentException("duplicate " + key + ": " + value);
319        }
320        return result;
321    }
322
323    private static byte[] relationshipSemanticPayload(SlpcObject record, Map<String, SlpcObject> endpoints, boolean candidate) {
324        String kind = requiredString(record, "relationshipKind");
325        List<String> roleOrder = relationshipRoles(kind);
326        Map<String, String> participants = new HashMap<String, String>();
327        for (Object item : requiredArray(record, "participants")) {
328            SlpcObject participant = asObject(item);
329            participants.put(requiredString(participant, "role"), requiredString(participant, "endpointRef"));
330        }
331        List<byte[]> keys = new ArrayList<byte[]>();
332        for (String role : roleOrder) {
333            SlpcObject endpoint = requiredEndpoint(endpoints, participants.get(role));
334            String key = candidate ? candidateEndpointKey(endpoint) : requiredString(endpoint, "canonicalEndpointId");
335            keys.add(concat(string(role), string(key)));
336        }
337        return concat(string(kind), framedList(keys), relationshipDetailBytes(record, endpoints, candidate));
338    }
339
340    private static byte[] relationshipDetailBytes(SlpcObject record, Map<String, SlpcObject> endpoints, boolean candidate) {
341        String kind = requiredString(record, "relationshipKind");
342        List<byte[]> details = new ArrayList<byte[]>();
343        if ("PRIMARY_KEY".equals(kind) || "UNIQUE_KEY".equals(kind)) {
344            for (Object item : requiredArray(record, "keyFields")) {
345                SlpcObject field = asObject(item);
346                details.add(concat(integer(requiredLong(field, "ordinal")),
347                        string(endpointKey(endpoints, requiredString(field, "fieldEndpointRef"), candidate))));
348            }
349            return framedList(details);
350        }
351        if ("FOREIGN_KEY_REFERENCE".equals(kind)) {
352            for (Object item : requiredArray(record, "keyPairs")) {
353                SlpcObject pair = asObject(item);
354                details.add(concat(integer(requiredLong(pair, "ordinal")),
355                        string(endpointKey(endpoints, requiredString(pair, "foreignKeyFieldRef"), candidate)),
356                        string(endpointKey(endpoints, requiredString(pair, "referencedFieldRef"), candidate))));
357            }
358            return framedList(details);
359        }
360        return new byte[] {0};
361    }
362
363    private static String endpointKey(Map<String, SlpcObject> endpoints, String ref, boolean candidate) {
364        SlpcObject endpoint = requiredEndpoint(endpoints, ref);
365        return candidate ? candidateEndpointKey(endpoint) : requiredString(endpoint, "canonicalEndpointId");
366    }
367
368    private static byte[] endpointScopeBytes(SlpcObject endpoint) {
369        SlpcObject scope = endpoint.object("scope");
370        if (scope == null) return new byte[] {0};
371        return concat(string("SCOPE"), string(requiredString(scope, "scopeKind")),
372                string(requiredString(scope, "scopeId")), string(scope.string("definedAtStatementId")));
373    }
374
375    private static byte[] endpointIdentityBytes(SlpcObject identity) {
376        String kind = requiredString(identity, "kind");
377        if ("COMPOSITE_EXACT".equals(kind) || "COMPOSITE_APPROXIMATE".equals(kind)) {
378            List<byte[]> segments = new ArrayList<byte[]>();
379            for (Object item : requiredArray(identity, "segments")) segments.add(segmentBytes(asObject(item)));
380            return concat(string(kind), string(requiredString(identity, "qualificationPolicyId")), framedList(segments));
381        }
382        if ("AUTHORITATIVE_EXTERNAL".equals(kind)) {
383            return concat(string(kind), string(requiredString(identity, "system")),
384                    string(requiredString(identity, "identityVersion")), string(requiredString(identity, "opaqueId")));
385        }
386        if ("URI_EXACT".equals(kind)) {
387            return concat(string(kind), string(requiredString(identity, "normalizationPolicyId")),
388                    string(requiredString(identity, "scheme")), string("SHA256"), string(requiredString(identity, "locatorDigest")));
389        }
390        if ("UNAVAILABLE".equals(kind)) return concat(string(kind), string(requiredString(identity, "reason")));
391        throw new IllegalArgumentException("unsupported endpoint identity: " + kind);
392    }
393
394    private static byte[] segmentBytes(SlpcObject segment) {
395        String kind = requiredString(segment, "identityKind");
396        byte[] base = concat(string(requiredString(segment, "segmentKind")), string(kind));
397        if ("GSP_PERSISTENT_V1".equals(kind) || "GSP_APPROXIMATE_V1".equals(kind)) {
398            return concat(base, string(requiredString(segment, "policyId")), string(requiredString(segment, "objectGroup")),
399                    string(requiredString(segment, "payload")));
400        }
401        if ("EXTERNAL_SEGMENT_V1".equals(kind)) {
402            return concat(base, string(requiredString(segment, "system")), string(requiredString(segment, "identityVersion")),
403                    string(requiredString(segment, "opaqueId")));
404        }
405        throw new IllegalArgumentException("unsupported identity segment kind: " + kind);
406    }
407
408    private static byte[] anchorBytes(SlpcObject anchor, Map<String, SlpcObject> endpoints, Map<String, SlpcObject> nodes) {
409        if ("ENDPOINT".equals(requiredString(anchor, "kind"))) {
410            return concat(string("ENDPOINT"),
411                    string(candidateEndpointKey(requiredEndpoint(endpoints, requiredString(anchor, "endpointRef")))),
412                    string(requiredString(anchor, "portCode")));
413        }
414        SlpcObject node = requiredRecord(nodes, requiredString(anchor, "nodeRef"), "node");
415        return concat(string("NODE"), string(requiredString(node, "nodeFingerprint")), string(requiredString(anchor, "portCode")));
416    }
417
418    private static String[] evidenceFingerprint(SlpcObject evidence) {
419        if (evidence == null) return new String[] {null, null};
420        SlpcObject fingerprint = requiredObject(evidence, "fingerprint");
421        return new String[] {requiredString(fingerprint, "algorithm"), requiredString(fingerprint, "value")};
422    }
423
424    private static byte[] framedStrings(List<String> values) {
425        List<byte[]> framed = new ArrayList<byte[]>();
426        for (String value : values) framed.add(string(value));
427        return framedList(framed);
428    }
429
430    private static List<String> fieldValues(List<Object> values, String field) {
431        List<String> result = new ArrayList<String>();
432        for (Object value : values) result.add(requiredString(asObject(value), field));
433        return result;
434    }
435
436    private static String digest(String prefix, String domain, byte[] payload) {
437        try {
438            MessageDigest digest = MessageDigest.getInstance("SHA-256");
439            digest.update(domain.getBytes(StandardCharsets.US_ASCII));
440            digest.update((byte) 0);
441            digest.update(payload);
442            byte[] value = digest.digest();
443            StringBuilder hex = new StringBuilder(prefix);
444            for (byte item : value) hex.append(String.format("%02x", item & 0xff));
445            return hex.toString();
446        } catch (NoSuchAlgorithmException impossible) {
447            throw new IllegalStateException("SHA-256 is required by the Java runtime", impossible);
448        }
449    }
450
451    private static byte[] concat(byte[]... values) {
452        ByteArrayOutputStream output = new ByteArrayOutputStream();
453        for (byte[] value : values) write(output, value);
454        return output.toByteArray();
455    }
456
457    private static void write(ByteArrayOutputStream output, byte[] value) {
458        output.write(value, 0, value.length);
459    }
460
461    private static SlpcObject requiredEndpoint(Map<String, SlpcObject> endpoints, String ref) {
462        return requiredRecord(endpoints, ref, "endpoint");
463    }
464
465    private static SlpcObject requiredRecord(Map<String, SlpcObject> records, String ref, String kind) {
466        SlpcObject value = records.get(ref);
467        if (value == null) throw new IllegalArgumentException("missing " + kind + " reference: " + ref);
468        return value;
469    }
470
471    private static SlpcObject requiredObject(SlpcObject object, String field) {
472        SlpcObject value = object.object(field);
473        if (value == null) throw new IllegalArgumentException("missing object field: " + field);
474        return value;
475    }
476
477    private static List<Object> requiredArray(SlpcObject object, String field) {
478        List<Object> value = object.array(field);
479        if (value == null) throw new IllegalArgumentException("missing array field: " + field);
480        return value;
481    }
482
483    private static String requiredString(SlpcObject object, String field) {
484        String value = object.string(field);
485        if (value == null) throw new IllegalArgumentException("missing string field: " + field);
486        return value;
487    }
488
489    private static long requiredLong(SlpcObject object, String field) {
490        BigInteger value = object.integer(field);
491        if (value == null) throw new IllegalArgumentException("missing integer field: " + field);
492        return value.longValueExact();
493    }
494
495    private static SlpcObject asObject(Object value) {
496        if (!(value instanceof SlpcObject)) throw new IllegalArgumentException("expected object");
497        return (SlpcObject) value;
498    }
499
500    private static List<String> relationshipRoles(String kind) {
501        if ("PRIMARY_KEY".equals(kind) || "UNIQUE_KEY".equals(kind)) return list("KEY_RELATION");
502        if ("FOREIGN_KEY_REFERENCE".equals(kind)) return list("FOREIGN_KEY_RELATION", "REFERENCED_RELATION");
503        if ("CALLS".equals(kind)) return list("CALLER_ROUTINE", "CALLEE_ROUTINE");
504        if ("STREAMS_FROM".equals(kind)) return list("STREAM", "BASE_RELATION");
505        if ("SYNONYM_OF".equals(kind)) return list("SYNONYM", "BASE_OBJECT");
506        if ("READS_FROM".equals(kind)) return list("CONSUMER_RELATION", "STORAGE_ASSET");
507        if ("WRITES_TO".equals(kind)) return list("PRODUCER_RELATION", "STORAGE_ASSET");
508        throw new IllegalArgumentException("unsupported relationship kind: " + kind);
509    }
510
511    private static List<OperationRole> operationRoles(String kind) {
512        if ("QUERY".equals(kind)) return roles("READ_RELATION", true, "OUTPUT_RELATION", true);
513        if ("INSERT".equals(kind) || "UPDATE".equals(kind) || "DELETE".equals(kind)) return roles("WRITE_TARGET", false, "READ_SOURCE", true);
514        if ("MERGE".equals(kind)) return roles("MERGE_TARGET", false, "MERGE_SOURCE", true);
515        if ("CREATE_TABLE".equals(kind)) return roles("CREATED_OBJECT", false);
516        if ("CREATE_TABLE_AS".equals(kind) || "CREATE_VIEW".equals(kind) || "CREATE_MATERIALIZED_VIEW".equals(kind)) return roles("CREATED_OBJECT", false, "READ_SOURCE", true);
517        if ("CREATE_EXTERNAL_TABLE".equals(kind) || "CREATE_STAGE".equals(kind)) return roles("CREATED_OBJECT", false, "STORAGE_SOURCE", true);
518        if ("CREATE_STREAM".equals(kind)) return roles("CREATED_STREAM", false, "BASE_RELATION", false);
519        if ("CREATE_ROUTINE".equals(kind)) return roles("CREATED_ROUTINE", false);
520        if ("CREATE_SYNONYM".equals(kind)) return roles("SYNONYM", false, "BASE_OBJECT", false);
521        if ("CREATE_INDEX".equals(kind)) return roles("CREATED_INDEX", false, "INDEXED_RELATION", false);
522        if ("RENAME".equals(kind)) return roles("BEFORE_OBJECT", false, "AFTER_OBJECT", false);
523        if ("SWAP".equals(kind)) return roles("SWAP_LEFT_OBJECT", false, "SWAP_RIGHT_OBJECT", false);
524        if ("CLONE".equals(kind)) return roles("CLONE_SOURCE", false, "CLONE_TARGET", false);
525        if ("LOAD".equals(kind)) return roles("STORAGE_SOURCE", true, "WRITE_TARGET", false);
526        if ("UNLOAD".equals(kind) || "INSERT_OVERWRITE_DIRECTORY".equals(kind)) return roles("READ_SOURCE", true, "STORAGE_TARGET", false);
527        if ("CALL_ROUTINE".equals(kind)) return roles("CALLEE_ROUTINE", false, "ARGUMENT_SOURCE", true);
528        if ("ALTER".equals(kind) || "DROP".equals(kind) || "TRUNCATE".equals(kind)) return roles("AFFECTED_OBJECT", false);
529        if ("OTHER".equals(kind)) return roles("AFFECTED_OBJECT", true);
530        throw new IllegalArgumentException("unsupported operation kind: " + kind);
531    }
532
533    private static List<OperationRole> roles(Object... values) {
534        List<OperationRole> result = new ArrayList<OperationRole>();
535        for (int index = 0; index < values.length; index += 2) {
536            result.add(new OperationRole((String) values[index], ((Boolean) values[index + 1]).booleanValue()));
537        }
538        return result;
539    }
540
541    private static List<String> list(String... values) {
542        ArrayList<String> result = new ArrayList<String>();
543        Collections.addAll(result, values);
544        return result;
545    }
546
547    private static final class OperationRole {
548        private final String name;
549        private final boolean setLike;
550        private OperationRole(String name, boolean setLike) { this.name = name; this.setLike = setLike; }
551    }
552
553    private static final Comparator<String> UTF8_STRING_COMPARATOR = new Comparator<String>() {
554        @Override public int compare(String left, String right) {
555            byte[] a = left.getBytes(StandardCharsets.UTF_8); byte[] b = right.getBytes(StandardCharsets.UTF_8);
556            int common = Math.min(a.length, b.length);
557            for (int i = 0; i < common; i++) { int d = (a[i] & 255) - (b[i] & 255); if (d != 0) return d; }
558            return a.length - b.length;
559        }
560    };
561}