001package gudusoft.gsqlparser.pp2; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.List; 006 007/** 008 * Immutable result of a {@code Pp2Formatter.format(...)} call. 009 * 010 * <p>Carries the formatted SQL text, the overall outcome 011 * ({@link FormatStatus}), the per-region rendering record 012 * ({@link Region}), and the diagnostic events the engine emitted along the 013 * way ({@link FormatDiagnostic}). 014 * 015 * <p>Built only by pp2 internals. Callers consume the immutable accessors: 016 * {@link #getText()}, {@link #getStatus()}, {@link #getRegions()}, 017 * {@link #getDiagnostics()}. The two list accessors return unmodifiable 018 * views. 019 * 020 * <p>{@link #getRegions()} is how you tell what actually formatted your SQL. 021 * A region tagged {@link RendererId#GUARDED_AST} holds 022 * {@code FormatterFactory.pp}'s own output, unchanged — on input that parses 023 * as a whole, every region is that and {@link #getText()} is byte-identical to 024 * {@code pp()}. Any other {@link RendererId} marks a byte range pp could not 025 * format, which pp2 rendered from the token stream instead. 026 */ 027public final class Pp2FormatResult { 028 029 /** 030 * Per-region rendering record. Spans are byte offsets into the 031 * <b>original</b> input string (not the formatted output), so multiple 032 * regions can be reconstructed against the source. 033 * 034 * <p>Carries the {@link RendererId} that the engine actually used and a 035 * region-local {@link FormatStatus} that may be {@link FormatStatus#OK} 036 * even when the result's overall status is 037 * {@link FormatStatus#OK_WITH_RECOVERY} (i.e., some other region needed 038 * recovery, but this one did not). 039 */ 040 public static final class Region { 041 private final int startOffset; 042 private final int endOffset; 043 private final RendererId rendererId; 044 private final FormatStatus status; 045 046 public Region(int startOffset, int endOffset, RendererId rendererId, 047 FormatStatus status) { 048 if (rendererId == null) throw new NullPointerException("rendererId"); 049 if (status == null) throw new NullPointerException("status"); 050 if (startOffset < 0) { 051 throw new IllegalArgumentException("startOffset < 0: " + startOffset); 052 } 053 if (endOffset < startOffset) { 054 throw new IllegalArgumentException( 055 "endOffset < startOffset: " + endOffset + " < " + startOffset); 056 } 057 this.startOffset = startOffset; 058 this.endOffset = endOffset; 059 this.rendererId = rendererId; 060 this.status = status; 061 } 062 063 public int getStartOffset() { return startOffset; } 064 public int getEndOffset() { return endOffset; } 065 public RendererId getRendererId() { return rendererId; } 066 public FormatStatus getStatus() { return status; } 067 068 @Override 069 public String toString() { 070 return "Region{[" + startOffset + ".." + endOffset + "] " 071 + rendererId + " " + status + "}"; 072 } 073 } 074 075 private final String text; 076 private final FormatStatus status; 077 private final List<Region> regions; 078 private final List<FormatDiagnostic> diagnostics; 079 private final boolean wholeDocument; 080 private final boolean nameCaseApplied; 081 082 public Pp2FormatResult(String text, FormatStatus status, 083 List<Region> regions, 084 List<FormatDiagnostic> diagnostics) { 085 this(text, status, regions, diagnostics, false, false); 086 } 087 088 /** 089 * @param wholeDocument true when {@code text} is {@code FormatterFactory.pp}'s 090 * output for the whole input; see {@link #isWholeDocument()} 091 * @param nameCaseApplied see {@link #isNameCaseApplied()} 092 */ 093 public Pp2FormatResult(String text, FormatStatus status, 094 List<Region> regions, 095 List<FormatDiagnostic> diagnostics, 096 boolean wholeDocument, boolean nameCaseApplied) { 097 if (text == null) throw new NullPointerException("text"); 098 if (status == null) throw new NullPointerException("status"); 099 this.text = text; 100 this.status = status; 101 this.regions = regions == null || regions.isEmpty() 102 ? Collections.<Region>emptyList() 103 : Collections.unmodifiableList(new ArrayList<Region>(regions)); 104 this.diagnostics = diagnostics == null || diagnostics.isEmpty() 105 ? Collections.<FormatDiagnostic>emptyList() 106 : Collections.unmodifiableList(new ArrayList<FormatDiagnostic>(diagnostics)); 107 this.wholeDocument = wholeDocument; 108 this.nameCaseApplied = nameCaseApplied; 109 } 110 111 public String getText() { return text; } 112 public FormatStatus getStatus() { return status; } 113 114 /** Unmodifiable view; never {@code null}, may be empty. */ 115 public List<Region> getRegions() { return regions; } 116 117 /** Unmodifiable view; never {@code null}, may be empty. */ 118 public List<FormatDiagnostic> getDiagnostics() { return diagnostics; } 119 120 /** 121 * True when the whole input parsed and {@link #getText()} is 122 * {@code FormatterFactory.pp}'s output for it. An all-{@link RendererId#GUARDED_AST} 123 * regional run looks the same in {@link #getRegions()}, so this is the 124 * way to tell. 125 */ 126 public boolean isWholeDocument() { return wholeDocument; } 127 128 /** 129 * True when the per-kind name cases ({@code GFmtOpt.caseTableName}, 130 * {@code caseColumnName}, {@code caseTableAlias}, {@code caseColumnAlias}) 131 * applied: the whole document was formatted by pp, its names were 132 * resolved, and they could be cased consistently. The regional pipeline 133 * parses without name resolution, so a regional result never applies them. 134 */ 135 public boolean isNameCaseApplied() { return nameCaseApplied; } 136 137 @Override 138 public String toString() { 139 return "Pp2FormatResult{status=" + status 140 + ", regions=" + regions.size() 141 + ", diagnostics=" + diagnostics.size() 142 + ", textLen=" + text.length() + "}"; 143 } 144}