001package gudusoft.gsqlparser.lineage.v2.identity; 002 003import gudusoft.gsqlparser.lineage.v2.SqllrException; 004import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson; 005 006import java.io.ByteArrayOutputStream; 007import java.math.BigInteger; 008import java.security.MessageDigest; 009import java.security.NoSuchAlgorithmException; 010import java.util.ArrayList; 011import java.util.Arrays; 012import java.util.Collection; 013import java.util.Collections; 014import java.util.Comparator; 015import java.util.HashMap; 016import java.util.HashSet; 017import java.util.LinkedHashMap; 018import java.util.List; 019import java.util.Map; 020import java.util.regex.Pattern; 021 022/** 023 * The identity algorithm family {@code PUBLIC-LINEAGE-ID-V1} ({@code lineage-spec/algorithms/identity-v1.md}). 024 * 025 * <p>Each algorithm takes its inputs as a JSON value in the shape of the specification's vectors and of the 026 * reference implementation ({@code sqllr_ids.py}, {@code compute}), validates them in the reference's order, 027 * and returns an {@link IdResult}: the identity status, and the framed input, digest, id and numeric key when 028 * an id is computed. Canonical segment keys are inputs; minting them belongs to the producer.</p> 029 * 030 * @since 4.2.17 031 */ 032public final class IdentityV1 { 033 034 public static final String FAMILY = "PUBLIC-LINEAGE-ID-V1"; 035 public static final String PATH_PROFILE_V1 = "sqllr-path-1"; 036 037 public static final String SOURCE_UNIT = "SourceUnitIdV1"; 038 public static final String ENDPOINT = "EndpointIdV1"; 039 public static final String FACT = "FactIdV1"; 040 public static final String OCCURRENCE_KEY = "OccurrenceKeyV1"; 041 public static final String ANALYSIS_CONTEXT = "AnalysisContextV1"; 042 public static final String OCCURRENCE_RECORD = "OccurrenceRecordIdV1"; 043 public static final String EXPLAIN_NODE_KEY = "ExplainNodeKeyV1"; 044 public static final String EXPLAIN_NODE_RECORD = "ExplainNodeRecordIdV1"; 045 public static final String DEPENDENCY_SNAPSHOT = "DependencySnapshotIdV1"; 046 public static final String METADATA_DIGEST = "MetadataDigestV1"; 047 048 /** Algorithm name to the prefix of its ids. */ 049 public static final Map<String, String> PREFIXES; 050 051 static { 052 Map<String, String> prefixes = new LinkedHashMap<String, String>(); 053 prefixes.put(SOURCE_UNIT, "su1"); 054 prefixes.put(ENDPOINT, "ep1"); 055 prefixes.put(FACT, "fact1"); 056 prefixes.put(OCCURRENCE_KEY, "occk1"); 057 prefixes.put(ANALYSIS_CONTEXT, "actx1"); 058 prefixes.put(OCCURRENCE_RECORD, "occr1"); 059 prefixes.put(EXPLAIN_NODE_KEY, "xnk1"); 060 prefixes.put(EXPLAIN_NODE_RECORD, "xnr1"); 061 prefixes.put(DEPENDENCY_SNAPSHOT, "snap1"); 062 PREFIXES = Collections.unmodifiableMap(prefixes); 063 } 064 065 public static final String PORTABLE = "PORTABLE"; 066 public static final String LOCAL = "LOCAL"; 067 public static final String UNAVAILABLE_COLLATION = "UNAVAILABLE_COLLATION"; 068 public static final String UNAVAILABLE_MIXED_PROFILE = "UNAVAILABLE_MIXED_PROFILE"; 069 public static final String OPAQUE_PRODUCER_LOCAL = "OPAQUE_PRODUCER_LOCAL"; 070 071 private static final Pattern ID = Pattern.compile("(su1|ep1|fact1|occk1|actx1|occr1|xnk1|xnr1|snap1)-([0-9a-f]{64})"); 072 private static final Pattern HEX64 = Pattern.compile("[0-9a-f]{64}"); 073 /** One step of an {@code sqllr-path-1} path. A whole path is checked step by step: Java's regex engine 074 * recurses once per repetition of a group, so one pattern over a long path would exhaust the stack. */ 075 private static final Pattern PATH_STEP = Pattern.compile("[A-Z][A-Z0-9_]*\\[(0|[1-9][0-9]*)\\]"); 076 private static final long MAX_ORDINAL = 9007199254740991L; 077 078 private static List<String> list(String... values) { 079 return Collections.unmodifiableList(Arrays.asList(values)); 080 } 081 082 public static final List<String> FACT_KINDS = list("VALUE", "ROW", "DATASET_DEPENDENCY", "CALL"); 083 public static final List<String> ROW_ROLES = list("FILTER", "JOIN_CONDITION", "GROUP_BY", "HAVING", "ORDER_BY", 084 "WINDOW_PARTITION", "WINDOW_ORDER", "DISTINCT", "QUALIFY", "LIMIT", "CONDITIONAL", "ROW_PROPAGATION", "UNKNOWN"); 085 public static final List<String> NON_PORTABLE = list(LOCAL, UNAVAILABLE_COLLATION, UNAVAILABLE_MIXED_PROFILE, 086 OPAQUE_PRODUCER_LOCAL); 087 public static final List<String> DIGEST_DOMAINS = list("RAW_BYTES_SHA256", "DECODED_TEXT_SHA256", 088 "REDACTED_TEXT_SHA256", "KEYED_TOKEN"); 089 public static final List<String> METADATA_STATUSES = list("NONE", "PARTIAL", "COMPLETE"); 090 public static final List<String> ANALYSIS_FEATURES = list("VALUE", "ROW", "DATASET_DEPENDENCY", "CALL", 091 "CONSTANT_INPUTS", "DYNAMIC_SQL", "ROUTINE_BODIES", "TEMPORARY_OBJECTS", "WILDCARD_EXPANSION", "NESTED_FIELDS"); 092 public static final List<String> CAPABILITY_STATUSES = list("SUPPORTED", "UNSUPPORTED", "PARTIAL"); 093 public static final List<String> BOM_HANDLING = list("NO_BOM", "BOM_STRIPPED"); 094 public static final List<String> LINE_ENDING_HANDLING = list("PRESERVED", "NORMALIZED_LF"); 095 public static final List<String> ROUTINE_NAMESPACES = list("CATALOG"); 096 public static final List<String> ROUTINE_KINDS = list("FUNCTION", "PROCEDURE"); 097 public static final List<String> SIGNATURE_FORMS = list("NOT_OVERLOADABLE", "PARAMETERS", "CATALOG_OVERLOAD_ID"); 098 public static final List<String> PARAMETER_MODES = list("IN", "OUT", "INOUT", "VARIADIC"); 099 public static final List<String> PATH_SEGMENT_KINDS = list("FIELD", "ARRAY_ELEMENT", "MAP_KEY", "MAP_VALUE", "INDEX"); 100 public static final List<String> CANDIDATE_SET_MEMBERSHIP = list("COMPLETE", "INCOMPLETE"); 101 public static final List<String> PATH_STEP_CODES = list( 102 "SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE_TABLE", "CREATE_VIEW", "COPY", "CALL", "EXECUTE", 103 "TARGET", "COLUMN_LIST", "SELECT_LIST", "FROM", "JOIN", "ON", "USING", "WHERE", "GROUP_BY", "HAVING", 104 "QUALIFY", "ORDER_BY", "LIMIT", "WINDOW", "PARTITION_BY", "WITH", "CTE", "SET_OP", "BRANCH", "SET", 105 "VALUES", "WHEN", "RETURNING", "SUBQUERY", "FUNCTION_ARG", "CASE_WHEN", "CASE_THEN", "CASE_ELSE", "PIVOT", 106 "LATERAL", "SOURCE", "CREATE_ROUTINE", "RETURN", "BLOCK", "ASSIGNMENT", "ASSIGN", "CONDITION", "CURSOR", 107 "FETCH"); 108 public static final List<String> EXPLAIN_NODE_KINDS = list("RESULT_SET", "CTE", "SUBQUERY", "SET_OPERATION", 109 "ROW_SET", "FIELD", "FUNCTION", "CASE", "EXPRESSION", "TERMINAL", "TABLE_FUNCTION", "PIVOT"); 110 111 /** 112 * Endpoint kind (registry group {@code endpointKinds}) to identity class (Section 5.2.1); a kind mapped to 113 * null has no identity class in V1 and is {@code LOCAL}. 114 */ 115 public static final Map<String, String> IDENTITY_CLASS; 116 117 static { 118 Map<String, String> classes = new LinkedHashMap<String, String>(); 119 for (String kind : new String[] {"RELATION", "TABLE", "VIEW", "MATERIALIZED_VIEW", "EXTERNAL_TABLE", "SYNONYM"}) { 120 classes.put(kind, "RELATION"); 121 } 122 classes.put("FIELD", "FIELD"); 123 classes.put("ROW_SET", "ROW_SET"); 124 classes.put("ROUTINE", "ROUTINE"); 125 for (String kind : new String[] {"QUERY_RESULT", "TEMP_TABLE", "TABLE_VARIABLE", "CTE", "VARIABLE", "PARAMETER", 126 "SEQUENCE", "STAGE", "STORAGE_LOCATION"}) { 127 classes.put(kind, null); 128 } 129 IDENTITY_CLASS = Collections.unmodifiableMap(classes); 130 } 131 132 /** Precedence of Section 4.2: the higher rank wins. */ 133 private static final Map<String, Integer> STATUS_RANK = new HashMap<String, Integer>(); 134 private static final Map<String, String> KEY_UNAVAILABLE = new HashMap<String, String>(); 135 136 static { 137 STATUS_RANK.put(PORTABLE, 0); 138 STATUS_RANK.put(UNAVAILABLE_COLLATION, 1); 139 STATUS_RANK.put(UNAVAILABLE_MIXED_PROFILE, 2); 140 STATUS_RANK.put(LOCAL, 3); 141 STATUS_RANK.put(OPAQUE_PRODUCER_LOCAL, 4); 142 KEY_UNAVAILABLE.put("COLLATION", UNAVAILABLE_COLLATION); 143 KEY_UNAVAILABLE.put("MIXED_PROFILE", UNAVAILABLE_MIXED_PROFILE); 144 } 145 146 private IdentityV1() { 147 } 148 149 // --------------------------------------------------------------------------------------------- 150 // Results 151 // --------------------------------------------------------------------------------------------- 152 153 /** The outcome of one algorithm run. */ 154 public static final class IdResult { 155 private final String status; 156 private final byte[] framed; 157 private final String sha256Hex; 158 private final String id; 159 private final String numericKey; 160 161 IdResult(String status, byte[] framed, String sha256Hex, String id, String numericKey) { 162 this.status = status; 163 this.framed = framed; 164 this.sha256Hex = sha256Hex; 165 this.id = id; 166 this.numericKey = numericKey; 167 } 168 169 /** The identity status ({@code PORTABLE}, {@code LOCAL}, ...). */ 170 public String getStatus() { 171 return status; 172 } 173 174 /** The framed input (the identity content), or null when no id is computed. */ 175 public byte[] getFramed() { 176 return framed == null ? null : framed.clone(); 177 } 178 179 public String getFramedHex() { 180 return framed == null ? null : Framing.hex(framed); 181 } 182 183 public String getSha256Hex() { 184 return sha256Hex; 185 } 186 187 /** The id ({@code ep1-…}), the bare digest for {@code MetadataDigestV1}, or null. */ 188 public String getId() { 189 return id; 190 } 191 192 /** {@code NumericKeyV1} of the id, or null. */ 193 public String getNumericKey() { 194 return numericKey; 195 } 196 } 197 198 /** The status and the fields an algorithm frames; null fields when no id is computed. */ 199 private static final class Outcome { 200 final String status; 201 final List<Object> fields; 202 203 Outcome(String status, List<Object> fields) { 204 this.status = status; 205 this.fields = fields; 206 } 207 } 208 209 private static Outcome portable(Object... fields) { 210 return new Outcome(PORTABLE, Arrays.asList(fields)); 211 } 212 213 private static Outcome noId(String status) { 214 return new Outcome(status, null); 215 } 216 217 /** 218 * Runs one algorithm on its inputs. 219 * 220 * @param algorithm one of the names of Section 2, such as {@code EndpointIdV1} 221 * @throws SqllrException with an {@code ID_*} code (or a {@code SCHEMA_*} code for JSON that canonical JSON 222 * cannot carry) when the inputs are rejected 223 */ 224 public static IdResult compute(String algorithm, Object inputs) { 225 Outcome outcome; 226 if (SOURCE_UNIT.equals(algorithm)) { 227 outcome = sourceUnitId(inputs); 228 } else if (ENDPOINT.equals(algorithm)) { 229 outcome = endpointId(inputs); 230 } else if (FACT.equals(algorithm)) { 231 outcome = factId(inputs); 232 } else if (OCCURRENCE_KEY.equals(algorithm)) { 233 outcome = occurrenceKey(inputs); 234 } else if (ANALYSIS_CONTEXT.equals(algorithm)) { 235 outcome = analysisContext(inputs); 236 } else if (OCCURRENCE_RECORD.equals(algorithm)) { 237 Map<?, ?> in = members(inputs, list("occurrenceKey", "analysisContext"), NONE, "OccurrenceRecordIdV1 input"); 238 outcome = portable(ref(in.get("occurrenceKey"), "occk1", "occurrenceKey"), 239 ref(in.get("analysisContext"), "actx1", "analysisContext")); 240 } else if (EXPLAIN_NODE_KEY.equals(algorithm)) { 241 outcome = explainNodeKey(inputs); 242 } else if (EXPLAIN_NODE_RECORD.equals(algorithm)) { 243 Map<?, ?> in = members(inputs, list("nodeKey", "analysisContext"), NONE, "ExplainNodeRecordIdV1 input"); 244 outcome = portable(ref(in.get("nodeKey"), "xnk1", "nodeKey"), 245 ref(in.get("analysisContext"), "actx1", "analysisContext")); 246 } else if (DEPENDENCY_SNAPSHOT.equals(algorithm)) { 247 Map<?, ?> in = members(inputs, list("manifest"), NONE, "DependencySnapshotIdV1 input"); 248 if (!(in.get("manifest") instanceof Map<?, ?>)) { 249 throw invalid("manifest must be an object"); 250 } 251 outcome = portable(CanonicalJson.jcsText(in.get("manifest"))); 252 } else if (METADATA_DIGEST.equals(algorithm)) { 253 outcome = metadataDigest(inputs); 254 } else { 255 throw invalid("unknown algorithm " + algorithm); 256 } 257 if (outcome.fields == null) { 258 return new IdResult(outcome.status, null, null, null, null); 259 } 260 return mint(algorithm, outcome.status, outcome.fields); 261 } 262 263 private static IdResult mint(String algorithm, String status, List<Object> fields) { 264 ByteArrayOutputStream out = new ByteArrayOutputStream(); 265 Framing.frameInto("SQL-LR/" + algorithm, out); 266 for (Object field : fields) { 267 Framing.frameInto(field, out); 268 } 269 byte[] framed = out.toByteArray(); 270 String digest = Framing.hex(sha256(framed)); 271 if (METADATA_DIGEST.equals(algorithm)) { 272 return new IdResult(status, framed, digest, digest, null); 273 } 274 return new IdResult(status, framed, digest, PREFIXES.get(algorithm) + "-" + digest, digest.substring(0, 32)); 275 } 276 277 static byte[] sha256(byte[] data) { 278 try { 279 return MessageDigest.getInstance("SHA-256").digest(data); 280 } catch (NoSuchAlgorithmException e) { 281 throw new IllegalStateException(e); 282 } 283 } 284 285 /** 286 * {@code NumericKeyV1} (Section 5.10): the first 16 bytes of the digest of an id, as 32 lowercase 287 * hexadecimal characters. 288 * 289 * @throws SqllrException {@code ID_INVALID_REFERENCE} for a string that is not an id of the family 290 */ 291 public static String numericKey(Object id) { 292 if (!(id instanceof String) || !ID.matcher((String) id).matches()) { 293 throw new SqllrException("ID_INVALID_REFERENCE", "not a PUBLIC-LINEAGE-ID-V1 id: " + id); 294 } 295 String text = (String) id; 296 return text.substring(text.indexOf('-') + 1, text.indexOf('-') + 33); 297 } 298 299 /** 300 * The importer rule of Section 5.10: two different ids with one numeric key are a fatal collision. 301 * 302 * @throws SqllrException {@code ID_NUMERIC_KEY_COLLISION} 303 */ 304 public static void checkNumericKeys(Collection<?> ids) { 305 Map<String, Object> seen = new HashMap<String, Object>(); 306 for (Object id : ids) { 307 String key = numericKey(id); 308 Object earlier = seen.get(key); 309 if (earlier != null && !earlier.equals(id)) { 310 throw new SqllrException("ID_NUMERIC_KEY_COLLISION", earlier + " and " + id + " share " + key); 311 } 312 seen.put(key, id); 313 } 314 } 315 316 /** True for a well-formed id with this prefix. */ 317 public static boolean isId(Object value, String prefix) { 318 return value instanceof String && ID.matcher((String) value).matches() && ((String) value).startsWith(prefix + "-"); 319 } 320 321 // --------------------------------------------------------------------------------------------- 322 // Input checks, in the reference's order 323 // --------------------------------------------------------------------------------------------- 324 325 private static final List<String> NONE = Collections.emptyList(); 326 327 private static SqllrException invalid(String detail) { 328 return new SqllrException("ID_INVALID_INPUT", detail); 329 } 330 331 /** The members of an input object: required ones present, no unknown ones ({@code evidence} and {@code note} allowed). */ 332 private static Map<?, ?> members(Object value, List<String> required, List<String> optional, String what) { 333 if (!(value instanceof Map<?, ?>)) { 334 throw invalid(what + " must be an object"); 335 } 336 Map<?, ?> obj = (Map<?, ?>) value; 337 for (String name : required) { 338 if (!obj.containsKey(name)) { 339 throw invalid(what + " lacks member " + name); 340 } 341 } 342 for (Object name : obj.keySet()) { 343 if (!required.contains(name) && !optional.contains(name) && !"evidence".equals(name) && !"note".equals(name)) { 344 throw invalid(what + " has unknown member " + name); 345 } 346 } 347 return obj; 348 } 349 350 private static String text(Map<?, ?> obj, String name) { 351 return text(obj, name, false, false); 352 } 353 354 private static String text(Map<?, ?> obj, String name, boolean allowEmpty, boolean nullable) { 355 Object value = obj.get(name); 356 if (value == null && nullable) { 357 return null; 358 } 359 if (!(value instanceof String)) { 360 throw invalid(name + " must be a string"); 361 } 362 Framing.utf8((String) value); 363 if (((String) value).isEmpty() && !allowEmpty) { 364 throw new SqllrException("ID_EMPTY_NAME", name + " must not be empty"); 365 } 366 return (String) value; 367 } 368 369 /** An integer in 0..2^53-1, or null when {@code value} is not one. */ 370 private static Long smallInteger(Object value) { 371 if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) { 372 long v = ((Number) value).longValue(); 373 return v >= 0 && v <= MAX_ORDINAL ? Long.valueOf(v) : null; 374 } 375 if (value instanceof BigInteger) { 376 BigInteger big = (BigInteger) value; 377 return big.signum() >= 0 && big.compareTo(BigInteger.valueOf(MAX_ORDINAL)) <= 0 ? Long.valueOf(big.longValue()) : null; 378 } 379 return null; 380 } 381 382 private static Long ordinal(Map<?, ?> obj, String name) { 383 Long value = smallInteger(obj.get(name)); 384 if (value == null) { 385 throw invalid(name + " must be an integer in 0..2^53-1"); 386 } 387 return value; 388 } 389 390 private static Code code(Map<?, ?> obj, String name, List<String> allowed) { 391 Code value = Code.of(obj.get(name)); 392 if (allowed != null && !allowed.contains(value.value())) { 393 throw new SqllrException("ID_INVALID_CODE", name + ": " + value.value() + " is not one of " + allowed); 394 } 395 return value; 396 } 397 398 /** An id-valued input, framed as the string of its full text (Section 3.6). */ 399 private static String ref(Object value, String prefix, String what) { 400 if (isId(value, prefix)) { 401 return (String) value; 402 } 403 throw new SqllrException("ID_INVALID_REFERENCE", what + " must be a " + prefix + "- id"); 404 } 405 406 /** A reference given as {@code {"status": <non-portable status>}} instead of an id, or null. */ 407 private static String statusOf(Object value) { 408 if (value instanceof Map<?, ?>) { 409 Map<?, ?> obj = members(value, list("status"), NONE, "non-portable reference"); 410 Object status = obj.get("status"); 411 if (!(status instanceof String) || !NON_PORTABLE.contains(status)) { 412 throw invalid("unknown identity status " + status); 413 } 414 return (String) status; 415 } 416 return null; 417 } 418 419 private static String worst(List<String> statuses) { 420 String out = null; 421 for (String status : statuses) { 422 if (out == null || STATUS_RANK.get(status) > STATUS_RANK.get(out)) { 423 out = status; 424 } 425 } 426 return out == null ? PORTABLE : out; 427 } 428 429 private static List<Object> pathScope(Object value) { 430 Map<?, ?> obj = members(value, list("profile", "producer"), NONE, "pathScope"); 431 String profile = text(obj, "profile"); 432 String producer = text(obj, "producer", false, true); 433 if (PATH_PROFILE_V1.equals(profile)) { 434 if (producer != null) { 435 throw new SqllrException("ID_INVALID_PATH_SCOPE", "producer must be null for " + PATH_PROFILE_V1); 436 } 437 } else if (producer == null) { 438 throw new SqllrException("ID_INVALID_PATH_SCOPE", "a producer path profile needs the producer name"); 439 } 440 return Arrays.<Object>asList(profile, producer); 441 } 442 443 private static String semanticPath(List<Object> scope, Object value) { 444 if (!(value instanceof String) || ((String) value).isEmpty()) { 445 throw new SqllrException("ID_INVALID_PATH", "path must be a non-empty string"); 446 } 447 String path = (String) value; 448 Framing.utf8(path); 449 if (PATH_PROFILE_V1.equals(scope.get(0))) { 450 String[] steps = path.split("/", -1); 451 for (String step : steps) { 452 if (!PATH_STEP.matcher(step).matches()) { 453 throw new SqllrException("ID_INVALID_PATH", path + " is not a " + PATH_PROFILE_V1 + " path"); 454 } 455 } 456 for (String step : steps) { 457 if (!PATH_STEP_CODES.contains(step.substring(0, step.indexOf('[')))) { 458 throw new SqllrException("ID_INVALID_PATH", step + " is not a step code of " + PATH_PROFILE_V1); 459 } 460 } 461 } 462 return path; 463 } 464 465 private static List<Object> contentDigest(Object value) { 466 Map<?, ?> obj = members(value, list("domain", "value"), list("tokenDomain"), "content digest"); 467 Code domain = code(obj, "domain", DIGEST_DOMAINS); 468 if ("KEYED_TOKEN".equals(domain.value())) { 469 return Arrays.<Object>asList(domain, text(obj, "tokenDomain"), text(obj, "value")); 470 } 471 if (obj.get("tokenDomain") != null) { 472 throw new SqllrException("ID_INVALID_DIGEST", "tokenDomain is allowed only for KEYED_TOKEN"); 473 } 474 Object digest = obj.get("value"); 475 if (!(digest instanceof String) || !HEX64.matcher((String) digest).matches()) { 476 throw new SqllrException("ID_INVALID_DIGEST", "a SHA-256 digest is 64 lowercase hex characters"); 477 } 478 return Arrays.<Object>asList(domain, null, digest); 479 } 480 481 private static String hex64(Map<?, ?> obj, String name, boolean nullable) { 482 Object value = obj.get(name); 483 if (value == null && nullable) { 484 return null; 485 } 486 if (!(value instanceof String) || !HEX64.matcher((String) value).matches()) { 487 throw new SqllrException("ID_INVALID_DIGEST", name + " must be 64 lowercase hex characters"); 488 } 489 return (String) value; 490 } 491 492 /** Framed name segments and the statuses that prevent an id (Section 3.9). */ 493 private static final class Segments { 494 final List<Object> framed = new ArrayList<Object>(); 495 final List<String> blocked = new ArrayList<String>(); 496 } 497 498 private static Segments nameSegments(Object value) { 499 if (!(value instanceof List<?>) || ((List<?>) value).size() < 2) { 500 throw invalid("a full name has an AUTHORITY segment and an object segment"); 501 } 502 List<?> items = (List<?>) value; 503 Segments out = new Segments(); 504 for (int position = 0; position < items.size(); position++) { 505 Object item = items.get(position); 506 Object kind = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("kind") : null; 507 if (!"NAMED".equals(kind) && !"PRODUCER_DEFAULT".equals(kind)) { 508 throw invalid("segment.kind must be NAMED or PRODUCER_DEFAULT"); 509 } 510 Map<?, ?> seg = (Map<?, ?>) item; 511 Code role = code(seg, "role", null); 512 if ("AUTHORITY".equals(role.value()) != (position == 0)) { 513 throw invalid("AUTHORITY is the first segment and only the first"); 514 } 515 if ("PRODUCER_DEFAULT".equals(kind)) { 516 if (seg.containsKey("canonicalKey") || seg.containsKey("keyUnavailable")) { 517 throw new SqllrException("ID_INVALID_DEFAULT_SEGMENT", "a PRODUCER_DEFAULT segment has no key"); 518 } 519 if (position == items.size() - 1) { 520 throw invalid("the object segment is never defaulted"); 521 } 522 members(seg, list("role", "kind"), NONE, "segment"); 523 out.framed.add(Arrays.<Object>asList(role, Code.of("PRODUCER_DEFAULT"), null)); 524 continue; 525 } 526 if (seg.containsKey("keyUnavailable")) { 527 members(seg, list("role", "kind", "keyUnavailable"), NONE, "segment"); 528 Object reason = seg.get("keyUnavailable"); 529 if (!KEY_UNAVAILABLE.containsKey(reason) || position == 0) { 530 throw invalid("keyUnavailable must be COLLATION or MIXED_PROFILE"); 531 } 532 out.blocked.add(KEY_UNAVAILABLE.get(reason)); 533 continue; 534 } 535 members(seg, list("role", "kind", "canonicalKey"), NONE, "segment"); 536 // For AUTHORITY the key is the caller-declared name, byte for byte (Section 3.9). 537 out.framed.add(Arrays.<Object>asList(role, Code.of("NAMED"), text(seg, "canonicalKey"))); 538 } 539 return out; 540 } 541 542 /** A field path (outermost first, starting with {@code FIELD}) and the statuses that prevent an id. */ 543 private static Segments fieldPath(Object value) { 544 if (!(value instanceof List<?>) || ((List<?>) value).isEmpty()) { 545 throw new SqllrException("ID_INVALID_FIELD_PATH", "a field path has at least one segment"); 546 } 547 List<?> items = (List<?>) value; 548 Segments out = new Segments(); 549 for (int position = 0; position < items.size(); position++) { 550 Object item = items.get(position); 551 Object kindValue = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("kind") : null; 552 if (!(kindValue instanceof String) || !PATH_SEGMENT_KINDS.contains(kindValue)) { 553 throw new SqllrException("ID_INVALID_FIELD_PATH", "unknown path segment kind"); 554 } 555 Map<?, ?> seg = (Map<?, ?>) item; 556 Code kind = Code.of(kindValue); 557 if (position == 0 && !"FIELD".equals(kind.value())) { 558 throw new SqllrException("ID_INVALID_FIELD_PATH", "the first path segment must be FIELD"); 559 } 560 if ("FIELD".equals(kind.value())) { 561 if (seg.containsKey("keyUnavailable")) { 562 members(seg, list("kind", "keyUnavailable"), NONE, "path segment"); 563 Object reason = seg.get("keyUnavailable"); 564 if (!KEY_UNAVAILABLE.containsKey(reason)) { 565 throw invalid("keyUnavailable must be COLLATION or MIXED_PROFILE"); 566 } 567 out.blocked.add(KEY_UNAVAILABLE.get(reason)); 568 continue; 569 } 570 members(seg, list("kind", "canonicalKey"), NONE, "path segment"); 571 // A column key is never empty; a nested key of semi-structured data may be. 572 out.framed.add(Arrays.<Object>asList(kind, text(seg, "canonicalKey", position > 0, false))); 573 } else if ("INDEX".equals(kind.value())) { 574 members(seg, list("kind", "index"), NONE, "path segment"); 575 Long index = smallInteger(seg.get("index")); 576 if (index == null) { 577 throw new SqllrException("ID_INVALID_FIELD_PATH", "INDEX is a 0-based integer"); 578 } 579 out.framed.add(Arrays.<Object>asList(kind, index)); 580 } else { 581 members(seg, list("kind"), NONE, "path segment"); 582 out.framed.add(Arrays.<Object>asList(kind, null)); 583 } 584 } 585 return out; 586 } 587 588 private static List<Object> routineSignature(Object value) { 589 Map<?, ?> obj = members(value, list("form"), list("parameters", "overloadId"), "signature"); 590 Code form = code(obj, "form", SIGNATURE_FORMS); 591 if ("NOT_OVERLOADABLE".equals(form.value())) { 592 members(obj, list("form"), NONE, "signature"); 593 return Arrays.<Object>asList(form, null); 594 } 595 if ("CATALOG_OVERLOAD_ID".equals(form.value())) { 596 members(obj, list("form", "overloadId"), NONE, "signature"); 597 return Arrays.<Object>asList(form, text(obj, "overloadId")); 598 } 599 members(obj, list("form", "parameters"), NONE, "signature"); 600 Object params = obj.get("parameters"); 601 if (!(params instanceof List<?>)) { 602 throw new SqllrException("ID_INVALID_SIGNATURE", "parameters must be a list (possibly empty)"); 603 } 604 List<Object> framed = new ArrayList<Object>(); 605 for (Object param : (List<?>) params) { 606 Map<?, ?> p = members(param, list("mode", "canonicalTypeKey"), NONE, "parameter"); 607 framed.add(Arrays.<Object>asList(code(p, "mode", PARAMETER_MODES), text(p, "canonicalTypeKey"))); 608 } 609 return Arrays.<Object>asList(form, framed); 610 } 611 612 // --------------------------------------------------------------------------------------------- 613 // The algorithms (Section 5) 614 // --------------------------------------------------------------------------------------------- 615 616 private static Outcome sourceUnitId(Object value) { 617 Object kind = value instanceof Map<?, ?> ? ((Map<?, ?>) value).get("kind") : null; 618 if ("LOCAL".equals(kind)) { 619 members(value, list("kind"), NONE, "SourceUnitIdV1 input"); 620 return noId(LOCAL); 621 } 622 if ("NAMED".equals(kind)) { 623 Map<?, ?> in = members(value, list("kind", "namespace", "logicalName"), NONE, "SourceUnitIdV1 input"); 624 return portable(Code.of("NAMED"), text(in, "namespace"), text(in, "logicalName")); 625 } 626 if ("CONTENT_ADDRESSED".equals(kind)) { 627 Map<?, ?> in = members(value, list("kind", "contextDomain", "contentDigest"), NONE, "SourceUnitIdV1 input"); 628 Code code = Code.of("CONTENT_ADDRESSED"); 629 String domain = text(in, "contextDomain"); 630 return portable(code, domain, contentDigest(in.get("contentDigest"))); 631 } 632 if ("DERIVED".equals(kind)) { 633 Map<?, ?> in = members(value, list("kind", "parentUnit", "statementOrdinal", "pathScope", "sitePath", 634 "siteOrdinal", "variantOrdinal"), NONE, "SourceUnitIdV1 input"); 635 String parentStatus = statusOf(in.get("parentUnit")); 636 if (parentStatus != null) { 637 return noId(worst(Arrays.asList(LOCAL, parentStatus))); 638 } 639 List<Object> scope = pathScope(in.get("pathScope")); 640 String parent = ref(in.get("parentUnit"), "su1", "parentUnit"); 641 Long statement = ordinal(in, "statementOrdinal"); 642 String sitePath = semanticPath(scope, in.get("sitePath")); 643 Long site = ordinal(in, "siteOrdinal"); 644 Long variant = ordinal(in, "variantOrdinal"); 645 return portable(Code.of("DERIVED"), parent, statement, scope, sitePath, site, variant); 646 } 647 throw invalid("SourceUnitIdV1 kind must be NAMED, CONTENT_ADDRESSED, DERIVED or LOCAL"); 648 } 649 650 private static Outcome endpointId(Object value) { 651 if (!(value instanceof Map<?, ?>)) { 652 throw invalid("EndpointIdV1 input must be an object"); 653 } 654 Map<?, ?> in = (Map<?, ?>) value; 655 Object endpointKind = in.get("endpointKind"); 656 if (!(endpointKind instanceof String) || !IDENTITY_CLASS.containsKey(endpointKind)) { 657 throw new SqllrException("ID_INVALID_CODE", "unknown endpointKind " + endpointKind); 658 } 659 if (in.containsKey("opaque")) { 660 members(in, list("endpointKind", "opaque"), NONE, "EndpointIdV1 input"); 661 members(in.get("opaque"), list("issuer", "algorithmId", "policyId", "comparisonDomain", "value"), NONE, 662 "opaque identity"); 663 return noId(OPAQUE_PRODUCER_LOCAL); 664 } 665 String kind = IDENTITY_CLASS.get(endpointKind); 666 if (kind == null) { 667 members(in, list("endpointKind"), NONE, "EndpointIdV1 input"); 668 return noId(LOCAL); 669 } 670 if (in.containsKey("qualificationSatisfied") && !Boolean.TRUE.equals(in.get("qualificationSatisfied"))) { 671 return noId(LOCAL); 672 } 673 if ("RELATION".equals(kind)) { 674 members(in, list("endpointKind", "segments"), list("qualificationSatisfied"), "EndpointIdV1 RELATION input"); 675 Segments segments = nameSegments(in.get("segments")); 676 if (!segments.blocked.isEmpty()) { 677 return noId(worst(segments.blocked)); 678 } 679 return portable(Code.of("RELATION"), segments.framed); 680 } 681 if ("FIELD".equals(kind) || "ROW_SET".equals(kind)) { 682 List<String> required = "FIELD".equals(kind) ? list("endpointKind", "owner", "path") : list("endpointKind", "owner"); 683 members(in, required, list("qualificationSatisfied"), "EndpointIdV1 " + kind + " input"); 684 List<String> blocked = new ArrayList<String>(); 685 String ownerStatus = statusOf(in.get("owner")); 686 if (ownerStatus != null) { 687 blocked.add(ownerStatus); 688 } 689 if ("ROW_SET".equals(kind)) { 690 if (!blocked.isEmpty()) { 691 return noId(worst(blocked)); 692 } 693 return portable(Code.of("ROW_SET"), ref(in.get("owner"), "ep1", "owner")); 694 } 695 Segments path = fieldPath(in.get("path")); 696 blocked.addAll(path.blocked); 697 if (!blocked.isEmpty()) { 698 return noId(worst(blocked)); 699 } 700 return portable(Code.of("FIELD"), ref(in.get("owner"), "ep1", "owner"), path.framed); 701 } 702 members(in, list("endpointKind", "routineNamespace", "routineKind", "segments"), 703 list("signature", "qualificationSatisfied"), "EndpointIdV1 ROUTINE input"); 704 Code namespace = code(in, "routineNamespace", ROUTINE_NAMESPACES); 705 Code routineKind = code(in, "routineKind", ROUTINE_KINDS); 706 Segments segments = nameSegments(in.get("segments")); 707 if (in.get("signature") == null) { 708 segments.blocked.add(LOCAL); // the overload is not proven (P1-3) 709 } 710 if (!segments.blocked.isEmpty()) { 711 return noId(worst(segments.blocked)); 712 } 713 return portable(Code.of("ROUTINE"), namespace, routineKind, segments.framed, routineSignature(in.get("signature"))); 714 } 715 716 private static Outcome factId(Object value) { 717 Map<?, ?> in = members(value, list("kind", "source", "target"), list("role"), "FactIdV1 input"); 718 Code kind = code(in, "kind", FACT_KINDS); 719 Object role = in.get("role"); 720 if ("ROW".equals(kind.value())) { 721 if (role == null) { 722 throw new SqllrException("ID_INVALID_ROLE", "a ROW Fact carries a role"); 723 } 724 role = code(in, "role", ROW_ROLES); 725 } else if (role != null) { 726 throw new SqllrException("ID_INVALID_ROLE", "only a ROW Fact carries a role"); 727 } 728 String sourceStatus = statusOf(in.get("source")); 729 String targetStatus = statusOf(in.get("target")); 730 if (sourceStatus != null || targetStatus != null) { 731 return noId(LOCAL); 732 } 733 String source = ref(in.get("source"), "ep1", "source"); 734 String target = ref(in.get("target"), "ep1", "target"); 735 return portable(kind, source, target, role); 736 } 737 738 /** A side of a FactCandidate subject, or null when a member has no {@code EndpointIdV1}. */ 739 private static List<Object> candidateSide(Object value) { 740 Map<?, ?> obj = members(value, NONE, list("endpoint", "candidates", "membership"), "candidate side"); 741 if (obj.containsKey("endpoint") == obj.containsKey("candidates")) { 742 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a side has either endpoint or candidates"); 743 } 744 if (obj.containsKey("endpoint")) { 745 if (obj.containsKey("membership")) { 746 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "membership belongs to a candidate set"); 747 } 748 if (statusOf(obj.get("endpoint")) != null) { 749 return null; 750 } 751 return Arrays.<Object>asList(Code.of("ENDPOINT"), ref(obj.get("endpoint"), "ep1", "endpoint")); 752 } 753 if (!obj.containsKey("membership")) { 754 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate set declares its membership"); 755 } 756 Code membership = code(obj, "membership", CANDIDATE_SET_MEMBERSHIP); 757 Object items = obj.get("candidates"); 758 if (!(items instanceof List<?>)) { 759 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "candidates must be a list"); 760 } 761 List<?> candidates = (List<?>) items; 762 if ("COMPLETE".equals(membership.value()) && candidates.size() < 2) { 763 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a COMPLETE candidate set has at least two members"); 764 } 765 for (Object item : candidates) { 766 if (statusOf(item) != null) { 767 return null; 768 } 769 } 770 List<String> ids = new ArrayList<String>(); 771 for (Object item : candidates) { 772 ids.add(ref(item, "ep1", "candidate")); 773 } 774 if (new HashSet<String>(ids).size() != ids.size()) { 775 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate set has no duplicates"); 776 } 777 Collections.sort(ids, UTF8_ORDER); 778 return Arrays.<Object>asList(Code.of("CANDIDATE_SET"), membership, new ArrayList<Object>(ids)); 779 } 780 781 /** Unsigned UTF-8 byte order of strings, which is code point order. */ 782 static final Comparator<String> UTF8_ORDER = new Comparator<String>() { 783 @Override 784 public int compare(String a, String b) { 785 return CanonicalJson.compareUnsigned(Framing.utf8(a), Framing.utf8(b)); 786 } 787 }; 788 789 private static List<Object> occurrenceSubject(Object value) { 790 Object kind = value instanceof Map<?, ?> ? ((Map<?, ?>) value).get("kind") : null; 791 if ("FACT".equals(kind)) { 792 Map<?, ?> obj = members(value, list("kind", "fact"), NONE, "subject"); 793 if (statusOf(obj.get("fact")) != null) { 794 return null; 795 } 796 return Arrays.<Object>asList(Code.of("FACT"), ref(obj.get("fact"), "fact1", "fact")); 797 } 798 if ("CANDIDATE".equals(kind)) { 799 Map<?, ?> obj = members(value, list("kind", "factKind", "source", "target"), list("role"), "subject"); 800 Code factKind = code(obj, "factKind", FACT_KINDS); 801 Object role = obj.get("role"); 802 if ("ROW".equals(factKind.value())) { 803 if (role == null) { 804 throw new SqllrException("ID_INVALID_ROLE", "a ROW candidate carries a role"); 805 } 806 role = code(obj, "role", ROW_ROLES); 807 } else if (role != null) { 808 throw new SqllrException("ID_INVALID_ROLE", "only a ROW candidate carries a role"); 809 } 810 if (!(obj.get("source") instanceof Map<?, ?>) || !(obj.get("target") instanceof Map<?, ?>)) { 811 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate side must be an object"); 812 } 813 if (!((Map<?, ?>) obj.get("source")).containsKey("candidates") 814 && !((Map<?, ?>) obj.get("target")).containsKey("candidates")) { 815 throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate subject has at least one candidate set"); 816 } 817 List<Object> source = candidateSide(obj.get("source")); 818 List<Object> target = candidateSide(obj.get("target")); 819 if (source == null || target == null) { 820 return null; 821 } 822 return Arrays.<Object>asList(Code.of("CANDIDATE"), factKind, role, source, target); 823 } 824 throw invalid("subject.kind must be FACT or CANDIDATE"); 825 } 826 827 private static Outcome occurrenceKey(Object value) { 828 Map<?, ?> in = members(value, list("sourceUnit", "statementOrdinal", "subject", "pathScope", "path", "ordinal"), 829 NONE, "OccurrenceKeyV1 input"); 830 List<Object> scope = pathScope(in.get("pathScope")); 831 String path = semanticPath(scope, in.get("path")); 832 Long statement = ordinal(in, "statementOrdinal"); 833 Long occurrence = ordinal(in, "ordinal"); 834 List<Object> subject = occurrenceSubject(in.get("subject")); 835 if (statusOf(in.get("sourceUnit")) != null || subject == null) { 836 return noId(LOCAL); 837 } 838 return portable(ref(in.get("sourceUnit"), "su1", "sourceUnit"), statement, subject, scope, path, occurrence); 839 } 840 841 private static Outcome metadataDigest(Object value) { 842 Map<?, ?> in = members(value, list("artifacts"), NONE, "MetadataDigestV1 input"); 843 Object artifacts = in.get("artifacts"); 844 if (!(artifacts instanceof List<?>)) { 845 throw invalid("artifacts must be a list"); 846 } 847 List<List<Object>> framed = new ArrayList<List<Object>>(); 848 for (Object artifact : (List<?>) artifacts) { 849 Map<?, ?> a = members(artifact, list("format", "sha256"), NONE, "metadata artifact"); 850 framed.add(Arrays.<Object>asList(text(a, "format"), hex64(a, "sha256", false))); 851 } 852 Collections.sort(framed, new Comparator<List<Object>>() { 853 @Override 854 public int compare(List<Object> x, List<Object> y) { 855 int c = UTF8_ORDER.compare((String) x.get(0), (String) y.get(0)); 856 return c != 0 ? c : UTF8_ORDER.compare((String) x.get(1), (String) y.get(1)); 857 } 858 }); 859 for (int i = 1; i < framed.size(); i++) { 860 if (framed.get(i).equals(framed.get(i - 1))) { 861 throw invalid("a metadata artifact is listed twice"); 862 } 863 } 864 return portable(new ArrayList<Object>(framed)); 865 } 866 867 private static List<Object> capabilityList(Object value) { 868 if (!(value instanceof Map<?, ?>) || ((Map<?, ?>) value).isEmpty()) { 869 throw invalid("capabilities must be a non-empty object"); 870 } 871 Map<?, ?> obj = (Map<?, ?>) value; 872 List<String> features = new ArrayList<String>(); 873 for (Object feature : obj.keySet()) { 874 Framing.utf8((String) feature); 875 features.add((String) feature); 876 } 877 Collections.sort(features, UTF8_ORDER); 878 List<Object> out = new ArrayList<Object>(); 879 for (String feature : features) { 880 Code name = Code.of(feature); 881 if (!ANALYSIS_FEATURES.contains(name.value())) { 882 throw new SqllrException("ID_INVALID_CODE", "f: " + feature + " is not one of " + ANALYSIS_FEATURES); 883 } 884 out.add(Arrays.<Object>asList(name, code(obj, feature, CAPABILITY_STATUSES))); 885 } 886 return out; 887 } 888 889 private static List<Object> decodingList(Object value) { 890 if (value == null) { 891 return null; 892 } 893 Map<?, ?> obj = members(value, list("encoding", "bom", "lineEndings"), NONE, "decoding"); 894 String encoding = text(obj, "encoding"); 895 Code bom = code(obj, "bom", BOM_HANDLING); 896 return Arrays.<Object>asList(encoding, bom, code(obj, "lineEndings", LINE_ENDING_HANDLING)); 897 } 898 899 private static Outcome analysisContext(Object value) { 900 Map<?, ?> in = members(value, list("producerName", "producerVersion", "dependencySnapshot", "dialectId", 901 "dialectVersion", "identifierPolicyId", "pathProfile", "effectiveOptionsDigest", "metadataStatus", 902 "metadataDigest", "redactionPolicyId", "redactionPolicyVersion", "capabilities", "decoding", 903 "sourceContentDigest", "resolutionInputDigests"), list("effectiveOptions", "metadataArtifacts"), 904 "AnalysisContextV1 input"); 905 String optionsDigest = hex64(in, "effectiveOptionsDigest", false); 906 if (in.containsKey("effectiveOptions")) { 907 String computed = Framing.hex(sha256(CanonicalJson.jcsBytes(in.get("effectiveOptions")))); 908 if (!computed.equals(optionsDigest)) { 909 throw new SqllrException("ID_MISMATCH", "effectiveOptionsDigest does not match effectiveOptions"); 910 } 911 } 912 Code status = code(in, "metadataStatus", METADATA_STATUSES); 913 String meta = hex64(in, "metadataDigest", true); 914 if ((meta == null) != "NONE".equals(status.value())) { 915 throw invalid("metadataDigest is null if and only if metadataStatus is NONE"); 916 } 917 if (in.containsKey("metadataArtifacts")) { 918 Map<String, Object> artifacts = new LinkedHashMap<String, Object>(); 919 artifacts.put("artifacts", in.get("metadataArtifacts")); 920 if (!compute(METADATA_DIGEST, artifacts).getId().equals(meta)) { 921 throw new SqllrException("ID_MISMATCH", "metadataDigest does not match metadataArtifacts"); 922 } 923 } 924 Object inputs = in.get("resolutionInputDigests"); 925 if (!(inputs instanceof List<?>)) { 926 throw invalid("resolutionInputDigests must be a list"); 927 } 928 List<List<Object>> digests = new ArrayList<List<Object>>(); 929 for (Object digest : (List<?>) inputs) { 930 digests.add(contentDigest(digest)); 931 } 932 final Map<List<Object>, byte[]> framedDigests = new HashMap<List<Object>, byte[]>(); 933 for (List<Object> digest : digests) { 934 framedDigests.put(digest, Framing.frame(digest)); 935 } 936 Collections.sort(digests, new Comparator<List<Object>>() { 937 @Override 938 public int compare(List<Object> x, List<Object> y) { 939 return CanonicalJson.compareUnsigned(framedDigests.get(x), framedDigests.get(y)); 940 } 941 }); 942 for (int i = 1; i < digests.size(); i++) { 943 if (Arrays.equals(framedDigests.get(digests.get(i)), framedDigests.get(digests.get(i - 1)))) { 944 throw invalid("a resolution input digest is listed twice"); 945 } 946 } 947 String producerName = text(in, "producerName"); 948 String producerVersion = text(in, "producerVersion"); 949 String snapshot = ref(in.get("dependencySnapshot"), "snap1", "dependencySnapshot"); 950 String dialectId = text(in, "dialectId"); 951 String dialectVersion = text(in, "dialectVersion", false, true); 952 String policyId = text(in, "identifierPolicyId"); 953 String pathProfile = text(in, "pathProfile"); 954 String redactionPolicyId = text(in, "redactionPolicyId"); 955 String redactionPolicyVersion = text(in, "redactionPolicyVersion"); 956 List<Object> capabilities = capabilityList(in.get("capabilities")); 957 List<Object> decoding = decodingList(in.get("decoding")); 958 List<Object> source = contentDigest(in.get("sourceContentDigest")); 959 return portable(producerName, producerVersion, snapshot, dialectId, dialectVersion, policyId, pathProfile, 960 optionsDigest, status, meta, redactionPolicyId, redactionPolicyVersion, capabilities, decoding, source, 961 new ArrayList<Object>(digests)); 962 } 963 964 private static Outcome explainNodeKey(Object value) { 965 Map<?, ?> in = members(value, list("sourceUnit", "statementOrdinal", "pathScope", "path", "nodeKind", "ordinal"), 966 NONE, "ExplainNodeKeyV1 input"); 967 List<Object> scope = pathScope(in.get("pathScope")); 968 String path = semanticPath(scope, in.get("path")); 969 Long statement = ordinal(in, "statementOrdinal"); 970 Long occurrence = ordinal(in, "ordinal"); 971 Code nodeKind = code(in, "nodeKind", EXPLAIN_NODE_KINDS); 972 if (statusOf(in.get("sourceUnit")) != null) { 973 return noId(LOCAL); 974 } 975 return portable(ref(in.get("sourceUnit"), "su1", "sourceUnit"), statement, scope, path, nodeKind, occurrence); 976 } 977 978 /** The algorithms in the order of Section 2. */ 979 public static final List<String> ALGORITHMS = list(SOURCE_UNIT, ENDPOINT, FACT, OCCURRENCE_KEY, ANALYSIS_CONTEXT, 980 OCCURRENCE_RECORD, EXPLAIN_NODE_KEY, EXPLAIN_NODE_RECORD, DEPENDENCY_SNAPSHOT, METADATA_DIGEST); 981}