1/*
2 * Copyright (C) 2007 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.dx.util;
18
19import java.io.IOException;
20import java.io.OutputStream;
21import java.io.OutputStreamWriter;
22import java.io.StringWriter;
23import java.io.Writer;
24
25/**
26 * Class that takes a combined output destination and provides two
27 * output writers, one of which ends up writing to the left column and
28 * one which goes on the right.
29 */
30public final class TwoColumnOutput {
31    /** {@code non-null;} underlying writer for final output */
32    private final Writer out;
33
34    /** {@code > 0;} the left column width */
35    private final int leftWidth;
36
37    /** {@code non-null;} pending left column output */
38    private final StringBuffer leftBuf;
39
40    /** {@code non-null;} pending right column output */
41    private final StringBuffer rightBuf;
42
43    /** {@code non-null;} left column writer */
44    private final IndentingWriter leftColumn;
45
46    /** {@code non-null;} right column writer */
47    private final IndentingWriter rightColumn;
48
49    /**
50     * Turns the given two strings (with widths) and spacer into a formatted
51     * two-column string.
52     *
53     * @param s1 {@code non-null;} first string
54     * @param width1 {@code > 0;} width of the first column
55     * @param spacer {@code non-null;} spacer string
56     * @param s2 {@code non-null;} second string
57     * @param width2 {@code > 0;} width of the second column
58     * @return {@code non-null;} an appropriately-formatted string
59     */
60    public static String toString(String s1, int width1, String spacer,
61                                  String s2, int width2) {
62        int len1 = s1.length();
63        int len2 = s2.length();
64
65        StringWriter sw = new StringWriter((len1 + len2) * 3);
66        TwoColumnOutput twoOut =
67            new TwoColumnOutput(sw, width1, width2, spacer);
68
69        try {
70            twoOut.getLeft().write(s1);
71            twoOut.getRight().write(s2);
72        } catch (IOException ex) {
73            throw new RuntimeException("shouldn't happen", ex);
74        }
75
76        twoOut.flush();
77        return sw.toString();
78    }
79
80    /**
81     * Constructs an instance.
82     *
83     * @param out {@code non-null;} writer to send final output to
84     * @param leftWidth {@code > 0;} width of the left column, in characters
85     * @param rightWidth {@code > 0;} width of the right column, in characters
86     * @param spacer {@code non-null;} spacer string to sit between the two columns
87     */
88    public TwoColumnOutput(Writer out, int leftWidth, int rightWidth,
89                           String spacer) {
90        if (out == null) {
91            throw new NullPointerException("out == null");
92        }
93
94        if (leftWidth < 1) {
95            throw new IllegalArgumentException("leftWidth < 1");
96        }
97
98        if (rightWidth < 1) {
99            throw new IllegalArgumentException("rightWidth < 1");
100        }
101
102        if (spacer == null) {
103            throw new NullPointerException("spacer == null");
104        }
105
106        StringWriter leftWriter = new StringWriter(1000);
107        StringWriter rightWriter = new StringWriter(1000);
108
109        this.out = out;
110        this.leftWidth = leftWidth;
111        this.leftBuf = leftWriter.getBuffer();
112        this.rightBuf = rightWriter.getBuffer();
113        this.leftColumn = new IndentingWriter(leftWriter, leftWidth);
114        this.rightColumn =
115            new IndentingWriter(rightWriter, rightWidth, spacer);
116    }
117
118    /**
119     * Constructs an instance.
120     *
121     * @param out {@code non-null;} stream to send final output to
122     * @param leftWidth {@code >= 1;} width of the left column, in characters
123     * @param rightWidth {@code >= 1;} width of the right column, in characters
124     * @param spacer {@code non-null;} spacer string to sit between the two columns
125     */
126    public TwoColumnOutput(OutputStream out, int leftWidth, int rightWidth,
127                           String spacer) {
128        this(new OutputStreamWriter(out), leftWidth, rightWidth, spacer);
129    }
130
131    /**
132     * Gets the writer to use to write to the left column.
133     *
134     * @return {@code non-null;} the left column writer
135     */
136    public Writer getLeft() {
137        return leftColumn;
138    }
139
140    /**
141     * Gets the writer to use to write to the right column.
142     *
143     * @return {@code non-null;} the right column writer
144     */
145    public Writer getRight() {
146        return rightColumn;
147    }
148
149    /**
150     * Flushes the output. If there are more lines of pending output in one
151     * column, then the other column will get filled with blank lines.
152     */
153    public void flush() {
154        try {
155            appendNewlineIfNecessary(leftBuf, leftColumn);
156            appendNewlineIfNecessary(rightBuf, rightColumn);
157            outputFullLines();
158            flushLeft();
159            flushRight();
160        } catch (IOException ex) {
161            throw new RuntimeException(ex);
162        }
163    }
164
165    /**
166     * Outputs to the final destination as many full line pairs as
167     * there are in the pending output, removing those lines from
168     * their respective buffers. This method terminates when at
169     * least one of the two column buffers is empty.
170     */
171    private void outputFullLines() throws IOException {
172        for (;;) {
173            int leftLen = leftBuf.indexOf("\n");
174            if (leftLen < 0) {
175                return;
176            }
177
178            int rightLen = rightBuf.indexOf("\n");
179            if (rightLen < 0) {
180                return;
181            }
182
183            if (leftLen != 0) {
184                out.write(leftBuf.substring(0, leftLen));
185            }
186
187            if (rightLen != 0) {
188                writeSpaces(out, leftWidth - leftLen);
189                out.write(rightBuf.substring(0, rightLen));
190            }
191
192            out.write('\n');
193
194            leftBuf.delete(0, leftLen + 1);
195            rightBuf.delete(0, rightLen + 1);
196        }
197    }
198
199    /**
200     * Flushes the left column buffer, printing it and clearing the buffer.
201     * If the buffer is already empty, this does nothing.
202     */
203    private void flushLeft() throws IOException {
204        appendNewlineIfNecessary(leftBuf, leftColumn);
205
206        while (leftBuf.length() != 0) {
207            rightColumn.write('\n');
208            outputFullLines();
209        }
210    }
211
212    /**
213     * Flushes the right column buffer, printing it and clearing the buffer.
214     * If the buffer is already empty, this does nothing.
215     */
216    private void flushRight() throws IOException {
217        appendNewlineIfNecessary(rightBuf, rightColumn);
218
219        while (rightBuf.length() != 0) {
220            leftColumn.write('\n');
221            outputFullLines();
222        }
223    }
224
225    /**
226     * Appends a newline to the given buffer via the given writer, but
227     * only if it isn't empty and doesn't already end with one.
228     *
229     * @param buf {@code non-null;} the buffer in question
230     * @param out {@code non-null;} the writer to use
231     */
232    private static void appendNewlineIfNecessary(StringBuffer buf,
233                                                 Writer out)
234            throws IOException {
235        int len = buf.length();
236
237        if ((len != 0) && (buf.charAt(len - 1) != '\n')) {
238            out.write('\n');
239        }
240    }
241
242    /**
243     * Writes the given number of spaces to the given writer.
244     *
245     * @param out {@code non-null;} where to write
246     * @param amt {@code >= 0;} the number of spaces to write
247     */
248    private static void writeSpaces(Writer out, int amt) throws IOException {
249        while (amt > 0) {
250            out.write(' ');
251            amt--;
252        }
253    }
254}
255