001package gudusoft.gsqlparser.dlineage.comment; 002 003import gudusoft.gsqlparser.dlineage.comment.CommentBindingBundle.*; 004import gudusoft.gsqlparser.dlineage.dataflow.model.xml.*; 005import java.util.*; 006import java.util.function.UnaryOperator; 007 008/** Centralized, typed model-ID projection. Source occurrence identities never use model IDs. */ 009public final class CommentBindingProjection { 010 private CommentBindingProjection() {} 011 private static CommentBindingBundle copy(CommentBindingBundle b) { 012 CommentBindingBundle n = new CommentBindingBundle(); 013 n.state=b.state; n.limits=b.limits; n.supportedHostKinds.addAll(b.supportedHostKinds); 014 n.sourceUnits.addAll(b.sourceUnits); n.comments.addAll(b.comments); n.bindings.addAll(b.bindings); 015 n.coverage.addAll(b.coverage); n.diagnostics.addAll(b.diagnostics); return n; 016 } 017 private static String mapped(String id, Map<String,String> map) { return id == null ? null : map.getOrDefault(id,id); } 018 public static void remap(dataflow flow, Map<String,String> objects, Map<String,String> columns, Map<String,String> relations) { 019 if (flow == null || flow.getCommentBindings() == null) return; 020 CommentBindingBundle b=flow.getCommentBindings(), n=copy(b); 021 for (Host h:b.hosts) { 022 List<Target> targets=new ArrayList<Target>(); 023 for (Target t:h.targets) targets.add(new Target(t.space,mapped(t.id, 024 "column".equals(t.space)?columns:"relationship".equals(t.space)?relations:objects),mapped(t.parentId,objects))); 025 n.hosts.add(host(h,targets,mapped(h.processId,objects),mapped(h.procedureId,objects),h.bindingState)); 026 } 027 flow.setCommentBindings(n); 028 } 029 public static void offset(dataflow flow, UnaryOperator<String> offset) { 030 if (flow == null || flow.getCommentBindings() == null) return; 031 CommentBindingBundle b=flow.getCommentBindings(), n=copy(b); 032 for (Host h:b.hosts) { 033 List<Target> targets=new ArrayList<Target>(); 034 for (Target t:h.targets) targets.add(new Target(t.space,offset.apply(t.id),t.parentId==null?null:offset.apply(t.parentId))); 035 n.hosts.add(host(h,targets,h.processId==null?null:offset.apply(h.processId),h.procedureId==null?null:offset.apply(h.procedureId),h.bindingState)); 036 } 037 flow.setCommentBindings(n); 038 } 039 private static Host host(Host h,List<Target> targets,String processId,String procedureId,String state) { 040 return new Host(h.id,h.sourceUnitId,h.kind,h.syntaxKind,h.range,h.nameRange,h.queryBlockId,h.parentHostId,h.ordinal,state,targets,processId,procedureId); 041 } 042 /** Remove dangling projections, preserving the occurrence and its original owner. */ 043 public static void retain(dataflow flow) { retain(flow, true); } 044 045 public static void retain(dataflow flow, boolean filtering) { 046 if (flow == null || flow.getCommentBindings() == null) return; 047 Set<String> present=new HashSet<String>(); 048 for (table t:flow.getTables()) table(present,t,"table"); 049 for (table t:flow.getViews()) table(present,t,"table"); 050 for (table t:flow.getResultsets()) table(present,t,"resultset"); 051 for (procedure p:flow.getProcedures()) present.add("procedure:"+p.getId()); 052 for (oraclePackage p:flow.getPackages()) { 053 present.add("package:"+p.getId()); 054 if (p.getProcedures()!=null) for (procedure routine:p.getProcedures()) present.add("procedure:"+routine.getId()); 055 } 056 for (process p:flow.getProcesses()) present.add("process:"+p.getId()); 057 for (relationship r:flow.getRelationships()) present.add("relationship:"+r.getId()); 058 CommentBindingBundle b=flow.getCommentBindings(), n=copy(b); 059 Set<String> filtered=new HashSet<String>(); 060 for (Host h:b.hosts) { 061 List<Target> targets=new ArrayList<Target>(); 062 for (Target t:h.targets) if (present.contains(key(t))) targets.add(t); 063 boolean removed=targets.size()!=h.targets.size(); 064 if (removed) { 065 if (filtering) filtered.add(h.sourceUnitId+":"+h.kind); 066 n.diagnostics.add(new Diagnostic("MODEL_NOT_EMITTED",h.sourceUnitId,null,h.id)); 067 } 068 n.hosts.add(host(h,targets,present.contains("process:"+h.processId)?h.processId:null, 069 present.contains("procedure:"+h.procedureId)?h.procedureId:null, 070 removed && targets.isEmpty()?(filtering?"filtered":"source_only"):h.bindingState)); 071 } 072 if (!filtered.isEmpty()) { 073 n.coverage.clear(); 074 for (Coverage c:b.coverage) n.coverage.add(new Coverage(c.sourceUnitId,c.hostKind, 075 filtered.contains(c.sourceUnitId+":"+c.hostKind)&&"complete".equals(c.state)?"filtered":c.state)); 076 if ("complete".equals(n.state)) n.state="partial"; 077 } 078 flow.setCommentBindings(n); 079 } 080 private static String key(Target t) { return t.space+":"+t.id+("column".equals(t.space)?":"+t.parentId:""); } 081 private static void table(Set<String> ids,table t,String space) { 082 ids.add(space+":"+t.getId()); 083 if (t.getColumns()!=null) for (column c:t.getColumns()) ids.add("column:"+c.getId()+":"+t.getId()); 084 } 085 /** Preserve imported source facts when a legacy append has no unambiguous model-ID map. */ 086 public static CommentBindingBundle sourceOnly(CommentBindingBundle b) { 087 CommentBindingBundle n = copy(b); 088 n.state = "failed".equals(b.state) ? "failed" : "partial"; 089 n.coverage.clear(); 090 for (Coverage c : b.coverage) n.coverage.add(new Coverage(c.sourceUnitId, c.hostKind, "unsupported")); 091 for (Host h : b.hosts) n.hosts.add(host(h, Collections.<Target>emptyList(), null, null, "source_only")); 092 n.diagnostics.add(new Diagnostic("MODEL_MAPPING_UNAVAILABLE", null, null, null)); 093 return n; 094 } 095 /** Merge already-remapped documents, giving each document its own source namespace. */ 096 public static CommentBindingBundle merge(List<CommentBindingBundle> bundles) { 097 CommentBindingBundle n=null; 098 for (CommentBindingBundle b:bundles) { 099 if (b==null) continue; 100 if (n==null) { n=new CommentBindingBundle(); n.limits=b.limits; n.supportedHostKinds.addAll(b.supportedHostKinds); } 101 Map<String,String> sources = new HashMap<String,String>(); 102 Map<String,String> comments = new HashMap<String,String>(); 103 Map<String,String> hosts = new HashMap<String,String>(); 104 if ("failed".equals(b.state)) n.state="failed"; 105 else if (!"complete".equals(b.state) && !"failed".equals(n.state)) n.state="partial"; 106 for (SourceUnit unit:b.sourceUnits) { 107 String id="s"+(n.sourceUnits.size()+1); sources.put(unit.id,id); 108 n.sourceUnits.add(new SourceUnit(id,n.sourceUnits.size(),unit.vendor,unit.contentDigest,unit.originKind,unit.fileName)); 109 } 110 int nextComment = n.comments.size() + 1, nextHost = n.hosts.size() + 1; 111 for (Comment c:b.comments) comments.put(c.id,sources.get(c.sourceUnitId)+":c"+(nextComment++)); 112 for (Host h:b.hosts) hosts.put(h.id,h.id.equals(h.sourceUnitId)?sources.get(h.sourceUnitId):sources.get(h.sourceUnitId)+":h"+(nextHost++)); 113 for (Comment c:b.comments) n.comments.add(new Comment(comments.get(c.id),sources.get(c.sourceUnitId),c.range,c.lexicalKind,c.raw,c.rawEncoding,c.originalLength,c.rawTruncated)); 114 for (Host h:b.hosts) n.hosts.add(new Host(hosts.get(h.id),sources.get(h.sourceUnitId),h.kind,h.syntaxKind,h.range,h.nameRange, 115 hosts.get(h.queryBlockId),hosts.get(h.parentHostId),h.ordinal,h.bindingState,h.targets,h.processId,h.procedureId)); 116 for (Binding x:b.bindings) n.bindings.add(new Binding(comments.get(x.commentId),hosts.get(x.hostId),x.placement,x.ruleId)); 117 for (Coverage c:b.coverage) n.coverage.add(new Coverage(sources.get(c.sourceUnitId),c.hostKind,c.state)); 118 for (Diagnostic d:b.diagnostics) n.diagnostics.add(new Diagnostic(d.code,sources.get(d.sourceUnitId),comments.get(d.commentId),hosts.get(d.hostId))); 119 } 120 if (n != null) limitMergedRaw(n); 121 return n; 122 } 123 private static void limitMergedRaw(CommentBindingBundle bundle) { 124 if (bundle.limits == null) return; 125 int remaining = bundle.limits.maxTotalRawLength; 126 Set<String> limited = new HashSet<String>(); 127 for (int i=0;i<bundle.comments.size();i++) { 128 Comment c=bundle.comments.get(i); 129 String raw=c.raw == null ? "" : c.raw; 130 boolean encoded="base64-utf8".equals(c.rawEncoding); 131 if (encoded) raw=new String(Base64.getDecoder().decode(raw),java.nio.charset.StandardCharsets.UTF_8); 132 int keep=Math.min(raw.length(),Math.min(bundle.limits.maxRawLength,remaining)); 133 if (keep<raw.length() && keep>0 && Character.isHighSurrogate(raw.charAt(keep-1))) keep--; 134 remaining-=keep; 135 if (keep<raw.length()) { 136 limited.add(c.sourceUnitId); 137 raw=raw.substring(0,keep); 138 if (encoded) raw=Base64.getEncoder().encodeToString(raw.getBytes(java.nio.charset.StandardCharsets.UTF_8)); 139 bundle.comments.set(i,new Comment(c.id,c.sourceUnitId,c.range,c.lexicalKind,raw,c.rawEncoding,c.originalLength,true)); 140 } 141 } 142 if (!limited.isEmpty()) { 143 bundle.state="partial"; 144 for (String source:limited) bundle.diagnostics.add(new Diagnostic("LIMIT_REACHED",source,null,null)); 145 for (int i=0;i<bundle.coverage.size();i++) { 146 Coverage c=bundle.coverage.get(i); 147 if (limited.contains(c.sourceUnitId)) bundle.coverage.set(i,new Coverage(c.sourceUnitId,c.hostKind,"limited")); 148 } 149 } 150 } 151 152}