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}