This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-cli.git
commit 35cf8df921caf3a71b05e6e7e450ee81e514bead Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Thu Aug 8 09:32:03 2024 -0400 Replace internal StringBuffer with StringBuilder --- pom.xml | 6 + .../java/org/apache/commons/cli/HelpFormatter.java | 261 ++++++++++++--------- .../org/apache/commons/cli/HelpFormatterTest.java | 18 +- 3 files changed, 172 insertions(+), 113 deletions(-) diff --git a/pom.xml b/pom.xml index 0abb6f2..1e05519 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,12 @@ <version>2.16.1</version> <scope>test</scope> </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <version>4.11.0</version> + <scope>test</scope> + </dependency> </dependencies> <properties> <project.build.outputTimestamp>2024-05-23T21:37:00Z</project.build.outputTimestamp> diff --git a/src/main/java/org/apache/commons/cli/HelpFormatter.java b/src/main/java/org/apache/commons/cli/HelpFormatter.java index 30e94cf..6593057 100644 --- a/src/main/java/org/apache/commons/cli/HelpFormatter.java +++ b/src/main/java/org/apache/commons/cli/HelpFormatter.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringReader; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -374,7 +375,6 @@ public class HelpFormatter { if (!group.isRequired()) { buff.append("["); } - final List<Option> optList = new ArrayList<>(group.getOptions()); if (getOptionComparator() != null) { Collections.sort(optList, getOptionComparator()); @@ -388,12 +388,135 @@ public class HelpFormatter { buff.append(" | "); } } - if (!group.isRequired()) { buff.append("]"); } } + /** + * Renders the specified Options and return the rendered Options in a StringBuffer. + * + * @param sb The StringBuffer to place the rendered Options into. + * @param width The number of characters to display per line + * @param options The command line Options + * @param leftPad the number of characters of padding to be prefixed to each line + * @param descPad the number of characters of padding to be prefixed to each description line + * @return the StringBuffer with the rendered Options contents. + * @throws IOException if an I/O error occurs. + */ + <A extends Appendable> A appendOptions(final A sb, final int width, final Options options, final int leftPad, final int descPad) throws IOException { + final String lpad = createPadding(leftPad); + final String dpad = createPadding(descPad); + // first create list containing only <lpad>-a,--aaa where + // -a is opt and --aaa is long opt; in parallel look for + // the longest opt string this list will be then used to + // sort options ascending + int max = 0; + final int maxSince = showSince ? determineMaxSinceLength(options) + leftPad : 0; + final List<StringBuilder> prefixList = new ArrayList<>(); + final List<Option> optList = options.helpOptions(); + if (getOptionComparator() != null) { + Collections.sort(optList, getOptionComparator()); + } + for (final Option option : optList) { + final StringBuilder optBuf = new StringBuilder(); + if (option.getOpt() == null) { + optBuf.append(lpad).append(" ").append(getLongOptPrefix()).append(option.getLongOpt()); + } else { + optBuf.append(lpad).append(getOptPrefix()).append(option.getOpt()); + if (option.hasLongOpt()) { + optBuf.append(',').append(getLongOptPrefix()).append(option.getLongOpt()); + } + } + if (option.hasArg()) { + final String argName = option.getArgName(); + if (argName != null && argName.isEmpty()) { + // if the option has a blank argname + optBuf.append(' '); + } else { + optBuf.append(option.hasLongOpt() ? longOptSeparator : " "); + optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">"); + } + } + + prefixList.add(optBuf); + max = Math.max(optBuf.length() + maxSince, max); + } + final int nextLineTabStop = max + descPad; + if (showSince) { + final StringBuilder optHeader = new StringBuilder(HEADER_OPTIONS).append(createPadding(max - maxSince - HEADER_OPTIONS.length() + leftPad)) + .append(HEADER_SINCE); + optHeader.append(createPadding(max - optHeader.length())).append(lpad).append(HEADER_DESCRIPTION); + appendWrappedText(sb, width, nextLineTabStop, optHeader.toString()); + sb.append(getNewLine()); + } + + int x = 0; + for (final Iterator<Option> it = optList.iterator(); it.hasNext();) { + final Option option = it.next(); + final StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString()); + if (optBuf.length() < max) { + optBuf.append(createPadding(max - maxSince - optBuf.length())); + if (showSince) { + optBuf.append(lpad).append(option.getSince() == null ? "-" : option.getSince()); + } + optBuf.append(createPadding(max - optBuf.length())); + } + optBuf.append(dpad); + + if (deprecatedFormatFunc != null && option.isDeprecated()) { + optBuf.append(deprecatedFormatFunc.apply(option).trim()); + } else if (option.getDescription() != null) { + optBuf.append(option.getDescription()); + } + appendWrappedText(sb, width, nextLineTabStop, optBuf.toString()); + if (it.hasNext()) { + sb.append(getNewLine()); + } + } + return sb; + } + + /** + * Renders the specified text and return the rendered Options in a StringBuffer. + * + * @param <A> The Appendable implementation. + * @param appendable The StringBuffer to place the rendered text into. + * @param width The number of characters to display per line + * @param nextLineTabStop The position on the next line for the first tab. + * @param text The text to be rendered. + * @return the StringBuffer with the rendered Options contents. + * @throws IOException if an I/O error occurs. + */ + <A extends Appendable> A appendWrappedText(final A appendable, final int width, final int nextLineTabStop, final String text) throws IOException { + String render = text; + int nextLineTabStopPos = nextLineTabStop; + int pos = findWrapPos(render, width, 0); + if (pos == -1) { + appendable.append(rtrim(render)); + return appendable; + } + appendable.append(rtrim(render.substring(0, pos))).append(getNewLine()); + if (nextLineTabStopPos >= width) { + // stops infinite loop happening + nextLineTabStopPos = 1; + } + // all following lines must be padded with nextLineTabStop space characters + final String padding = createPadding(nextLineTabStopPos); + while (true) { + render = padding + render.substring(pos).trim(); + pos = findWrapPos(render, width, 0); + if (pos == -1) { + appendable.append(render); + return appendable; + } + if (render.length() > width && pos == nextLineTabStopPos - 1) { + pos = width; + } + appendable.append(rtrim(render.substring(0, pos))).append(getNewLine()); + } + } + /** * Creates a String of padding of length {@code len}. * @@ -407,6 +530,12 @@ public class HelpFormatter { return new String(padding); } + private int determineMaxSinceLength(final Options options) { + final int minLen = HEADER_SINCE.length(); + final int len = options.getOptions().stream().map(o -> o.getSince() == null ? minLen : o.getSince().length()).max(Integer::compareTo).orElse(minLen); + return len < minLen ? minLen : len; + } + /** * Finds the next text wrap position after {@code startPos} for the text in {@code text} with the column width * {@code width}. The wrap point is the last position before startPos+width having a whitespace character (space, @@ -443,7 +572,6 @@ public class HelpFormatter { } // if we didn't find one, simply chop at startPos+width pos = startPos + width; - return pos == text.length() ? -1 : pos; } @@ -685,9 +813,12 @@ public class HelpFormatter { * @param descPad the number of characters of padding to be prefixed to each description line */ public void printOptions(final PrintWriter pw, final int width, final Options options, final int leftPad, final int descPad) { - final StringBuffer sb = new StringBuffer(); - renderOptions(sb, width, options, leftPad, descPad); - pw.println(sb.toString()); + try { + pw.println(appendOptions(new StringBuilder(), width, options, leftPad, descPad)); + } catch (final IOException e) { + // Cannot happen + throw new UncheckedIOException(e); + } } /** @@ -759,9 +890,7 @@ public class HelpFormatter { * @param text The text to be written to the PrintWriter */ public void printWrapped(final PrintWriter pw, final int width, final int nextLineTabStop, final String text) { - final StringBuffer sb = new StringBuffer(text.length()); - renderWrappedTextBlock(sb, width, nextLineTabStop, text); - pw.println(sb.toString()); + pw.println(renderWrappedTextBlock(new StringBuilder(text.length()), width, nextLineTabStop, text)); } /** @@ -775,12 +904,6 @@ public class HelpFormatter { printWrapped(pw, width, 0, text); } - private int determineMaxSinceLength(final Options options) { - int minLen = HEADER_SINCE.length(); - int len = options.getOptions().stream().map(o -> o.getSince() == null ? minLen : o.getSince().length()).max(Integer::compareTo).orElse(minLen); - return len < minLen ? minLen : len; - } - /** * Renders the specified Options and return the rendered Options in a StringBuffer. * @@ -793,76 +916,12 @@ public class HelpFormatter { * @return the StringBuffer with the rendered Options contents. */ protected StringBuffer renderOptions(final StringBuffer sb, final int width, final Options options, final int leftPad, final int descPad) { - final String lpad = createPadding(leftPad); - final String dpad = createPadding(descPad); - // first create list containing only <lpad>-a,--aaa where - // -a is opt and --aaa is long opt; in parallel look for - // the longest opt string this list will be then used to - // sort options ascending - int max = 0; - int maxSince = showSince ? determineMaxSinceLength(options) + leftPad : 0; - final List<StringBuilder> prefixList = new ArrayList<>(); - final List<Option> optList = options.helpOptions(); - if (getOptionComparator() != null) { - Collections.sort(optList, getOptionComparator()); - } - for (final Option option : optList) { - final StringBuilder optBuf = new StringBuilder(); - if (option.getOpt() == null) { - optBuf.append(lpad).append(" ").append(getLongOptPrefix()).append(option.getLongOpt()); - } else { - optBuf.append(lpad).append(getOptPrefix()).append(option.getOpt()); - if (option.hasLongOpt()) { - optBuf.append(',').append(getLongOptPrefix()).append(option.getLongOpt()); - } - } - if (option.hasArg()) { - final String argName = option.getArgName(); - if (argName != null && argName.isEmpty()) { - // if the option has a blank argname - optBuf.append(' '); - } else { - optBuf.append(option.hasLongOpt() ? longOptSeparator : " "); - optBuf.append("<").append(argName != null ? option.getArgName() : getArgName()).append(">"); - } - } - - prefixList.add(optBuf); - max = Math.max(optBuf.length() + maxSince, max); - } - final int nextLineTabStop = max + descPad; - if (showSince) { - StringBuilder optHeader = new StringBuilder(HEADER_OPTIONS).append(createPadding(max - maxSince - HEADER_OPTIONS.length() + leftPad)) - .append(HEADER_SINCE); - optHeader.append(createPadding(max - optHeader.length())).append(lpad).append(HEADER_DESCRIPTION); - renderWrappedText(sb, width, nextLineTabStop, optHeader.toString()); - sb.append(getNewLine()); - } - - int x = 0; - for (final Iterator<Option> it = optList.iterator(); it.hasNext();) { - final Option option = it.next(); - final StringBuilder optBuf = new StringBuilder(prefixList.get(x++).toString()); - if (optBuf.length() < max) { - optBuf.append(createPadding(max - maxSince - optBuf.length())); - if (showSince) { - optBuf.append(lpad).append(option.getSince() == null ? "-" : option.getSince()); - } - optBuf.append(createPadding(max - optBuf.length())); - } - optBuf.append(dpad); - - if (deprecatedFormatFunc != null && option.isDeprecated()) { - optBuf.append(deprecatedFormatFunc.apply(option).trim()); - } else if (option.getDescription() != null) { - optBuf.append(option.getDescription()); - } - renderWrappedText(sb, width, nextLineTabStop, optBuf.toString()); - if (it.hasNext()) { - sb.append(getNewLine()); - } + try { + return appendOptions(sb, width, options, leftPad, descPad); + } catch (final IOException e) { + // Cannot happen + throw new UncheckedIOException(e); } - return sb; } /** @@ -876,31 +935,11 @@ public class HelpFormatter { * @return the StringBuffer with the rendered Options contents. */ protected StringBuffer renderWrappedText(final StringBuffer sb, final int width, final int nextLineTabStop, final String text) { - String render = text; - int nextLineTabStopPos = nextLineTabStop; - int pos = findWrapPos(render, width, 0); - if (pos == -1) { - sb.append(rtrim(render)); - return sb; - } - sb.append(rtrim(render.substring(0, pos))).append(getNewLine()); - if (nextLineTabStopPos >= width) { - // stops infinite loop happening - nextLineTabStopPos = 1; - } - // all following lines must be padded with nextLineTabStop space characters - final String padding = createPadding(nextLineTabStopPos); - while (true) { - render = padding + render.substring(pos).trim(); - pos = findWrapPos(render, width, 0); - if (pos == -1) { - sb.append(render); - return sb; - } - if (render.length() > width && pos == nextLineTabStopPos - 1) { - pos = width; - } - sb.append(rtrim(render.substring(0, pos))).append(getNewLine()); + try { + return appendWrappedText(sb, width, nextLineTabStop, text); + } catch (final IOException e) { + // Cannot happen. + throw new UncheckedIOException(e); } } @@ -913,7 +952,7 @@ public class HelpFormatter { * @param nextLineTabStop The position on the next line for the first tab. * @param text The text to be rendered. */ - private Appendable renderWrappedTextBlock(final StringBuffer sb, final int width, final int nextLineTabStop, final String text) { + private <A extends Appendable> A renderWrappedTextBlock(final A sb, final int width, final int nextLineTabStop, final String text) { try { final BufferedReader in = new BufferedReader(new StringReader(text)); String line; @@ -924,7 +963,7 @@ public class HelpFormatter { } else { firstLine = false; } - renderWrappedText(sb, width, nextLineTabStop, line); + appendWrappedText(sb, width, nextLineTabStop, line); } } catch (final IOException e) { // NOPMD // cannot happen diff --git a/src/test/java/org/apache/commons/cli/HelpFormatterTest.java b/src/test/java/org/apache/commons/cli/HelpFormatterTest.java index 09b449f..fc9904e 100644 --- a/src/test/java/org/apache/commons/cli/HelpFormatterTest.java +++ b/src/test/java/org/apache/commons/cli/HelpFormatterTest.java @@ -21,11 +21,15 @@ import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.StringWriter; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -36,6 +40,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + /** * Test case for the HelpFormatter class. */ @@ -657,7 +662,7 @@ public class HelpFormatterTest { } @Test - public void testRenderSince() { + public void testRenderSince() throws IOException { final String[] expected = {"Options Since Description", " -n,--no-since - Description for n", " -W,--with-since 1.19.0 Descripton for W"}; final Options options = new Options() @@ -668,6 +673,10 @@ public class HelpFormatterTest { final StringBuffer sb = new StringBuffer(); formatter.renderOptions(sb, 50, options, 2, 5); assertArrayEquals(expected, sb.toString().split(System.lineSeparator())); + // check internal exception handling for coverage + final HelpFormatter spy = spy(formatter); + when(spy.appendOptions(sb, 50, options, 2, 5)).thenThrow(IOException.class); + assertThrows(UncheckedIOException.class, () -> spy.renderOptions(sb, 50, options, 2, 5)); } @Test @@ -706,7 +715,7 @@ public class HelpFormatterTest { } @Test - public void testRenderWrappedTextSingleLine() { + public void testRenderWrappedTextSingleLine() throws IOException { // single line text final int width = 12; final int padding = 0; @@ -716,6 +725,11 @@ public class HelpFormatterTest { final StringBuffer sb = new StringBuffer(); new HelpFormatter().renderWrappedText(sb, width, padding, text); assertEquals(expected, sb.toString(), "single line text"); + // check internal exception handling for coverage + final HelpFormatter spy = spy(new HelpFormatter()); + when(spy.appendWrappedText(sb, width, padding, text)).thenThrow(IOException.class); + assertThrows(UncheckedIOException.class, () -> spy.renderWrappedText(sb, width, padding, text)); + } @Test