001package gudusoft.gsqlparser.lineage.v2.merge; 002 003import gudusoft.gsqlparser.lineage.v2.json.ArrayRegistry; 004import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson; 005import gudusoft.gsqlparser.lineage.v2.model.Document; 006import gudusoft.gsqlparser.lineage.v2.model.Producer; 007import gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex; 008import gudusoft.gsqlparser.lineage.v2.validation.LineageValidator; 009import gudusoft.gsqlparser.lineage.v2.validation.SchemaValidator; 010import gudusoft.gsqlparser.lineage.v2.validation.SourceTextProvider; 011 012import java.security.MessageDigest; 013import java.security.NoSuchAlgorithmException; 014import java.util.ArrayDeque; 015import java.util.ArrayList; 016import java.util.Arrays; 017import java.util.Collections; 018import java.util.Deque; 019import java.util.HashMap; 020import java.util.HashSet; 021import java.util.Iterator; 022import java.util.LinkedHashMap; 023import java.util.LinkedHashSet; 024import java.util.List; 025import java.util.Map; 026import java.util.Set; 027import java.util.TreeMap; 028import java.util.TreeSet; 029 030import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.intRef; 031import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.items; 032 033/** 034 * The minimal L1 merger of milestone M1 (specification Sections 9 and 15.2; contract: section 7 of 035 * {@code docs/designs/data-lineage/05-sqllr-m1-core-library-plan-zh.md}). 036 * 037 * <p>Each input must be a valid document, and so is the result: a merge that would produce an invalid document, 038 * or that cannot keep what an input asserts, is refused with a {@link MergeException}; nothing is dropped 039 * silently.</p> 040 * 041 * <p>Source units merge by (unit id, analysis context id). The content of such a unit (statements, scopes, 042 * Occurrences, writes, diagnostics, ExplainGraph content, module objects) is carried from one input whose content 043 * includes every other input's, compared with references replaced by identities; a {@code LOCAL} unit is carried 044 * once per input. Endpoints merge by portable identity, Facts by {@code FactIdV1}, Occurrences by record id, 045 * analysis contexts and objects without an identity that no unit owns by their content. References are renumbered 046 * in the order of the inputs.</p> 047 * 048 * <p>The merge module of the specification is not defined yet (Section 20, item 16), so the input digests and the 049 * ledger are returned in the {@link Result}, not written into the document. Because the document cannot yet carry 050 * two endpoints of one identity, a kind difference other than a generic {@code RELATION} refined by a specific 051 * relation kind is refused (Section 9.4 asks to keep both).</p> 052 * 053 * @since 4.2.17 054 */ 055public final class LineageMerger { 056 057 /** The inputs cannot be merged by this merger; nothing was merged. */ 058 public static class MergeException extends RuntimeException { 059 private static final long serialVersionUID = 1L; 060 061 public MergeException(String message) { 062 super(message); 063 } 064 } 065 066 /** One entry of the ledger: an identity whose generic kind an input refined. */ 067 public static final class Conflict { 068 private final String identity; 069 private final String detail; 070 071 Conflict(String identity, String detail) { 072 this.identity = identity; 073 this.detail = detail; 074 } 075 076 /** The identity (an {@code ep1} id). */ 077 public String getIdentity() { 078 return identity; 079 } 080 081 public String getDetail() { 082 return detail; 083 } 084 085 @Override 086 public String toString() { 087 return identity + ": " + detail; 088 } 089 } 090 091 /** The merged document and what the merge module would carry. */ 092 public static final class Result { 093 private final Document document; 094 private final List<String> inputDigests; 095 private final List<Conflict> conflicts; 096 private final List<String> notes; 097 098 Result(Document document, List<String> inputDigests, List<Conflict> conflicts, List<String> notes) { 099 this.document = document; 100 this.inputDigests = Collections.unmodifiableList(inputDigests); 101 this.conflicts = Collections.unmodifiableList(conflicts); 102 this.notes = Collections.unmodifiableList(notes); 103 } 104 105 public Document getDocument() { 106 return document; 107 } 108 109 /** The sha256 of each distinct input's canonical bytes, in the order the inputs were first given. */ 110 public List<String> getInputDigests() { 111 return inputDigests; 112 } 113 114 /** The ledger: identities whose generic kind an input refined. */ 115 public List<Conflict> getConflicts() { 116 return conflicts; 117 } 118 119 public List<String> getNotes() { 120 return notes; 121 } 122 } 123 124 private static final List<String> LEVELS = Arrays.asList("L1", "L2", "L3"); 125 /** The specific endpoint kinds of identity class RELATION (identity-v1, Section 5.2.1). */ 126 private static final Set<String> RELATION_KINDS = new HashSet<String>(Arrays.asList("TABLE", "VIEW", 127 "MATERIALIZED_VIEW", "EXTERNAL_TABLE", "SYNONYM")); 128 /** The root members the merger carries, besides the module arrays. */ 129 private static final Set<String> ROOT = new HashSet<String>(Arrays.asList("specUri", "specVersion", 130 "declaredLevel", "modules", "header", "analysisContexts", "sourceUnits", "statements", "scopes", "endpoints", 131 "facts", "factCandidates", "candidateSets", "occurrences", "targetFieldOccurrences", "analysisCompleteness", 132 "featureCompleteness", "servingCompleteness", "diagnostics", "explainGraph")); 133 private static final Set<String> GRAPH = new HashSet<String>(Arrays.asList("completeness", "statementCompleteness", 134 "nodes", "microEdges", "candidateSets", "supportPaths")); 135 /** The collections of the output, by kind of object. */ 136 private static final String[][] COLLECTIONS = { 137 {"analysisContext", "analysisContexts"}, {"sourceUnit", "sourceUnits"}, {"statement", "statements"}, 138 {"scope", "scopes"}, {"endpoint", "endpoints"}, {"fact", "facts"}, {"factCandidate", "factCandidates"}, 139 {"rootCandidateSet", "candidateSets"}, {"occurrence", "occurrences"}, {"tfo", "targetFieldOccurrences"}, {"diagnostic", "diagnostics"}, 140 {"node", "nodes"}, {"microEdge", "microEdges"}, {"candidateSet", "candidateSets"}, 141 {"supportPath", "supportPaths"}}; 142 private static final Set<String> GRAPH_KINDS = new HashSet<String>(Arrays.asList("node", "microEdge", 143 "candidateSet", "supportPath")); 144 /** Members that modules add to objects of the core (registry group modules); a carrier may add them. */ 145 private static final Set<String> MODULE_MEMBERS_OF_OBJECTS = new HashSet<String>(Semantics.MODULE_ADDED); 146 /** The kinds whose objects belong to the unit of their statement or source unit. */ 147 private static final Set<String> UNIT_CONTENT = new HashSet<String>(Arrays.asList("statement", "scope", 148 "occurrence", "tfo", "diagnostic", "node", "microEdge", "endpoint", "rootCandidateSet", "supportPath", 149 "candidateSet")); 150 151 private final Producer producer; 152 private final String createdAt; 153 private final LineageValidator validator; 154 155 /** 156 * @param producer the producer of the merged document: the merger; must be a valid {@code Producer} 157 * @param createdAt the {@code header.createdAt} of the merged document, or null for none; never read from a clock 158 */ 159 public LineageMerger(Producer producer, String createdAt) { 160 this(producer, createdAt, LineageValidator.standard(SourceTextProvider.NONE)); 161 } 162 163 public LineageMerger(Producer producer, String createdAt, LineageValidator validator) { 164 if (producer == null || !SchemaValidator.lineageDocument().isValid(producer.toJson(), "Producer")) { 165 throw new IllegalArgumentException("the merged document names the merger as its producer, which must be" 166 + " a valid Producer: " + producer); 167 } 168 this.producer = producer; 169 this.createdAt = createdAt; 170 this.validator = validator; 171 } 172 173 public Result merge(List<Document> inputs) { 174 if (inputs == null || inputs.isEmpty()) { 175 throw new MergeException("nothing to merge"); 176 } 177 List<byte[]> distinct = new ArrayList<byte[]>(); 178 List<String> digests = new ArrayList<String>(); 179 for (Document input : inputs) { 180 byte[] bytes = input.toBytes(); 181 String digest = sha256(bytes); 182 if (!digests.contains(digest)) { 183 digests.add(digest); 184 distinct.add(bytes); 185 } 186 } 187 for (int i = 0; i < distinct.size(); i++) { 188 LineageValidator.Result check = validator.validate(distinct.get(i)); 189 if (!check.isValid()) { 190 throw new MergeException("input " + i + " is not valid: " + check.getStage() + " " + check.getCodes()); 191 } 192 } 193 Result result = new Run(distinct).run(digests); 194 LineageValidator.Result check = validator.validate(result.getDocument().toBytes()); 195 if (!check.isValid()) { 196 throw new MergeException("the merged document would not be valid: " + check.getStage() + " " 197 + check.getFindings()); 198 } 199 return result; 200 } 201 202 static String sha256(byte[] data) { 203 try { 204 StringBuilder out = new StringBuilder(); 205 for (byte b : MessageDigest.getInstance("SHA-256").digest(data)) { 206 out.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16)); 207 } 208 return out.toString(); 209 } catch (NoSuchAlgorithmException e) { 210 throw new IllegalStateException(e); 211 } 212 } 213 214 /** Analysis completeness of a merge (Section 4.4): one status when all agree, otherwise PARTIAL. */ 215 static Object combined(Object a, Object b) { 216 return a == null ? b : (a.equals(b) ? a : "PARTIAL"); 217 } 218 219 // --------------------------------------------------------------------------------------------- 220 221 private final class Run { 222 private final List<byte[]> bytes; 223 private final List<Map<String, Object>> docs = new ArrayList<Map<String, Object>>(); 224 private final List<Semantics> semantics = new ArrayList<Semantics>(); 225 private final List<DocumentIndex> indexes = new ArrayList<DocumentIndex>(); 226 private final List<Map<Long, Long>> remaps = new ArrayList<Map<Long, Long>>(); 227 /** For each input, the ref of every keyed object by key, and of every object by kind and form. */ 228 private final List<Map<String, Long>> keyed = new ArrayList<Map<String, Long>>(); 229 private final List<Map<String, Long>> byForm = new ArrayList<Map<String, Long>>(); 230 /** For each input, the refs of the candidate sets of its ExplainGraph (not those inside FactCandidates). */ 231 private final List<Set<Long>> graphCsets = new ArrayList<Set<Long>>(); 232 /** For each input, the FactCandidate and end ({@code source}, {@code target}) of each embedded candidate set. */ 233 private final List<Map<Long, Object[]>> embeddedIn = new ArrayList<Map<Long, Object[]>>(); 234 /** The new ref of the candidate set at each end of each carried FactCandidate ("new FactCandidate ref|end"). */ 235 private final Map<String, Long> embeddedSlots = new HashMap<String, Long>(); 236 /** The input that carries the content of each (unit id, analysis context id). */ 237 private final Map<String, Integer> carrier = new HashMap<String, Integer>(); 238 private final Map<String, Long> byKey = new HashMap<String, Long>(); 239 private final Map<String, TreeMap<Long, Map<String, Object>>> out = new HashMap<String, TreeMap<Long, Map<String, Object>>>(); 240 private final Deque<Object[]> pending = new ArrayDeque<Object[]>(); 241 private final Map<String, String> kindOfIdentity = new HashMap<String, String>(); 242 private final Map<Long, String> kindOverride = new HashMap<Long, String>(); 243 private final List<Conflict> conflicts = new ArrayList<Conflict>(); 244 private final List<String> notes = new ArrayList<String>(); 245 private final TreeMap<Long, Object[]> statementGraph = new TreeMap<Long, Object[]>(); 246 private long nextRef = 1; 247 248 @SuppressWarnings("unchecked") 249 Run(List<byte[]> bytes) { 250 this.bytes = bytes; 251 for (String[] collection : COLLECTIONS) { 252 out.put(collection[0], new TreeMap<Long, Map<String, Object>>()); 253 } 254 for (String member : DocumentIndex.MODULE_MEMBERS) { 255 out.put("module:" + member, new TreeMap<Long, Map<String, Object>>()); 256 } 257 for (byte[] data : bytes) { 258 docs.add((Map<String, Object>) CanonicalJson.parse(data)); 259 } 260 } 261 262 Result run(List<String> digests) { 263 checkCompatible(); 264 for (int i = 0; i < bytes.size(); i++) { 265 Semantics forms = new Semantics(bytes.get(i), true, String.valueOf(i)); 266 semantics.add(forms); 267 indexes.add(new DocumentIndex(docs.get(i))); 268 remaps.add(new HashMap<Long, Long>()); 269 Map<String, Long> keys = new HashMap<String, Long>(); 270 Map<String, Long> formed = new HashMap<String, Long>(); 271 for (Long ref : allRefs(i)) { 272 String key = keyOf(i, ref); 273 if (key != null && !keys.containsKey(key)) { 274 keys.put(key, ref); 275 } 276 String form = formKey(i, ref); 277 if (!formed.containsKey(form)) { 278 formed.put(form, ref); 279 } 280 } 281 keyed.add(keys); 282 byForm.add(formed); 283 Set<Long> csets = new HashSet<Long>(); 284 for (Map<?, ?> cset : items(indexes.get(i).graph().get("candidateSets"))) { 285 csets.add(intRef(cset.get("ref"))); 286 } 287 graphCsets.add(csets); 288 Map<Long, Object[]> owners = new HashMap<Long, Object[]>(); 289 for (Map<?, ?> candidate : indexes.get(i).all("factCandidate")) { 290 for (String end : new String[] {"source", "target"}) { 291 Object side = candidate.get(end); 292 Object cset = side instanceof Map<?, ?> ? ((Map<?, ?>) side).get("candidateSet") : null; 293 Long ref = cset instanceof Map<?, ?> ? intRef(((Map<?, ?>) cset).get("ref")) : null; 294 if (ref != null) { 295 owners.put(ref, new Object[] {candidate.get("ref"), end}); 296 } 297 } 298 } 299 embeddedIn.add(owners); 300 } 301 chooseCarriers(); 302 for (int i = 0; i < bytes.size(); i++) { 303 mergeInput(i); 304 } 305 return new Result(buildDocument(), new ArrayList<String>(digests), conflicts, notes); 306 } 307 308 private TreeSet<Long> allRefs(int i) { 309 List<Map<?, ?>> objects = new ArrayList<Map<?, ?>>(items(docs.get(i).get("analysisContexts"))); 310 for (String[] collection : COLLECTIONS) { 311 objects.addAll(semantics.get(i).index().all(collection[0])); 312 } 313 for (String member : DocumentIndex.MODULE_MEMBERS) { 314 objects.addAll(semantics.get(i).index().all("module:" + member)); 315 } 316 TreeSet<Long> refs = new TreeSet<Long>(); 317 for (Map<?, ?> obj : objects) { 318 Long ref = intRef(obj.get("ref")); 319 if (ref != null) { 320 refs.add(ref); 321 } 322 } 323 return refs; 324 } 325 326 private void checkCompatible() { 327 Map<String, Object> first = docs.get(0); 328 for (int i = 0; i < docs.size(); i++) { 329 Map<String, Object> doc = docs.get(i); 330 for (String member : doc.keySet()) { 331 if (!ROOT.contains(member) && !DocumentIndex.MODULE_MEMBERS.contains(member)) { 332 throw new MergeException("input " + i + ": member " + member + " is not supported by this merger"); 333 } 334 } 335 Map<?, ?> graph = doc.get("explainGraph") instanceof Map<?, ?> ? (Map<?, ?>) doc.get("explainGraph") 336 : Collections.emptyMap(); 337 for (Object member : graph.keySet()) { 338 if (!GRAPH.contains(member)) { 339 throw new MergeException("input " + i + ": explainGraph." + member + " is not supported"); 340 } 341 } 342 for (String member : new String[] {"specUri", "specVersion"}) { 343 if (!same(first.get(member), doc.get(member))) { 344 throw new MergeException("input " + i + ": " + member + " differs from input 0"); 345 } 346 } 347 Map<?, ?> header = (Map<?, ?>) doc.get("header"); 348 Map<?, ?> header0 = (Map<?, ?>) first.get("header"); 349 for (String member : new String[] {"registryVersion", "sourceEmbedding"}) { 350 if (!same(header0.get(member), header.get(member))) { 351 throw new MergeException("input " + i + ": header." + member + " differs from input 0"); 352 } 353 } 354 if (header.containsKey("extensions")) { 355 throw new MergeException("input " + i + ": header extensions are not supported by this merger"); 356 } 357 } 358 } 359 360 private boolean same(Object a, Object b) { 361 return CanonicalJson.jcsText(a).equals(CanonicalJson.jcsText(b)); 362 } 363 364 // ----------------------------------------------------------------------------------------- 365 366 private String kindOf(int i, Object ref) { 367 return semantics.get(i).kindOf(ref); 368 } 369 370 /** The (unit id, analysis context id) key of a unit of input i (read with identifiers filled). */ 371 private String unitKey(int i, Object unitRef) { 372 Map<?, ?> unit = semantics.get(i).index().get("sourceUnit", unitRef); 373 if (unit == null) { 374 return null; 375 } 376 Object id = unit.get("unitId"); 377 Object context = unit.get("analysisContextId"); 378 return id instanceof String && context instanceof String ? "unit|" + id + "|" + context : null; 379 } 380 381 /** The unit ref an object of unit content belongs to, or null when no unit owns it. */ 382 private Long owningUnit(int i, Long ref) { 383 Long current = ref; 384 for (int guard = 0; guard < 4096 && current != null; guard++) { 385 String kind = kindOf(i, current); 386 Map<?, ?> obj = semantics.get(i).object(current); 387 if (obj == null) { 388 return null; 389 } 390 if ("sourceUnit".equals(kind)) { 391 return current; 392 } 393 if (!UNIT_CONTENT.contains(kind) && !kind.startsWith("module:")) { 394 return null; 395 } 396 DocumentIndex index = semantics.get(i).index(); 397 if ("statement".equals(kind)) { 398 return intRef(obj.get("sourceUnitRef")); 399 } 400 Map<?, ?> statement = index.get("statement", "rootCandidateSet".equals(kind) 401 ? semantics.get(i).rootSetStatement(current) : "candidateSet".equals(kind) 402 ? semantics.get(i).graphSetStatement(current) : obj.get("statementRef")); 403 if (statement != null) { 404 return intRef(statement.get("sourceUnitRef")); 405 } 406 Long unit = intRef(obj.get("sourceUnitRef")); 407 for (String member : new String[] {"span", "location"}) { 408 if (unit == null && obj.get(member) instanceof Map<?, ?>) { 409 unit = intRef(((Map<?, ?>) obj.get(member)).get("sourceUnitRef")); 410 } 411 } 412 if (unit != null) { 413 return unit; 414 } 415 if ("scope".equals(kind)) { 416 current = intRef(obj.get("parentRef")); 417 } else if ("supportPath".equals(kind)) { 418 // a support path is its micro-edges' statement's: two paths of one Occurrence through two LOCAL 419 // endpoints have one shape, and are still two objects (plan 09, ruling f) 420 Object edges = obj.get("microEdgeRefs"); 421 current = edges instanceof List<?> && !((List<?>) edges).isEmpty() 422 ? intRef(((List<?>) edges).get(0)) : null; 423 } else if ("endpoint".equals(kind)) { 424 // a LOCAL endpoint belongs where its scope or its owner (a QUERY_RESULT, a temporary table) is 425 current = obj.get("scopeRef") != null ? intRef(obj.get("scopeRef")) : intRef(obj.get("ownerRef")); 426 } else { 427 current = null; 428 } 429 } 430 return null; 431 } 432 433 /** The unit key whose carrier carries an object, or null. */ 434 private String owningKey(int i, Long ref) { 435 Long unit = owningUnit(i, ref); 436 return unit == null ? null : unitKey(i, unit); 437 } 438 439 /** The merge key of an object of input i, or null when it has none. */ 440 private String keyOf(int i, Long ref) { 441 String kind = kindOf(i, ref); 442 Map<?, ?> obj = semantics.get(i).object(ref); 443 if (obj == null) { 444 return null; 445 } 446 if ("analysisContext".equals(kind)) { 447 return semantics.get(i).keyedIdentity(ref); 448 } 449 if ("sourceUnit".equals(kind)) { 450 return unitKey(i, ref); 451 } 452 if ("statement".equals(kind)) { 453 String unit = unitKey(i, obj.get("sourceUnitRef")); 454 return unit == null ? null : "statement|" + unit + "|" + obj.get("ordinal"); 455 } 456 if ("endpoint".equals(kind)) { 457 Map<?, ?> identity = obj.get("identity") instanceof Map<?, ?> ? (Map<?, ?>) obj.get("identity") : null; 458 if (identity != null && "PORTABLE".equals(identity.get("status")) && identity.get("id") instanceof String) { 459 return "endpoint|" + identity.get("id"); 460 } 461 return null; 462 } 463 if ("fact".equals(kind)) { 464 return obj.get("id") instanceof String ? "fact|" + obj.get("id") : null; 465 } 466 if ("occurrence".equals(kind) || "node".equals(kind)) { 467 return obj.get("recordId") instanceof String ? kind + "|" + obj.get("recordId") : null; 468 } 469 return null; 470 } 471 472 /** The analysis contexts of an input, as one key. */ 473 private String analysisKey(int i) { 474 TreeSet<String> contexts = new TreeSet<String>(); 475 for (Map<?, ?> context : items(docs.get(i).get("analysisContexts"))) { 476 contexts.add(String.valueOf(semantics.get(i).keyedIdentity(context.get("ref")))); 477 } 478 return sha256(CanonicalJson.jcsBytes(new ArrayList<String>(contexts))); 479 } 480 481 private String formKey(int i, Long ref) { 482 return "form|" + kindOf(i, ref) + "|" 483 + CanonicalJson.jcsText(semantics.get(i).formOf(ref)); 484 } 485 486 /** One object of a unit's content: its form without the members modules add, and those members. */ 487 private final class Item { 488 final String core; 489 final Map<String, String> added = new TreeMap<String, String>(); 490 491 Item(int i, Long ref) { 492 Map<String, Object> form = new TreeMap<String, Object>(); 493 Object full = semantics.get(i).formOf(ref); 494 for (Map.Entry<?, ?> entry : ((Map<?, ?>) full).entrySet()) { 495 String member = String.valueOf(entry.getKey()); 496 if (MODULE_MEMBERS_OF_OBJECTS.contains(member)) { 497 added.put(member, CanonicalJson.jcsText(entry.getValue())); 498 } else { 499 form.put(member, entry.getValue()); 500 } 501 } 502 core = kindOf(i, ref) + " " + CanonicalJson.jcsText(form); 503 } 504 505 /** True when this item says at least what another says. */ 506 boolean covers(Item other) { 507 for (Map.Entry<String, String> entry : other.added.entrySet()) { 508 if (!entry.getValue().equals(added.get(entry.getKey()))) { 509 return false; 510 } 511 } 512 return true; 513 } 514 } 515 516 /** 517 * Section 9.2: the records of one unit under one context are unioned. This merger carries the content of 518 * such a unit from one input, which must include every other input's content: compared with references 519 * replaced by identities, every statement, scope, Occurrence, write, diagnostic, ExplainGraph node and 520 * micro-edge, endpoint and module object the unit owns, each with at least the members modules add to it 521 * (support paths, evidence). Among such inputs it takes the highest declared level, then the most content, 522 * then the first; when there is none it refuses. 523 */ 524 private void chooseCarriers() { 525 Map<String, List<Integer>> holders = new LinkedHashMap<String, List<Integer>>(); 526 List<Map<String, List<Item>>> contents = new ArrayList<Map<String, List<Item>>>(); 527 for (int i = 0; i < bytes.size(); i++) { 528 Map<String, List<Item>> mine = new HashMap<String, List<Item>>(); 529 for (Long ref : allRefs(i)) { 530 String unit = owningKey(i, ref); 531 if (unit == null) { 532 continue; 533 } 534 if ("sourceUnit".equals(kindOf(i, ref))) { 535 List<Integer> list = holders.get(unit); 536 if (list == null) { 537 list = new ArrayList<Integer>(); 538 holders.put(unit, list); 539 } 540 if (!list.contains(i)) { 541 list.add(i); 542 } 543 } 544 List<Item> items = mine.get(unit); 545 if (items == null) { 546 items = new ArrayList<Item>(); 547 mine.put(unit, items); 548 } 549 items.add(new Item(i, ref)); 550 } 551 contents.add(mine); 552 } 553 for (Map.Entry<String, List<Integer>> entry : holders.entrySet()) { 554 String unit = entry.getKey(); 555 Integer chosen = null; 556 for (Integer i : entry.getValue()) { 557 boolean all = true; 558 for (Integer j : entry.getValue()) { 559 all &= j.intValue() == i.intValue() || contains(unitItems(contents, i, unit), unitItems(contents, j, unit)); 560 } 561 if (all && (chosen == null || better(i, chosen, unitItems(contents, i, unit).size(), 562 unitItems(contents, chosen, unit).size()))) { 563 chosen = i; 564 } 565 } 566 if (chosen == null) { 567 throw new MergeException("inputs " + entry.getValue() + " assert different content for one source" 568 + " unit under one analysis context and none includes the others: " + unit); 569 } 570 carrier.put(unit, chosen); 571 } 572 } 573 574 private List<Item> unitItems(List<Map<String, List<Item>>> contents, int i, String unit) { 575 List<Item> items = contents.get(i).get(unit); 576 return items == null ? Collections.<Item>emptyList() : items; 577 } 578 579 /** True when every item of a part is covered by a distinct item of a whole with the same core. */ 580 private boolean contains(List<Item> whole, List<Item> part) { 581 Map<String, List<Item>> byCore = new HashMap<String, List<Item>>(); 582 for (Item item : whole) { 583 List<Item> list = byCore.get(item.core); 584 if (list == null) { 585 list = new ArrayList<Item>(); 586 byCore.put(item.core, list); 587 } 588 list.add(item); 589 } 590 for (Item item : part) { 591 List<Item> list = byCore.get(item.core); 592 boolean found = false; 593 for (Iterator<Item> it = list == null ? Collections.<Item>emptyList().iterator() : list.iterator(); 594 it.hasNext(); ) { 595 if (it.next().covers(item)) { 596 it.remove(); 597 found = true; 598 break; 599 } 600 } 601 if (!found) { 602 return false; 603 } 604 } 605 return true; 606 } 607 608 private boolean better(int i, int chosen, int size, int chosenSize) { 609 int level = LEVELS.indexOf(docs.get(i).get("declaredLevel")) - LEVELS.indexOf(docs.get(chosen).get("declaredLevel")); 610 return level != 0 ? level > 0 : size > chosenSize; 611 } 612 613 private boolean carries(int i, Long ref) { 614 String unit = owningKey(i, ref); 615 return unit == null || carrier.get(unit) == i; 616 } 617 618 /** The new ref of a ref of input i; the object is queued to be carried when it is new. */ 619 private Long map(int i, Object value) { 620 Long ref = intRef(value); 621 if (ref == null) { 622 return null; 623 } 624 Long known = remaps.get(i).get(ref); 625 if (known != null) { 626 return known; 627 } 628 String kind = kindOf(i, ref); 629 if (kind == null) { 630 throw new MergeException("input " + i + ": ref " + ref + " names nothing"); 631 } 632 String key = keyOf(i, ref); 633 String unit = owningKey(i, ref); 634 Integer from = unit == null ? null : carrier.get(unit); 635 if (from != null && from != i) { 636 // the object belongs to a unit another input carries: use that input's equal object 637 Long there = key != null ? keyed.get(from).get(key) : null; 638 if (there == null) { 639 there = byForm.get(from).get(formKey(i, ref)); 640 } 641 if (there == null) { 642 throw new MergeException("input " + i + ": " + kind + " ref " + ref + " belongs to " + unit 643 + ", which input " + from + " carries without it"); 644 } 645 Long mapped = map(from, there); 646 remaps.get(i).put(ref, mapped); 647 return mapped; 648 } 649 boolean embedded = "candidateSet".equals(kind) && !graphCsets.get(i).contains(ref); 650 if (embedded && embeddedIn.get(i).containsKey(ref)) { 651 // the set at one end of a FactCandidate: when an equal FactCandidate (of this input or another) is 652 // carried in its place, a reference to this set names the set of that one 653 Object[] owner = embeddedIn.get(i).get(ref); 654 String slot = map(i, owner[0]) + "|" + owner[1]; 655 Long shared = embeddedSlots.get(slot); 656 if (shared == null) { 657 shared = nextRef++; 658 embeddedSlots.put(slot, shared); 659 } 660 remaps.get(i).put(ref, shared); 661 return shared; 662 } 663 if (key == null && unit == null && !embedded) { 664 // an object without identity that no unit owns is one object per content within one analysis: two 665 // inputs with the same analysis contexts assert it once, inputs with different contexts each keep it 666 key = "analysis|" + analysisKey(i) + "|" + formKey(i, ref); 667 } 668 Long mapped = key == null ? null : byKey.get(key); 669 Map<?, ?> obj = "analysisContext".equals(kind) || "rootCandidateSet".equals(kind) 670 ? semantics.get(i).object(ref) : indexes.get(i).get(kind, ref); 671 if ("endpoint".equals(kind) && key != null && key.startsWith("endpoint|")) { 672 checkKind(i, key.substring("endpoint|".length()), String.valueOf(obj.get("kind")), mapped); 673 } 674 if (mapped == null) { 675 mapped = nextRef++; 676 if (key != null) { 677 byKey.put(key, mapped); 678 } 679 // a candidate set inside a FactCandidate end is copied with its FactCandidate, not on its own 680 if (!embedded) { 681 pending.add(new Object[] {i, kind, obj, mapped}); 682 } 683 } 684 remaps.get(i).put(ref, mapped); 685 return mapped; 686 } 687 688 /** 689 * The endpoint kind is not an identity input (identity-v1, Section 5.2.1): FROM orders does not say table or 690 * view. A generic RELATION is refined by a specific relation kind; any other difference cannot be kept in one 691 * document until the merge module carries MergeConflict, so it is refused. 692 */ 693 private void checkKind(int i, String identity, String kind, Long mapped) { 694 String seen = kindOfIdentity.get(identity); 695 if (seen == null || seen.equals(kind)) { 696 kindOfIdentity.put(identity, kind); 697 return; 698 } 699 if ("RELATION".equals(seen) && RELATION_KINDS.contains(kind)) { 700 kindOfIdentity.put(identity, kind); 701 kindOverride.put(mapped, kind); 702 conflicts.add(new Conflict(identity, "kind RELATION refined to " + kind + " by input " + i)); 703 } else if (!("RELATION".equals(kind) && RELATION_KINDS.contains(seen))) { 704 throw new MergeException("input " + i + ": endpoint " + identity + " is a " + kind 705 + " where an earlier input has a " + seen + "; both cannot be kept (Section 9.4)"); 706 } 707 } 708 709 private void mergeInput(int i) { 710 DocumentIndex index = indexes.get(i); 711 for (Long ref : allRefs(i)) { 712 String kind = kindOf(i, ref); 713 boolean embedded = "candidateSet".equals(kind) && !graphCsets.get(i).contains(ref); 714 if (!embedded && carries(i, ref) && !"supportPath".equals(kind) && !"factCandidate".equals(kind) 715 && !"fact".equals(kind)) { 716 map(i, ref); 717 } 718 } 719 Map<?, ?> graph = index.graph(); 720 Map<Long, Map<?, ?>> listed = new HashMap<Long, Map<?, ?>>(); 721 for (Map<?, ?> entry : items(graph.get("statementCompleteness"))) { 722 listed.put(intRef(entry.get("statementRef")), entry); 723 } 724 for (Map<?, ?> statement : index.all("statement")) { 725 Long ref = intRef(statement.get("ref")); 726 if (!carries(i, ref)) { 727 continue; 728 } 729 Map<?, ?> entry = listed.get(ref); 730 Object completeness = entry != null ? entry.get("completeness") 731 : index.hasGraph() ? graph.get("completeness") : "PARTIAL"; 732 Long out = remaps.get(i).get(ref); 733 Object[] prior = statementGraph.get(out); 734 // a statement several inputs carry has the graph of the one that has the most of it (an L1 input has 735 // none of it), and of equals the one that says more, whatever the order of the inputs: merging stays 736 // associative 737 if (prior == null || graphRank(completeness) > graphRank(prior[0]) 738 || (graphRank(completeness) == graphRank(prior[0]) && prior[1] == null && entry != null)) { 739 statementGraph.put(out, new Object[] {completeness, entry == null ? null : copy(i, entry, false)}); 740 } 741 } 742 drain(); 743 } 744 745 /** Carries every queued object, which may queue the objects it references. */ 746 private void drain() { 747 while (!pending.isEmpty()) { 748 Object[] job = pending.poll(); 749 int i = (Integer) job[0]; 750 String kind = (String) job[1]; 751 @SuppressWarnings("unchecked") 752 Map<String, Object> copied = (Map<String, Object>) copy(i, job[2], true); 753 copied.put("ref", job[3]); 754 out.get(kind).put((Long) job[3], copied); 755 } 756 } 757 758 /** One container being copied. */ 759 private final class Frame { 760 final Iterator<?> items; 761 final Map<String, Object> map; 762 final List<Object> list; 763 String key; 764 765 Frame(Object container) { 766 if (container instanceof Map<?, ?>) { 767 items = ((Map<?, ?>) container).entrySet().iterator(); 768 map = new LinkedHashMap<String, Object>(); 769 list = null; 770 } else { 771 items = ((List<?>) container).iterator(); 772 map = null; 773 list = new ArrayList<Object>(); 774 } 775 } 776 777 void add(Object value) { 778 if (map != null) { 779 map.put(key, value); 780 } else { 781 list.add(value); 782 } 783 } 784 } 785 786 /** 787 * A copy of a JSON value of input i with every reference renumbered, made with an explicit stack. The 788 * {@code ref} of the top object is its own and is left for the caller; a {@code ref} below it (a candidate 789 * set inside a FactCandidate) is renumbered. 790 */ 791 private Object copy(int i, Object value, boolean top) { 792 if (!(value instanceof Map<?, ?> || value instanceof List<?>)) { 793 return value; 794 } 795 Deque<Frame> stack = new ArrayDeque<Frame>(); 796 stack.push(new Frame(value)); 797 Object result = null; 798 while (!stack.isEmpty()) { 799 Frame frame = stack.peek(); 800 if (!frame.items.hasNext()) { 801 stack.pop(); 802 Object built = frame.map != null ? frame.map : frame.list; 803 if (stack.isEmpty()) { 804 result = built; 805 } else { 806 stack.peek().add(built); 807 } 808 continue; 809 } 810 Object item = frame.items.next(); 811 Object child; 812 if (frame.map != null) { 813 Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item; 814 String member = (String) entry.getKey(); 815 child = entry.getValue(); 816 frame.key = member; 817 if ("extensions".equals(member)) { 818 frame.add(child); 819 continue; 820 } 821 if ("ref".equals(member)) { 822 frame.add(top && stack.size() == 1 ? child : map(i, child)); 823 continue; 824 } 825 if (member.endsWith("Refs") && child instanceof List<?>) { 826 List<Object> refs = new ArrayList<Object>(); 827 for (Object r : (List<?>) child) { 828 refs.add(map(i, r)); 829 } 830 frame.add(refs); 831 continue; 832 } 833 if (member.endsWith("Ref") && intRef(child) != null) { 834 frame.add(map(i, child)); 835 continue; 836 } 837 } else { 838 child = item; 839 } 840 if (child instanceof Map<?, ?> || child instanceof List<?>) { 841 stack.push(new Frame(child)); 842 } else { 843 frame.add(child); 844 } 845 } 846 return result; 847 } 848 849 // ----------------------------------------------------------------------------------------- 850 851 private Document buildDocument() { 852 Map<String, Object> first = docs.get(0); 853 Map<String, Object> doc = new LinkedHashMap<String, Object>(); 854 doc.put("specUri", first.get("specUri")); 855 doc.put("specVersion", first.get("specVersion")); 856 String level = "L1"; 857 Set<Object> modules = new TreeSet<Object>(); 858 Object analysis = null; 859 List<Object> serving = new ArrayList<Object>(); 860 Map<Object, List<Object>> features = new TreeMap<Object, List<Object>>(); 861 for (int i = 0; i < docs.size(); i++) { 862 Map<String, Object> input = docs.get(i); 863 if (LEVELS.indexOf(input.get("declaredLevel")) > LEVELS.indexOf(level)) { 864 level = (String) input.get("declaredLevel"); 865 } 866 modules.addAll((List<?>) input.get("modules")); 867 analysis = combined(analysis, input.get("analysisCompleteness")); 868 serving.add(copy(i, input.get("servingCompleteness"), false)); 869 for (Map<?, ?> feature : items(input.get("featureCompleteness"))) { 870 List<Object> list = features.get(feature.get("feature")); 871 if (list == null) { 872 list = new ArrayList<Object>(); 873 features.put(feature.get("feature"), list); 874 } 875 list.add(copy(i, feature, false)); 876 } 877 } 878 drain(); 879 for (Map.Entry<Long, String> entry : kindOverride.entrySet()) { 880 out.get("endpoint").get(entry.getKey()).put("kind", entry.getValue()); 881 } 882 doc.put("declaredLevel", level); 883 doc.put("modules", new ArrayList<Object>(modules)); 884 Map<String, Object> header = new LinkedHashMap<String, Object>(); 885 header.put("producer", producer.toJson()); 886 Map<?, ?> header0 = (Map<?, ?>) first.get("header"); 887 header.put("registryVersion", header0.get("registryVersion")); 888 header.put("sourceEmbedding", header0.get("sourceEmbedding")); 889 if (createdAt != null) { 890 header.put("createdAt", createdAt); 891 } 892 doc.put("header", header); 893 for (String[] collection : COLLECTIONS) { 894 // the root candidate sets are present only when an Occurrence of a Fact names one (spec 4.3, rule 1) 895 if (!GRAPH_KINDS.contains(collection[0]) 896 && !("rootCandidateSet".equals(collection[0]) && out.get(collection[0]).isEmpty())) { 897 doc.put(collection[1], new ArrayList<Object>(out.get(collection[0]).values())); 898 } 899 } 900 for (String member : DocumentIndex.MODULE_MEMBERS) { 901 if (!out.get("module:" + member).isEmpty()) { 902 doc.put(member, new ArrayList<Object>(out.get("module:" + member).values())); 903 } 904 } 905 doc.put("analysisCompleteness", analysis); 906 doc.put("servingCompleteness", mergedServing(serving)); 907 if (!features.isEmpty()) { 908 List<Object> list = new ArrayList<Object>(); 909 for (List<Object> entries : features.values()) { 910 list.add(mergedFeature(entries)); 911 } 912 doc.put("featureCompleteness", list); 913 } 914 if (modules.contains("explain")) { 915 doc.put("explainGraph", graph()); 916 } 917 dropRepeats(doc); 918 return Document.fromJson(doc); 919 } 920 921 /** 922 * Equal objects without an identity are one object of the analysis, so two elements of a SET can become 923 * equal once their refs are renumbered (two equal diagnostics of one statement, two equal FactCandidates 924 * among the inputs of one target field); the SET keeps such an element once. Two elements with an equal 925 * sort key and different content stay, and canonical JSON refuses them. 926 */ 927 @SuppressWarnings("unchecked") 928 private void dropRepeats(Map<String, Object> doc) { 929 Deque<Object[]> stack = new ArrayDeque<Object[]>(); 930 stack.push(new Object[] {doc, ArrayRegistry.standard().root()}); 931 while (!stack.isEmpty()) { 932 Object[] top = stack.pop(); 933 ArrayRegistry.Position position = (ArrayRegistry.Position) top[1]; 934 if (top[0] instanceof Map<?, ?>) { 935 for (Map.Entry<?, ?> entry : ((Map<?, ?>) top[0]).entrySet()) { 936 if (!"extensions".equals(entry.getKey())) { 937 stack.push(new Object[] {entry.getValue(), 938 position == null ? null : position.member(String.valueOf(entry.getKey()))}); 939 } 940 } 941 } else if (top[0] instanceof List<?>) { 942 List<Object> list = (List<Object>) top[0]; 943 if (position != null && position.registration() != null 944 && position.registration().getOrder() == ArrayRegistry.Order.SET) { 945 Set<String> seen = new HashSet<String>(); 946 for (Iterator<Object> it = list.iterator(); it.hasNext();) { 947 if (!seen.add(CanonicalJson.jcsText(it.next()))) { 948 it.remove(); 949 } 950 } 951 } 952 ArrayRegistry.Position element = position == null ? null : position.element(); 953 for (Object item : list) { 954 stack.push(new Object[] {item, element}); 955 } 956 } 957 } 958 } 959 960 /** Equal objects stay as they are; otherwise TRUNCATED unless all are COMPLETE, the truncations unioned. */ 961 @SuppressWarnings("unchecked") 962 private Map<String, Object> mergedServing(List<Object> objects) { 963 List<Object> distinct = distinct(objects); 964 if (distinct.size() == 1) { 965 return (Map<String, Object>) distinct.get(0); 966 } 967 Map<String, Object> merged = new LinkedHashMap<String, Object>(); 968 boolean complete = true; 969 List<Object> truncations = new ArrayList<Object>(); 970 for (Object object : distinct) { 971 Map<String, Object> serving = (Map<String, Object>) object; 972 rejectExtensions(serving, "servingCompleteness"); 973 complete &= "COMPLETE".equals(serving.get("status")); 974 if (serving.get("truncations") instanceof List<?>) { 975 truncations.addAll((List<Object>) serving.get("truncations")); 976 } 977 } 978 merged.put("status", complete ? "COMPLETE" : "TRUNCATED"); 979 if (!truncations.isEmpty()) { 980 merged.put("truncations", distinct(truncations)); 981 } 982 return merged; 983 } 984 985 /** Equal entries stay as they are; otherwise the status is combined and the diagnostics unioned. */ 986 @SuppressWarnings("unchecked") 987 private Map<String, Object> mergedFeature(List<Object> objects) { 988 List<Object> distinct = distinct(objects); 989 if (distinct.size() == 1) { 990 return (Map<String, Object>) distinct.get(0); 991 } 992 Map<String, Object> merged = new LinkedHashMap<String, Object>(); 993 Object status = null; 994 Set<Object> diagnostics = new LinkedHashSet<Object>(); 995 for (Object object : distinct) { 996 Map<String, Object> entry = (Map<String, Object>) object; 997 rejectExtensions(entry, "featureCompleteness"); 998 merged.put("feature", entry.get("feature")); 999 status = combined(status, entry.get("status")); 1000 if (entry.get("diagnosticRefs") instanceof List<?>) { 1001 diagnostics.addAll((List<Object>) entry.get("diagnosticRefs")); 1002 } 1003 } 1004 merged.put("status", status); 1005 if (!diagnostics.isEmpty()) { 1006 merged.put("diagnosticRefs", new ArrayList<Object>(diagnostics)); 1007 } 1008 return merged; 1009 } 1010 1011 private void rejectExtensions(Map<String, Object> object, String what) { 1012 if (object.containsKey("extensions")) { 1013 throw new MergeException(what + " objects with extensions differ between inputs and cannot be combined"); 1014 } 1015 } 1016 1017 /** How much of a statement's graph an input has: COMPLETE above PARTIAL above anything else. */ 1018 private int graphRank(Object completeness) { 1019 return "COMPLETE".equals(completeness) ? 2 : "PARTIAL".equals(completeness) ? 1 : 0; 1020 } 1021 1022 private List<Object> distinct(List<Object> values) { 1023 Map<String, Object> seen = new LinkedHashMap<String, Object>(); 1024 for (Object value : values) { 1025 seen.put(CanonicalJson.jcsText(value), value); 1026 } 1027 return new ArrayList<Object>(seen.values()); 1028 } 1029 1030 private Map<String, Object> graph() { 1031 Map<String, Object> graph = new LinkedHashMap<String, Object>(); 1032 boolean allComplete = true; 1033 for (Object[] completeness : statementGraph.values()) { 1034 allComplete &= "COMPLETE".equals(completeness[0]); 1035 } 1036 String byDefault = allComplete ? "COMPLETE" : "PARTIAL"; 1037 graph.put("completeness", byDefault); 1038 List<Object> listed = new ArrayList<Object>(); 1039 for (Map.Entry<Long, Object[]> entry : statementGraph.entrySet()) { 1040 Object[] value = entry.getValue(); 1041 if (value[1] != null && (!byDefault.equals(value[0]) || ((Map<?, ?>) value[1]).get("diagnosticRefs") != null)) { 1042 // only a statement that differs from the graph's completeness is listed (fold-v1, Section 2) 1043 @SuppressWarnings("unchecked") 1044 Map<String, Object> copied = (Map<String, Object>) value[1]; 1045 copied.put("statementRef", entry.getKey()); 1046 listed.add(copied); 1047 } else if (!byDefault.equals(value[0])) { 1048 Map<String, Object> item = new LinkedHashMap<String, Object>(); 1049 item.put("statementRef", entry.getKey()); 1050 item.put("completeness", value[0]); 1051 listed.add(item); 1052 } 1053 } 1054 if (!listed.isEmpty()) { 1055 graph.put("statementCompleteness", listed); 1056 } 1057 graph.put("nodes", new ArrayList<Object>(out.get("node").values())); 1058 graph.put("microEdges", new ArrayList<Object>(out.get("microEdge").values())); 1059 if (!out.get("candidateSet").isEmpty()) { 1060 graph.put("candidateSets", new ArrayList<Object>(out.get("candidateSet").values())); 1061 } 1062 graph.put("supportPaths", new ArrayList<Object>(out.get("supportPath").values())); 1063 return graph; 1064 } 1065 } 1066}