001package gudusoft.gsqlparser.lineage.v2.identity; 002 003import gudusoft.gsqlparser.lineage.v2.Finding; 004import gudusoft.gsqlparser.lineage.v2.SqllrException; 005import gudusoft.gsqlparser.lineage.v2.SqllrResources; 006import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson; 007 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.Collections; 011import java.util.HashMap; 012import java.util.HashSet; 013import java.util.LinkedHashMap; 014import java.util.List; 015import java.util.Map; 016import java.util.Set; 017import java.util.TreeSet; 018 019/** 020 * Recomputes every id of a lineage document from the identity inputs it carries ({@code identity-v1.md}, 021 * Section 6), as the reference's {@code check-ids} and {@code fill-ids} do ({@code sqllr_ids.py}, 022 * {@code DocumentIds} and {@code check_document}). 023 * 024 * <p>Canonical segment keys are read from the document ({@code canonicalKey}); checking them against the 025 * identifier-rule data is the IDENTITY stage's job. An endpoint declared {@code LOCAL} whose class is portable 026 * is accepted and listed as unchecked, because the qualification policy needs the profile data.</p> 027 * 028 * <p>A finding is reported where the reference reports it; every input the class cannot check is listed as a 029 * note. Owner chains and derived-unit chains are walked with explicit lists, so their length is not limited by 030 * the thread stack; a chain that returns to itself is a note (owners) or {@code REF_OWNER_CYCLE} (units).</p> 031 * 032 * @since 4.2.17 033 */ 034public final class DocumentIds { 035 036 /** Findings and notes of one run. */ 037 public static final class Report { 038 private final List<Finding> findings; 039 private final List<String> notes; 040 private final Object document; 041 042 Report(List<Finding> findings, List<String> notes, Object document) { 043 this.findings = Collections.unmodifiableList(findings); 044 this.notes = Collections.unmodifiableList(notes); 045 this.document = document; 046 } 047 048 /** Mismatches ({@code ID_MISMATCH}, {@code ID_MISSING}, ...) and rejected inputs. */ 049 public List<Finding> getFindings() { 050 return findings; 051 } 052 053 /** The inputs that could not be checked. */ 054 public List<String> getNotes() { 055 return notes; 056 } 057 058 /** For {@link #fill(Object)}: the document with every computable id filled in; null otherwise. */ 059 public Object getDocument() { 060 return document; 061 } 062 } 063 064 /** 065 * {@code check-ids}: recomputes every id of a document, and the id of the dependency snapshot each analysis 066 * context names (looked up among the snapshots this library ships). 067 */ 068 public static Report check(Object document) { 069 return check(document, null); 070 } 071 072 /** As {@link #check(Object)}, with the given dependency snapshot (a parsed snapshot file) for every context. */ 073 public static Report check(Object document, Object snapshot) { 074 DocumentIds ids = new DocumentIds(document, false); 075 Set<Object> seen = new HashSet<Object>(); 076 for (Object item : list(map(document).get("analysisContexts"))) { 077 Object snapshotId = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("dependencySnapshotId") : null; 078 if (!seen.add(snapshotId == null ? NULL_KEY : snapshotId)) { 079 continue; 080 } 081 String where = "analysisContexts ref " + map(item).get("ref") + ": dependencySnapshotId"; 082 String path = "snapshot"; 083 Map<?, ?> found = null; 084 if (snapshot != null) { 085 found = map(snapshot); 086 } else { 087 for (String candidate : shippedSnapshots()) { 088 Map<?, ?> parsed = map(SqllrResources.json(candidate)); 089 if (snapshotId != null && snapshotId.equals(parsed.get("snapshotId"))) { 090 found = parsed; 091 path = candidate; 092 break; 093 } 094 } 095 } 096 if (found == null) { 097 ids.notes.add("dependency snapshot " + snapshotId + " not found among the shipped snapshots; its id is used" 098 + " as declared"); 099 continue; 100 } 101 String recomputed = snapshotIdOf(found); 102 if (!recomputed.equals(found.get("snapshotId"))) { 103 ids.findings.add(new Finding("ID_MISMATCH", path, "snapshotId " + found.get("snapshotId") + ", computed " 104 + recomputed)); 105 } 106 if (!recomputed.equals(snapshotId)) { 107 ids.findings.add(new Finding("ID_MISMATCH", where, "declared " + snapshotId + ", snapshot " + path 108 + " computes " + recomputed)); 109 } 110 } 111 ids.run(); 112 return new Report(ids.findings, ids.notes, null); 113 } 114 115 /** 116 * {@code fill-ids}: a copy of the document with every computable id, numeric key, key and record id set, 117 * and every id that is not computed removed. 118 */ 119 public static Report fill(Object document) { 120 Object copy = mutableCopy(document); 121 DocumentIds ids = new DocumentIds(copy, true); 122 ids.run(); 123 return new Report(ids.findings, ids.notes, copy); 124 } 125 126 /** The id of a dependency snapshot: {@code DependencySnapshotIdV1} of the file without {@code snapshotId}. */ 127 public static String snapshotIdOf(Object snapshot) { 128 Map<String, Object> manifest = new LinkedHashMap<String, Object>(); 129 for (Map.Entry<?, ?> entry : map(snapshot).entrySet()) { 130 if (!"snapshotId".equals(entry.getKey())) { 131 manifest.put((String) entry.getKey(), entry.getValue()); 132 } 133 } 134 Map<String, Object> inputs = new LinkedHashMap<String, Object>(); 135 inputs.put("manifest", manifest); 136 return IdentityV1.compute(IdentityV1.DEPENDENCY_SNAPSHOT, inputs).getId(); 137 } 138 139 /** The shipped snapshot files, sorted by path. */ 140 public static List<String> shippedSnapshots() { 141 List<String> out = new ArrayList<String>(); 142 for (String path : new TreeSet<String>(SqllrResources.index().keySet())) { 143 if (path.startsWith("snapshots/") && path.endsWith(".json")) { 144 out.add(path); 145 } 146 } 147 return out; 148 } 149 150 // --------------------------------------------------------------------------------------------- 151 152 private static final Object NULL_KEY = new Object(); 153 private static final List<String> DIGEST_PRECEDENCE = IdentityV1.DIGEST_DOMAINS; 154 155 private final Map<?, ?> doc; 156 private final boolean fill; 157 private final List<Finding> findings = new ArrayList<Finding>(); 158 private final List<String> notes = new ArrayList<String>(); 159 private final Map<Object, Object[]> byRef = new HashMap<Object, Object[]>(); 160 private final Map<?, ?> header; 161 private final Map<Object, String> units = new HashMap<Object, String>(); 162 private final Map<Object, String> contexts = new HashMap<Object, String>(); 163 private final Map<Object, String[]> endpoints = new HashMap<Object, String[]>(); 164 private final Map<Object, String[]> facts = new HashMap<Object, String[]>(); 165 166 private DocumentIds(Object document, boolean fill) { 167 this.doc = map(document); 168 this.fill = fill; 169 this.header = map(doc.get("header")); 170 Map<?, ?> explain = map(doc.get("explainGraph")); 171 Object[][] collections = { 172 {"analysisContexts", doc.get("analysisContexts")}, {"sourceUnits", doc.get("sourceUnits")}, 173 {"statements", doc.get("statements")}, {"scopes", doc.get("scopes")}, {"endpoints", doc.get("endpoints")}, 174 {"facts", doc.get("facts")}, {"factCandidates", doc.get("factCandidates")}, 175 {"candidateSets", doc.get("candidateSets")}, 176 {"occurrences", doc.get("occurrences")}, {"targetFieldOccurrences", doc.get("targetFieldOccurrences")}, 177 {"diagnostics", doc.get("diagnostics")}, {"explainGraph.nodes", explain.get("nodes")}, 178 {"explainGraph.microEdges", explain.get("microEdges")}, 179 {"explainGraph.candidateSets", explain.get("candidateSets")}, 180 {"explainGraph.supportPaths", explain.get("supportPaths")}}; 181 for (Object[] collection : collections) { 182 for (Object item : list(collection[1])) { 183 index((String) collection[0], item); 184 } 185 if ("factCandidates".equals(collection[0])) { 186 for (Object item : list(collection[1])) { 187 for (String side : new String[] {"source", "target"}) { 188 Object end = map(item).get(side); 189 if (end instanceof Map<?, ?> && ((Map<?, ?>) end).containsKey("candidateSet")) { 190 index("candidateSets", ((Map<?, ?>) end).get("candidateSet")); 191 } 192 } 193 } 194 } 195 } 196 } 197 198 private static Map<?, ?> map(Object value) { 199 return value instanceof Map<?, ?> ? (Map<?, ?>) value : Collections.emptyMap(); 200 } 201 202 private static List<?> list(Object value) { 203 return value instanceof List<?> ? (List<?>) value : Collections.emptyList(); 204 } 205 206 /** 207 * The map key of a reference value: integers by numeric value whatever their Java type (a producer may 208 * build maps with Integer or BigInteger), booleans and everything else as they are. 209 */ 210 private static Object refKey(Object value) { 211 if (value == null) { 212 return NULL_KEY; 213 } 214 if (value instanceof Integer || value instanceof Short || value instanceof Byte) { 215 return Long.valueOf(((Number) value).longValue()); 216 } 217 if (value instanceof java.math.BigInteger && ((java.math.BigInteger) value).bitLength() < 64) { 218 return Long.valueOf(((java.math.BigInteger) value).longValue()); 219 } 220 return value; 221 } 222 223 private void index(String collection, Object item) { 224 Object ref = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("ref") : null; 225 if (byRef.containsKey(refKey(ref))) { 226 findings.add(new Finding("REF_DUPLICATE", collection + " ref " + ref, "ref used twice")); 227 } 228 byRef.put(refKey(ref), new Object[] {collection, item}); 229 } 230 231 private Map<?, ?> get(Object ref, String collection) { 232 Object[] found = byRef.get(refKey(ref)); 233 if (found == null || !collection.equals(found[0])) { 234 throw new SqllrException("REF_DANGLING", "ref " + ref + " is not a " + collection + " object"); 235 } 236 return map(found[1]); 237 } 238 239 private void compare(String location, Object declared, String computed, String what) { 240 if (computed == null && declared != null) { 241 findings.add(new Finding("ID_UNEXPECTED", location, "declares " + what + " " + declared + ", but none is computed")); 242 } else if (computed != null && declared == null) { 243 findings.add(new Finding("ID_MISSING", location, what + " " + computed + " is computed but not declared")); 244 } else if (computed != null && !computed.equals(declared)) { 245 findings.add(new Finding("ID_MISMATCH", location, "declared " + declared + ", computed " + computed)); 246 } 247 } 248 249 private void compareNumeric(String location, Object declared, String id) { 250 if (declared != null && (id == null || !declared.equals(IdentityV1.numericKey(id)))) { 251 findings.add(new Finding("ID_NUMERIC_KEY_MISMATCH", location, "numericKey " + declared + " is not derived from " 252 + id)); 253 } 254 } 255 256 private static Map<String, Object> inputs(Object... pairs) { 257 Map<String, Object> out = new LinkedHashMap<String, Object>(); 258 for (int i = 0; i < pairs.length; i += 2) { 259 out.put((String) pairs[i], pairs[i + 1]); 260 } 261 return out; 262 } 263 264 private static Map<String, Object> status(String status) { 265 return inputs("status", status); 266 } 267 268 // -- source units and analysis contexts 269 270 private Map<?, ?> contextOf(Map<?, ?> unit) { 271 return get(unit.get("analysisContextRef"), "analysisContexts"); 272 } 273 274 private Map<String, Object> scopeOf(Map<?, ?> unit) { 275 Map<?, ?> context = contextOf(unit); 276 Object profile = context.get("pathProfile"); 277 Object producer = map(context.get("producer")).get("name"); 278 return inputs("profile", profile, "producer", IdentityV1.PATH_PROFILE_V1.equals(profile) ? null : producer); 279 } 280 281 /** 282 * The {@code SourceUnitIdV1} of a unit. A {@code DERIVED} unit needs its parent's id first; the parent 283 * chain is walked with an explicit list, and a chain that returns to a unit is {@code REF_OWNER_CYCLE}. 284 */ 285 private String unitId(Map<?, ?> unit) { 286 List<Map<?, ?>> chain = new ArrayList<Map<?, ?>>(); 287 Set<Map<?, ?>> onChain = Collections.newSetFromMap(new java.util.IdentityHashMap<Map<?, ?>, Boolean>()); 288 Map<?, ?> current = unit; 289 while (!units.containsKey(refKey(current.get("ref")))) { 290 if (!onChain.add(current)) { 291 throw new SqllrException("REF_OWNER_CYCLE", "the parent chain of source unit " + unit.get("ref") 292 + " returns to unit " + current.get("ref")); 293 } 294 chain.add(current); 295 if (!"DERIVED".equals(current.get("identityKind"))) { 296 break; 297 } 298 current = get(current.get("parentUnitRef"), "sourceUnits"); 299 } 300 for (int i = chain.size() - 1; i >= 0; i--) { 301 Map<?, ?> item = chain.get(i); 302 Object kind = item.get("identityKind"); 303 Map<String, Object> in; 304 if ("NAMED".equals(kind)) { 305 in = inputs("kind", "NAMED", "namespace", item.get("namespace"), "logicalName", item.get("logicalName")); 306 } else if ("CONTENT_ADDRESSED".equals(kind)) { 307 in = inputs("kind", "CONTENT_ADDRESSED", "contextDomain", item.get("contextDomain"), "contentDigest", 308 item.get("contentDigest")); 309 } else if ("DERIVED".equals(kind)) { 310 Map<?, ?> parentUnit = get(item.get("parentUnitRef"), "sourceUnits"); 311 String parent = units.get(refKey(parentUnit.get("ref"))); 312 Map<?, ?> site = get(item.get("siteStatementRef"), "statements"); 313 in = inputs("kind", "DERIVED", "parentUnit", parent != null ? parent : status(IdentityV1.LOCAL), 314 "statementOrdinal", site.get("ordinal"), "pathScope", scopeOf(parentUnit), 315 "sitePath", item.get("sitePath"), "siteOrdinal", item.get("siteOrdinal"), 316 "variantOrdinal", item.get("variantOrdinal")); 317 } else { 318 in = inputs("kind", "LOCAL"); 319 } 320 units.put(refKey(item.get("ref")), IdentityV1.compute(IdentityV1.SOURCE_UNIT, in).getId()); 321 } 322 return units.get(refKey(unit.get("ref"))); 323 } 324 325 private String contextId(Map<?, ?> unit) { 326 Object key = refKey(unit.get("ref")); 327 if (!contexts.containsKey(key)) { 328 Map<Object, Object> digests = new HashMap<Object, Object>(); 329 for (Object digest : list(unit.get("digests"))) { 330 digests.put(map(digest).get("domain"), digest); 331 } 332 Object chosen = null; 333 for (String domain : DIGEST_PRECEDENCE) { 334 if (digests.containsKey(domain)) { 335 chosen = digests.get(domain); 336 break; 337 } 338 } 339 Map<?, ?> context = contextOf(unit); 340 if (unitId(unit) == null || chosen == null) { 341 contexts.put(key, null); 342 } else { 343 Map<?, ?> metadata = map(context.get("metadata")); 344 Map<?, ?> dialect = map(context.get("dialect")); 345 Map<?, ?> redaction = map(context.get("redaction")); 346 Map<?, ?> producer = map(context.get("producer")); 347 Object resolution = unit.containsKey("resolutionInputDigests") ? unit.get("resolutionInputDigests") 348 : new ArrayList<Object>(); 349 contexts.put(key, IdentityV1.compute(IdentityV1.ANALYSIS_CONTEXT, inputs( 350 "producerName", producer.get("name"), "producerVersion", producer.get("version"), 351 "dependencySnapshot", context.get("dependencySnapshotId"), 352 "dialectId", dialect.get("id"), "dialectVersion", dialect.get("version"), 353 "identifierPolicyId", map(context.get("identifierPolicy")).get("policyId"), 354 "pathProfile", context.get("pathProfile"), 355 "effectiveOptionsDigest", context.get("effectiveOptionsDigest"), 356 "metadataStatus", metadata.get("status"), "metadataDigest", metadata.get("digest"), 357 "redactionPolicyId", redaction.get("policyId"), 358 "redactionPolicyVersion", redaction.get("policyVersion"), 359 "capabilities", context.get("capabilities"), "decoding", unit.get("decoding"), 360 "sourceContentDigest", chosen, "resolutionInputDigests", resolution)).getId()); 361 } 362 } 363 return contexts.get(key); 364 } 365 366 private void checkContext(Map<?, ?> context, String where) { 367 Map<?, ?> metadata = map(context.get("metadata")); 368 if ("NONE".equals(metadata.get("status")) || !metadata.containsKey("digest")) { 369 return; 370 } 371 if (!metadata.containsKey("artifacts")) { 372 notes.add(where + ": metadata artifacts not listed; the metadata digest is not checked"); 373 return; 374 } 375 String computed = IdentityV1.compute(IdentityV1.METADATA_DIGEST, inputs("artifacts", metadata.get("artifacts"))).getId(); 376 if (!computed.equals(metadata.get("digest"))) { 377 findings.add(new Finding("ID_MISMATCH", where, "metadata digest " + metadata.get("digest") + ", artifacts compute " 378 + computed)); 379 } 380 } 381 382 // -- endpoints 383 384 private List<Object> segmentInputs(Map<?, ?> endpoint) { 385 List<Object> out = new ArrayList<Object>(); 386 for (Object item : list(map(endpoint.get("name")).get("segments"))) { 387 Map<?, ?> seg = map(item); 388 Object role = seg.get("role"); 389 if ("PRODUCER_DEFAULT".equals(seg.get("kind"))) { 390 out.add(inputs("role", role, "kind", "PRODUCER_DEFAULT")); 391 } else if ("AUTHORITY".equals(role) || "LOCATION".equals(role)) { 392 out.add(inputs("role", role, "kind", "NAMED", "canonicalKey", seg.get("text"))); 393 } else if (seg.containsKey("keyUnavailable")) { 394 out.add(inputs("role", role, "kind", "NAMED", "keyUnavailable", seg.get("keyUnavailable"))); 395 } else if (seg.containsKey("canonicalKey")) { 396 out.add(inputs("role", role, "kind", "NAMED", "canonicalKey", seg.get("canonicalKey"))); 397 } else { 398 return null; 399 } 400 } 401 return out; 402 } 403 404 /** (top-level owner, full field path) of a FIELD endpoint; the path is null when a key is missing. */ 405 private Object[] fieldPath(Map<?, ?> endpoint) { 406 List<Map<?, ?>> chain = new ArrayList<Map<?, ?>>(); 407 Set<Map<?, ?>> visited = Collections.newSetFromMap(new java.util.IdentityHashMap<Map<?, ?>, Boolean>()); 408 Map<?, ?> current = endpoint; 409 Map<?, ?> owner = get(current.get("ownerRef"), "endpoints"); 410 while ("FIELD".equals(owner.get("kind"))) { 411 chain.add(current); 412 if (!visited.add(current)) { 413 return new Object[] {owner, null}; 414 } 415 current = owner; 416 owner = get(current.get("ownerRef"), "endpoints"); 417 } 418 // current is the top-level field; owner its relation (or ROW_SET, ...). 419 List<?> segments = list(map(current.get("name")).get("segments")); 420 if (segments.size() != 1) { 421 return new Object[] {owner, null}; 422 } 423 Map<?, ?> seg = map(segments.get(0)); 424 List<Object> path = new ArrayList<Object>(); 425 if (seg.containsKey("keyUnavailable")) { 426 path.add(inputs("kind", "FIELD", "keyUnavailable", seg.get("keyUnavailable"))); 427 } else if (seg.containsKey("canonicalKey")) { 428 path.add(inputs("kind", "FIELD", "canonicalKey", seg.get("canonicalKey"))); 429 } else { 430 return new Object[] {owner, null}; 431 } 432 for (int i = chain.size() - 1; i >= 0; i--) { 433 for (Object item : list(chain.get(i).get("pathSegments"))) { 434 Map<?, ?> segment = map(item); 435 Map<String, Object> copy = new LinkedHashMap<String, Object>(); 436 for (String name : new String[] {"kind", "canonicalKey", "index", "keyUnavailable"}) { 437 if (segment.containsKey(name)) { 438 copy.put(name, segment.get(name)); 439 } 440 } 441 path.add(copy); 442 } 443 } 444 return new Object[] {owner, path}; 445 } 446 447 /** 448 * (status, id) recomputed for an endpoint; null when the document lacks an input. A FIELD or ROW_SET needs 449 * its owner's result first: the owner chain is followed with an explicit list and computed from the far 450 * end back, in the order the reference's recursion computes it. 451 */ 452 private String[] endpointResult(Map<?, ?> endpoint) { 453 List<Object[]> pending = new ArrayList<Object[]>(); // {endpoint, path} awaiting their owner's result 454 Map<?, ?> current = endpoint; 455 String[] result; 456 while (true) { 457 Object key = refKey(current.get("ref")); 458 if (endpoints.containsKey(key)) { 459 result = endpoints.get(key); 460 break; 461 } 462 endpoints.put(key, null); // guards against owner cycles 463 Object kind = current.get("kind"); 464 Object declared = map(current.get("identity")).get("status"); 465 String identityClass = kind instanceof String ? IdentityV1.IDENTITY_CLASS.get(kind) : null; 466 if (IdentityV1.OPAQUE_PRODUCER_LOCAL.equals(declared)) { 467 result = new String[] {IdentityV1.OPAQUE_PRODUCER_LOCAL, null}; 468 } else if (identityClass == null) { 469 result = new String[] {IdentityV1.LOCAL, null}; 470 } else if (IdentityV1.LOCAL.equals(declared) && "ROUTINE".equals(identityClass) 471 && !current.containsKey("signature")) { 472 // without a signature the overload is not proven: a routine is LOCAL whatever its qualification 473 // (spec P1-3; EndpointIdV1 blocks it), so nothing is left unchecked 474 result = new String[] {IdentityV1.LOCAL, null}; 475 } else if (IdentityV1.LOCAL.equals(declared) 476 && ("RELATION".equals(identityClass) || "ROUTINE".equals(identityClass))) { 477 notes.add("endpoint ref " + current.get("ref") + ": declared LOCAL; the qualification policy is not checked"); 478 result = new String[] {IdentityV1.LOCAL, null}; 479 } else if ("RELATION".equals(identityClass) || "ROUTINE".equals(identityClass)) { 480 result = null; 481 List<Object> segments = segmentInputs(current); 482 if (segments == null) { 483 notes.add("endpoint ref " + current.get("ref") + ": a NAMED segment has no canonicalKey; not recomputed"); 484 } else { 485 Map<String, Object> in = inputs("endpointKind", kind, "segments", segments); 486 if ("ROUTINE".equals(kind)) { 487 in.put("routineNamespace", current.get("routineNamespace")); 488 in.put("routineKind", current.get("routineKind")); 489 if (current.containsKey("signature")) { 490 in.put("signature", current.get("signature")); 491 } 492 } 493 IdentityV1.IdResult computed = IdentityV1.compute(IdentityV1.ENDPOINT, in); 494 result = new String[] {computed.getStatus(), computed.getId()}; 495 } 496 } else { 497 Map<?, ?> owner; 498 Object path; 499 if ("FIELD".equals(kind)) { 500 Object[] found = fieldPath(current); 501 owner = (Map<?, ?>) found[0]; 502 path = found[1]; 503 } else { 504 owner = get(current.get("ownerRef"), "endpoints"); 505 path = new ArrayList<Object>(); 506 } 507 pending.add(new Object[] {current, path}); 508 current = owner; 509 continue; 510 } 511 endpoints.put(key, result); 512 break; 513 } 514 for (int i = pending.size() - 1; i >= 0; i--) { 515 Map<?, ?> item = (Map<?, ?>) pending.get(i)[0]; 516 Object path = pending.get(i)[1]; 517 String[] ownerResult = result; 518 if (ownerResult != null && path == null && ownerResult[1] == null 519 && (IdentityV1.LOCAL.equals(ownerResult[0]) 520 || IdentityV1.OPAQUE_PRODUCER_LOCAL.equals(ownerResult[0]))) { 521 // a FIELD of a LOCAL owner (a routine's unnamed return field) is LOCAL whatever its path: its status 522 // is the worst of the owner's and the path's, and no path status ranks above LOCAL 523 result = new String[] {ownerResult[0], null}; 524 } else if (ownerResult == null || path == null) { 525 notes.add("endpoint ref " + item.get("ref") + ": owner or field key not available; not recomputed"); 526 result = null; 527 } else { 528 Object ownerInput = ownerResult[1] != null ? ownerResult[1] : status(ownerResult[0]); 529 Map<String, Object> in = inputs("endpointKind", item.get("kind"), "owner", ownerInput); 530 if ("FIELD".equals(item.get("kind"))) { 531 in.put("path", path); 532 } 533 IdentityV1.IdResult computed = IdentityV1.compute(IdentityV1.ENDPOINT, in); 534 result = new String[] {computed.getStatus(), computed.getId()}; 535 } 536 endpoints.put(refKey(item.get("ref")), result); 537 } 538 return result; 539 } 540 541 private Object endpointInput(Object ref) { 542 String[] result = endpointResult(get(ref, "endpoints")); 543 if (result == null) { 544 return null; 545 } 546 return result[1] != null ? result[1] : status(result[0]); 547 } 548 549 /** ("OK", id or null) or ("UNCHECKED", null). */ 550 private String[] factId(Map<?, ?> fact) { 551 Object key = refKey(fact.get("ref")); 552 if (!facts.containsKey(key)) { 553 Object source = endpointInput(fact.get("sourceRef")); 554 Object target = endpointInput(fact.get("targetRef")); 555 if (source == null || target == null) { 556 facts.put(key, new String[] {"UNCHECKED", null}); 557 } else { 558 Map<String, Object> in = inputs("kind", fact.get("kind"), "source", source, "target", target); 559 if (fact.containsKey("role")) { 560 in.put("role", fact.get("role")); 561 } 562 facts.put(key, new String[] {"OK", IdentityV1.compute(IdentityV1.FACT, in).getId()}); 563 } 564 } 565 return facts.get(key); 566 } 567 568 private Map<String, Object> sideInput(Map<?, ?> end) { 569 if (end.containsKey("endpointRef")) { 570 Object value = endpointInput(end.get("endpointRef")); 571 return value == null ? null : inputs("endpoint", value); 572 } 573 Map<?, ?> set = map(end.get("candidateSet")); 574 List<Object> members = new ArrayList<Object>(); 575 for (Object member : list(set.get("candidateRefs"))) { 576 members.add(endpointInput(member)); 577 } 578 if (members.contains(null)) { 579 return null; 580 } 581 return inputs("candidates", members, "membership", set.get("membership")); 582 } 583 584 private Map<String, Object> subjectInput(Map<?, ?> occurrence) { 585 if (occurrence.containsKey("factRef")) { 586 String[] fact = factId(get(occurrence.get("factRef"), "facts")); 587 if ("UNCHECKED".equals(fact[0])) { 588 return null; 589 } 590 return inputs("kind", "FACT", "fact", fact[1] != null ? fact[1] : status(IdentityV1.LOCAL)); 591 } 592 Map<?, ?> candidate = get(occurrence.get("factCandidateRef"), "factCandidates"); 593 Map<String, Object> source = sideInput(map(candidate.get("source"))); 594 Map<String, Object> target = sideInput(map(candidate.get("target"))); 595 if (source == null || target == null) { 596 return null; 597 } 598 Map<String, Object> subject = inputs("kind", "CANDIDATE", "factKind", candidate.get("kind"), "source", source, 599 "target", target); 600 if (candidate.containsKey("role")) { 601 subject.put("role", candidate.get("role")); 602 } 603 return subject; 604 } 605 606 // -- the whole document 607 608 private interface Check { 609 void run(); 610 } 611 612 private void guard(String where, Check check) { 613 try { 614 check.run(); 615 } catch (SqllrException e) { 616 findings.add(new Finding(e.getCode(), where, e.getDetail())); 617 } 618 } 619 620 private void run() { 621 for (Object item : list(doc.get("analysisContexts"))) { 622 final Map<?, ?> context = map(item); 623 final String where = "analysisContexts ref " + context.get("ref"); 624 guard(where, new Check() { 625 @Override 626 public void run() { 627 checkContext(context, where); 628 } 629 }); 630 } 631 for (Object item : list(doc.get("sourceUnits"))) { 632 final Map<?, ?> unit = map(item); 633 final String where = "sourceUnits ref " + unit.get("ref"); 634 guard(where, new Check() { 635 @Override 636 public void run() { 637 checkUnit(unit, where); 638 } 639 }); 640 } 641 for (Object item : list(doc.get("endpoints"))) { 642 final Map<?, ?> endpoint = map(item); 643 final String where = "endpoints ref " + endpoint.get("ref"); 644 guard(where, new Check() { 645 @Override 646 public void run() { 647 checkEndpoint(endpoint, where); 648 } 649 }); 650 } 651 for (Object item : list(doc.get("facts"))) { 652 final Map<?, ?> fact = map(item); 653 final String where = "facts ref " + fact.get("ref"); 654 guard(where, new Check() { 655 @Override 656 public void run() { 657 checkFact(fact, where); 658 } 659 }); 660 } 661 for (Object item : list(doc.get("occurrences"))) { 662 final Map<?, ?> occurrence = map(item); 663 final String where = "occurrences ref " + occurrence.get("ref"); 664 guard(where, new Check() { 665 @Override 666 public void run() { 667 checkOccurrence(occurrence, where); 668 } 669 }); 670 } 671 for (Object item : list(map(doc.get("explainGraph")).get("nodes"))) { 672 final Map<?, ?> node = map(item); 673 final String where = "explainGraph.nodes ref " + node.get("ref"); 674 guard(where, new Check() { 675 @Override 676 public void run() { 677 checkNode(node, where); 678 } 679 }); 680 } 681 } 682 683 @SuppressWarnings("unchecked") 684 private void settle(Map<?, ?> obj, String member, String where, String computed, String what) { 685 if (fill) { 686 Map<String, Object> target = (Map<String, Object>) obj; 687 if (computed == null) { 688 target.remove(member); 689 } else { 690 target.put(member, computed); 691 } 692 } else { 693 compare(where, obj.get(member), computed, what); 694 } 695 } 696 697 private void checkUnit(Map<?, ?> unit, String where) { 698 settle(unit, "unitId", where, unitId(unit), "unitId"); 699 settle(unit, "analysisContextId", where, contextId(unit), "analysisContextId"); 700 Object mode = header.get("sourceEmbedding"); 701 if (unit.containsKey("text") && ("FULL".equals(mode) || "REDACTED".equals(mode))) { 702 String domain = "FULL".equals(mode) ? "DECODED_TEXT_SHA256" : "REDACTED_TEXT_SHA256"; 703 List<Object> declared = new ArrayList<Object>(); 704 for (Object digest : list(unit.get("digests"))) { 705 if (domain.equals(map(digest).get("domain"))) { 706 declared.add(map(digest).get("value")); 707 } 708 } 709 Object text = unit.get("text"); 710 String actual = text instanceof String ? Framing.hex(IdentityV1.sha256(Framing.utf8((String) text))) : null; 711 if (actual == null || !declared.equals(Arrays.<Object>asList(actual))) { 712 findings.add(new Finding("ID_SOURCE_DIGEST_MISMATCH", where, "embedded text does not match " + domain)); 713 } 714 } 715 } 716 717 @SuppressWarnings("unchecked") 718 private void checkEndpoint(Map<?, ?> endpoint, String where) { 719 String[] result = endpointResult(endpoint); 720 if (result == null) { 721 return; 722 } 723 String status = result[0]; 724 String id = result[1]; 725 Map<?, ?> identity; 726 if (fill) { 727 Map<String, Object> target = (Map<String, Object>) endpoint; 728 if (!(target.get("identity") instanceof Map<?, ?>)) { 729 target.put("identity", new LinkedHashMap<String, Object>()); 730 } 731 identity = (Map<?, ?>) target.get("identity"); 732 ((Map<String, Object>) identity).put("status", status); 733 } else { 734 identity = map(endpoint.get("identity")); 735 if (!status.equals(identity.get("status"))) { 736 findings.add(new Finding("ID_MISMATCH", where, "declared status " + identity.get("status") + ", computed " 737 + status)); 738 } 739 } 740 settle(identity, "id", where, id, "id"); 741 if (fill) { 742 settle(identity, "numericKey", where, id == null ? null : IdentityV1.numericKey(id), "numericKey"); 743 } else { 744 compareNumeric(where, identity.get("numericKey"), id); 745 } 746 } 747 748 private void checkFact(Map<?, ?> fact, String where) { 749 String[] result = factId(fact); 750 if ("UNCHECKED".equals(result[0])) { 751 notes.add(where + ": an endpoint was not recomputed; the Fact id is not checked"); 752 return; 753 } 754 settle(fact, "id", where, result[1], "id"); 755 if (fill) { 756 settle(fact, "numericKey", where, result[1] == null ? null : IdentityV1.numericKey(result[1]), "numericKey"); 757 } else { 758 compareNumeric(where, fact.get("numericKey"), result[1]); 759 } 760 } 761 762 private void checkOccurrence(Map<?, ?> occurrence, String where) { 763 Map<?, ?> statement = get(occurrence.get("statementRef"), "statements"); 764 Map<?, ?> unit = get(statement.get("sourceUnitRef"), "sourceUnits"); 765 Map<String, Object> subject = subjectInput(occurrence); 766 if (subject == null) { 767 notes.add(where + ": an endpoint was not recomputed; the key is not checked"); 768 return; 769 } 770 String unitId = unitId(unit); 771 String key = IdentityV1.compute(IdentityV1.OCCURRENCE_KEY, inputs( 772 "sourceUnit", unitId != null ? unitId : status(IdentityV1.LOCAL), 773 "statementOrdinal", statement.get("ordinal"), "subject", subject, "pathScope", scopeOf(unit), 774 "path", occurrence.get("path"), "ordinal", occurrence.get("ordinal"))).getId(); 775 String context = contextId(unit); 776 String record = key != null && context != null 777 ? IdentityV1.compute(IdentityV1.OCCURRENCE_RECORD, inputs("occurrenceKey", key, "analysisContext", context)) 778 .getId() : null; 779 settle(occurrence, "key", where, key, "key"); 780 settle(occurrence, "recordId", where, record, "recordId"); 781 } 782 783 private void checkNode(Map<?, ?> node, String where) { 784 Map<?, ?> statement = get(node.get("statementRef"), "statements"); 785 Map<?, ?> unit = get(statement.get("sourceUnitRef"), "sourceUnits"); 786 String unitId = unitId(unit); 787 String key = IdentityV1.compute(IdentityV1.EXPLAIN_NODE_KEY, inputs( 788 "sourceUnit", unitId != null ? unitId : status(IdentityV1.LOCAL), 789 "statementOrdinal", statement.get("ordinal"), "pathScope", scopeOf(unit), "path", node.get("path"), 790 "nodeKind", node.get("kind"), "ordinal", node.get("ordinal"))).getId(); 791 String context = contextId(unit); 792 String record = key != null && context != null 793 ? IdentityV1.compute(IdentityV1.EXPLAIN_NODE_RECORD, inputs("nodeKey", key, "analysisContext", context)) 794 .getId() : null; 795 settle(node, "key", where, key, "key"); 796 settle(node, "recordId", where, record, "recordId"); 797 } 798 799 /** A deep copy with mutable objects and arrays, for {@link #fill(Object)}. */ 800 private static Object mutableCopy(Object value) { 801 return CanonicalJson.parse(CanonicalJson.jcsBytes(value)); 802 } 803}