1/*
2 * Copyright 2013, Google Inc.
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are
7 * met:
8 *
9 *     * Redistributions of source code must retain the above copyright
10 * notice, this list of conditions and the following disclaimer.
11 *     * Redistributions in binary form must reproduce the above
12 * copyright notice, this list of conditions and the following disclaimer
13 * in the documentation and/or other materials provided with the
14 * distribution.
15 *     * Neither the name of Google Inc. nor the names of its
16 * contributors may be used to endorse or promote products derived from
17 * this software without specific prior written permission.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 */
31
32package org.jf.util;
33
34import javax.annotation.Nonnull;
35import javax.annotation.Nullable;
36import java.io.PrintStream;
37import java.text.BreakIterator;
38import java.util.Iterator;
39
40public class StringWrapper {
41    /**
42     * Splits the given string into lines of maximum width maxWidth. The splitting is done using the current locale's
43     * rules for splitting lines.
44     *
45     * @param string The string to split
46     * @param maxWidth The maximum length of any line
47     * @return An iterable of Strings containing the wrapped lines
48     */
49    public static Iterable<String> wrapStringOnBreaks(@Nonnull final String string, final int maxWidth) {
50        // TODO: should we strip any trailing newlines?
51        final BreakIterator breakIterator = BreakIterator.getLineInstance();
52        breakIterator.setText(string);
53
54        return new Iterable<String>() {
55            @Override
56            public Iterator<String> iterator() {
57                return new Iterator<String>() {
58                    private int currentLineStart = 0;
59                    private boolean nextLineSet = false;
60                    private String nextLine;
61
62                    @Override
63                    public boolean hasNext() {
64                        if (!nextLineSet) {
65                            calculateNext();
66                        }
67                        return nextLine != null;
68                    }
69
70                    private void calculateNext() {
71                        int lineEnd = currentLineStart;
72                        while (true) {
73                            lineEnd = breakIterator.following(lineEnd);
74                            if (lineEnd == BreakIterator.DONE) {
75                                lineEnd = breakIterator.last();
76                                if (lineEnd <= currentLineStart) {
77                                    nextLine = null;
78                                    nextLineSet = true;
79                                    return;
80                                }
81                                break;
82                            }
83
84                            if (lineEnd - currentLineStart > maxWidth) {
85                                lineEnd = breakIterator.preceding(lineEnd);
86                                if (lineEnd <= currentLineStart) {
87                                    lineEnd = currentLineStart + maxWidth;
88                                }
89                                break;
90                            }
91
92                            if (string.charAt(lineEnd-1) == '\n') {
93                                nextLine = string.substring(currentLineStart, lineEnd-1);
94                                nextLineSet = true;
95                                currentLineStart = lineEnd;
96                                return;
97                            }
98                        }
99                        nextLine = string.substring(currentLineStart, lineEnd);
100                        nextLineSet = true;
101                        currentLineStart = lineEnd;
102                    }
103
104                    @Override
105                    public String next() {
106                        String ret = nextLine;
107                        nextLine = null;
108                        nextLineSet = false;
109                        return ret;
110                    }
111
112                    @Override
113                    public void remove() {
114                        throw new UnsupportedOperationException();
115                    }
116                };
117            }
118        };
119    }
120
121    /**
122     * Splits the given string into lines using on any embedded newlines, and wrapping the text as needed to conform to
123     * the given maximum line width.
124     *
125     * This uses and assumes unix-style newlines
126     *
127     * @param str The string to split
128     * @param maxWidth The maximum length of any line
129     * @param output If given, try to use this array as the return value. If there are more values than will fit
130     *               into the array, a new array will be allocated and returned, while the given array will be filled
131     *               with as many lines as would fit.
132     * @return The split lines from the original, as an array of Strings. The returned array may be larger than the
133     *         number of lines. If this is the case, the end of the split lines will be denoted by a null entry in the
134     *         array. If there is no null entry, then the size of the array exactly matches the number of lines.
135     *         The returned lines will not contain an ending newline
136     */
137    public static String[] wrapString(@Nonnull String str, int maxWidth, @Nullable String[] output) {
138        if (output == null) {
139            output = new String[(int)((str.length() / maxWidth) * 1.5d + 1)];
140        }
141
142        int lineStart = 0;
143        int arrayIndex = 0;
144        int i;
145        for (i=0; i<str.length(); i++) {
146            char c = str.charAt(i);
147
148            if (c == '\n') {
149                output = addString(output, str.substring(lineStart, i), arrayIndex++);
150                lineStart = i+1;
151            } else if (i - lineStart == maxWidth) {
152                output = addString(output, str.substring(lineStart, i), arrayIndex++);
153                lineStart = i;
154            }
155        }
156        if (lineStart != i || i == 0) {
157            output = addString(output, str.substring(lineStart), arrayIndex++, output.length+1);
158        }
159
160        if (arrayIndex < output.length) {
161            output[arrayIndex] = null;
162        }
163        return output;
164    }
165
166    private static String[] addString(@Nonnull String[] arr, String str, int index) {
167        if (index >= arr.length) {
168            arr = enlargeArray(arr, (int)(Math.ceil((arr.length + 1) * 1.5)));
169        }
170
171        arr[index] = str;
172        return arr;
173    }
174
175    private static String[] addString(@Nonnull String[] arr, String str, int index, int newLength) {
176        if (index >= arr.length) {
177            arr = enlargeArray(arr, newLength);
178        }
179
180        arr[index] = str;
181        return arr;
182    }
183
184    private static String[] enlargeArray(String[] arr, int newLength) {
185        String[] newArr = new String[newLength];
186        System.arraycopy(arr, 0, newArr, 0, arr.length);
187        return newArr;
188    }
189
190    public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string) {
191        printWrappedString(stream, string, ConsoleUtil.getConsoleWidth());
192    }
193
194    public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string, int maxWidth) {
195        for (String str: wrapStringOnBreaks(string, maxWidth)) {
196            stream.println(str);
197        }
198    }
199}
200