From 322cf8c5df3a416ff1fb8311a48af02899123fad Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 3 Sep 2019 10:46:51 -0700 Subject: [PATCH] Add ReservedWords builder for simpler construction The ReservedWordsBuilder can be used to more easily construct a ReservedWords implementation, including loading reserved words from a newline delimited file that uses "#" as comments. --- .../core/CaseInsensitiveReservedWords.java | 42 +++++ .../codegen/core/MappedReservedWords.java | 17 +- .../core/ReservedWordSymbolProvider.java | 54 +++++- .../codegen/core/ReservedWordsBuilder.java | 148 ++++++++++++++++ .../amazon/smithy/codegen/core/Symbol.java | 2 +- .../core/ReservedWordSymbolProviderTest.java | 24 ++- .../core/ReservedWordsBuilderTest.java | 56 ++++++ .../codegen/core/ReservedWordsTest.java | 4 +- .../amazon/smithy/codegen/core/words.txt | 8 + config/checkstyle/suppressions.xml | 1 + .../amazon/smithy/utils/StringUtils.java | 159 ++++++++++++++++++ 11 files changed, 501 insertions(+), 14 deletions(-) create mode 100644 codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CaseInsensitiveReservedWords.java create mode 100644 codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordsBuilder.java create mode 100644 codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsBuilderTest.java create mode 100644 codegen/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/words.txt diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CaseInsensitiveReservedWords.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CaseInsensitiveReservedWords.java new file mode 100644 index 00000000000..eef7cefebad --- /dev/null +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CaseInsensitiveReservedWords.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.codegen.core; + +import java.util.Locale; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +final class CaseInsensitiveReservedWords implements ReservedWords { + + private final Set words; + private final Function escaper; + + CaseInsensitiveReservedWords(Set words, Function escaper) { + this.words = words.stream().map(word -> word.toLowerCase(Locale.ENGLISH)).collect(Collectors.toSet()); + this.escaper = escaper; + } + + @Override + public String escape(String word) { + return isReserved(word) ? escaper.apply(word) : word; + } + + @Override + public boolean isReserved(String word) { + return words.contains(word.toLowerCase(Locale.ENGLISH)); + } +} diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/MappedReservedWords.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/MappedReservedWords.java index 6259300c4d3..16c4be05c22 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/MappedReservedWords.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/MappedReservedWords.java @@ -77,7 +77,8 @@ public static Builder builder() { @Override public String escape(String word) { String result = mappings.get(word); - if (result == null) { + + if (result == null && !caseInsensitiveMappings.isEmpty()) { result = caseInsensitiveMappings.get(word.toLowerCase(Locale.US)); } @@ -86,7 +87,11 @@ public String escape(String word) { @Override public boolean isReserved(String word) { - return mappings.containsKey(word) || caseInsensitiveMappings.containsKey(word.toLowerCase(Locale.US)); + if (mappings.containsKey(word)) { + return true; + } + + return !caseInsensitiveMappings.isEmpty() && caseInsensitiveMappings.containsKey(word.toLowerCase(Locale.US)); } /** @@ -111,7 +116,13 @@ public Builder put(String reservedWord, String conversion) { } /** - * Add a new case-insensitive reserved words. + * Add a new case-insensitive reserved word that converts the given + * reserved word to the given conversion string. + * + *

Note that the conversion string is used literally. The casing + * of the original word has no effect on the conversion. Use + * {@link ReservedWordsBuilder} for a case-insensitive reserved words + * implementation that can take casing into account. * * @param reservedWord Case-insensitive reserved word to convert. * @param conversion Word to convert to. diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProvider.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProvider.java index e7af093ebe8..8a44636523c 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProvider.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProvider.java @@ -15,6 +15,7 @@ package software.amazon.smithy.codegen.core; +import java.util.function.BiPredicate; import java.util.logging.Logger; import software.amazon.smithy.model.shapes.Shape; import software.amazon.smithy.utils.SmithyBuilder; @@ -44,6 +45,7 @@ public final class ReservedWordSymbolProvider implements SymbolProvider { private final ReservedWords namespaceReservedWords; private final ReservedWords nameReservedWords; private final ReservedWords memberReservedWords; + private final BiPredicate escapePredicate; private ReservedWordSymbolProvider(Builder builder) { this.delegate = SmithyBuilder.requiredState("symbolProvider", builder.delegate); @@ -51,6 +53,7 @@ private ReservedWordSymbolProvider(Builder builder) { this.namespaceReservedWords = resolveReserved(builder.namespaceReservedWords); this.nameReservedWords = resolveReserved(builder.nameReservedWords); this.memberReservedWords = resolveReserved(builder.memberReservedWords); + this.escapePredicate = builder.escapePredicate; } private static ReservedWords resolveReserved(ReservedWords specific) { @@ -69,11 +72,30 @@ public static Builder builder() { @Override public Symbol toSymbol(Shape shape) { Symbol upstream = delegate.toSymbol(shape); + + // Only escape symbols when the predicate returns true. + if (!escapePredicate.test(shape, upstream)) { + return upstream; + } + + String newName = convertWord("name", upstream.getName(), nameReservedWords); + String newNamespace = convertWord("namespace", upstream.getNamespace(), namespaceReservedWords); + String newDeclarationFile = convertWord("filename", upstream.getDeclarationFile(), filenameReservedWords); + String newDefinitionFile = convertWord("filename", upstream.getDefinitionFile(), filenameReservedWords); + + // Only create a new symbol when needed. + if (newName.equals(upstream.getName()) + && newNamespace.equals(upstream.getNamespace()) + && newDeclarationFile.equals(upstream.getDeclarationFile()) + && newDefinitionFile.equals(upstream.getDeclarationFile())) { + return upstream; + } + return upstream.toBuilder() - .name(nameReservedWords.escape(upstream.getName())) - .namespace(namespaceReservedWords.escape(upstream.getNamespace()), upstream.getNamespaceDelimiter()) - .declarationFile(filenameReservedWords.escape(upstream.getDeclarationFile())) - .definitionFile(filenameReservedWords.escape(upstream.getDefinitionFile())) + .name(newName) + .namespace(newNamespace, upstream.getNamespaceDelimiter()) + .declarationFile(newDeclarationFile) + .definitionFile(newDefinitionFile) .build(); } @@ -103,6 +125,7 @@ public static final class Builder { private ReservedWords namespaceReservedWords; private ReservedWords nameReservedWords; private ReservedWords memberReservedWords; + private BiPredicate escapePredicate = (shape, symbol) -> true; /** * Builds the provider. @@ -180,5 +203,28 @@ public Builder memberReservedWords(ReservedWords memberReservedWords) { this.memberReservedWords = memberReservedWords; return this; } + + /** + * Sets a predicate that is used to control when a shape + symbol + * combination should be checked if it's a reserved word. + * + *

The predicate is invoked when {@code toSymbol} is called. It + * is used to disable/enable escaping reserved words based on the + * shape and symbol. The given predicate accepts the {@code Shape} + * and the {@code Symbol} that was created for the shape and returns + * true if reserved word checks should be made or false if reserved + * word checks should not be made. For example, some code generators + * only escape words that have namespaces to differentiate between + * language built-ins and user-defined types. + * + *

By default, all symbols are checked for reserved words. + * + * @param escapePredicate Predicate that returns true if escaping should be checked. + * @return Returns the builder. + */ + public Builder escapePredicate(BiPredicate escapePredicate) { + this.escapePredicate = escapePredicate; + return this; + } } } diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordsBuilder.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordsBuilder.java new file mode 100644 index 00000000000..c6ccae69100 --- /dev/null +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/ReservedWordsBuilder.java @@ -0,0 +1,148 @@ +/* + * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +package software.amazon.smithy.codegen.core; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import software.amazon.smithy.utils.StringUtils; + +/** + * Builds a {@link ReservedWords} implementation from explicit + * mappings and from line-delimited files that contain reserved words. + */ +public class ReservedWordsBuilder { + + private final Map mappings = new HashMap<>(); + private final List delegates = new ArrayList<>(); + + /** + * Builds the reserved words. + * + * @return Returns the created reserved words implementation. + */ + public ReservedWords build() { + ReservedWords[] words = new ReservedWords[1 + delegates.size()]; + words[0] = new MappedReservedWords(mappings, Collections.emptyMap()); + for (int i = 0; i < delegates.size(); i++) { + words[i + 1] = delegates.get(i); + } + + return ReservedWords.compose(words); + } + + /** + * Add a new reserved words. + * + * @param reservedWord Reserved word to convert. + * @param conversion Word to convert to. + * @return Returns the builder. + */ + public ReservedWordsBuilder put(String reservedWord, String conversion) { + mappings.put(reservedWord, conversion); + return this; + } + + /** + * Load a list of case-sensitive, line-delimited reserved words from a file. + * + *

This method will escape words by prefixing them with "_". Use + * {@link #loadWords(URL, Function)} to customize how words are escaped. + * + *

Blank lines and lines that start with # are ignored. + * + * @param location URL of the file to load. + * @return Returns the builder. + */ + public ReservedWordsBuilder loadWords(URL location) { + return loadWords(location, ReservedWordsBuilder::escapeWithUnderscore); + } + + /** + * Load a list of case-sensitive, line-delimited reserved words from a file. + * + *

Blank lines and lines that start with # are ignored. + * + * @param location URL of the file to load. + * @param escaper Function used to escape reserved words. + * @return Returns the builder. + */ + public ReservedWordsBuilder loadWords(URL location, Function escaper) { + for (String word : readNonBlankNonCommentLines(location)) { + put(word, escaper.apply(word)); + } + return this; + } + + /** + * Load a list of case-insensitive, line-delimited reserved words from a file. + * + *

This method will escape words by prefixing them with "_". Use + * {@link #loadCaseInsensitiveWords(URL, Function)} to customize how words + * are escaped. + * + *

Blank lines and lines that start with # are ignored. + * + * @param location URL of the file to load. + * @return Returns the builder. + */ + public ReservedWordsBuilder loadCaseInsensitiveWords(URL location) { + return loadCaseInsensitiveWords(location, ReservedWordsBuilder::escapeWithUnderscore); + } + + /** + * Load a list of case-insensitive, line-delimited reserved words from a file. + * + *

Blank lines and lines that start with # are ignored. + * + * @param location URL of the file to load. + * @param escaper Function used to escape reserved words. + * @return Returns the builder. + */ + public ReservedWordsBuilder loadCaseInsensitiveWords(URL location, Function escaper) { + delegates.add(new CaseInsensitiveReservedWords(readNonBlankNonCommentLines(location), escaper)); + return this; + } + + private static String escapeWithUnderscore(String word) { + return "_" + word; + } + + private static Set readNonBlankNonCommentLines(URL url) { + try (InputStream is = url.openConnection().getInputStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { + return reader.lines() + .filter(StringUtils::isNotBlank) + .filter(line -> !line.startsWith("#")) + .map(word -> StringUtils.stripEnd(word, null)) + .collect(Collectors.toSet()); + } catch (IOException e) { + throw new UncheckedIOException("Error loading reserved words from " + url + ": " + e.getMessage(), e); + } + } +} diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java index c3df940e10a..0a90f160973 100644 --- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java +++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/Symbol.java @@ -348,7 +348,7 @@ public Builder references(List references) { * Add a symbol reference to indicate that this symbol points to * or contains references to other symbols. * - * @param reference Symbol that is referenced with no specific reference name. + * @param reference Symbol that is referenced. * @return Returns the builder. */ public Builder addReference(Symbol reference) { diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProviderTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProviderTest.java index bfafa7652dd..a4342728465 100644 --- a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProviderTest.java +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordSymbolProviderTest.java @@ -29,7 +29,7 @@ public void escapesReservedFilenames() { Shape s1 = StringShape.builder().id("foo.bar#Baz").build(); Shape s2 = StringShape.builder().id("foo.bar#Bam").build(); - ReservedWords reservedWords = MappedReservedWords.builder().put("/foo/bar/bam", "/rewritten").build(); + ReservedWords reservedWords = new ReservedWordsBuilder().put("/foo/bar/bam", "/rewritten").build(); MockProvider delegate = new MockProvider(); SymbolProvider provider = ReservedWordSymbolProvider.builder() .symbolProvider(delegate) @@ -50,7 +50,7 @@ public void escapesReservedNamespaces() { Shape s1 = StringShape.builder().id("foo.bar#Baz").build(); Shape s2 = StringShape.builder().id("foo.baz#Bam").build(); - ReservedWords reservedWords = MappedReservedWords.builder().put("foo.baz", "foo._baz").build(); + ReservedWords reservedWords = new ReservedWordsBuilder().put("foo.baz", "foo._baz").build(); MockProvider delegate = new MockProvider(); SymbolProvider provider = ReservedWordSymbolProvider.builder() .symbolProvider(delegate) @@ -69,7 +69,7 @@ public void escapesReservedNames() { Shape s1 = StringShape.builder().id("foo.bar#Baz").build(); Shape s2 = StringShape.builder().id("foo.baz#Bam").build(); - ReservedWords reservedWords = MappedReservedWords.builder().put("Bam", "_Bam").build(); + ReservedWords reservedWords = new ReservedWordsBuilder().put("Bam", "_Bam").build(); MockProvider delegate = new MockProvider(); SymbolProvider provider = ReservedWordSymbolProvider.builder() .symbolProvider(delegate) @@ -88,7 +88,7 @@ public void escapesReservedMemberNames() { Shape s1 = MemberShape.builder().id("foo.bar#Baz$foo").target("foo.baz#T").build(); Shape s2 = MemberShape.builder().id("foo.baz#Baz$baz").target("foo.baz#T").build(); - ReservedWords reservedWords = MappedReservedWords.builder().put("baz", "_baz").build(); + ReservedWords reservedWords = new ReservedWordsBuilder().put("baz", "_baz").build(); SymbolProvider delegate = new MockProvider(); SymbolProvider provider = ReservedWordSymbolProvider.builder() .symbolProvider(delegate) @@ -99,6 +99,22 @@ public void escapesReservedMemberNames() { assertThat(provider.toMemberName(s2), equalTo("_baz")); } + @Test + public void escapesOnlyWhenPredicateReturnsTrue() { + Shape stringShape = StringShape.builder().id("foo.baz#Bam").build(); + + ReservedWords reservedWords = new ReservedWordsBuilder().put("Bam", "_Bam").build(); + MockProvider delegate = new MockProvider(); + SymbolProvider provider = ReservedWordSymbolProvider.builder() + .symbolProvider(delegate) + .nameReservedWords(reservedWords) + .escapePredicate((shape, symbol) -> false) + .build(); + + delegate.mock = Symbol.builder().namespace("foo.baz", ".").name("Bam").build(); + assertThat(provider.toSymbol(stringShape).getName(), equalTo("Bam")); + } + private static final class MockProvider implements SymbolProvider { public Symbol mock; diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsBuilderTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsBuilderTest.java new file mode 100644 index 00000000000..bcfe4059f67 --- /dev/null +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsBuilderTest.java @@ -0,0 +1,56 @@ +package software.amazon.smithy.codegen.core; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import org.junit.jupiter.api.Test; + +public class ReservedWordsBuilderTest { + @Test + public void loadsWords() { + ReservedWords words = new ReservedWordsBuilder() + .loadWords(getClass().getResource("words.txt")) + .build(); + + basicWordTestTest(words, "_"); + assertThat(words.escape("Boolean"), equalTo("Boolean")); + } + + private void basicWordTestTest(ReservedWords words, String prefix) { + assertThat(words.escape("undefined"), equalTo(prefix + "undefined")); + assertThat(words.escape("null"), equalTo(prefix + "null")); + assertThat(words.escape("string"), equalTo(prefix + "string")); + assertThat(words.escape("boolean"), equalTo(prefix + "boolean")); + assertThat(words.escape("random"), equalTo("random")); + } + + @Test + public void loadsCaseInsensitiveWords() { + ReservedWords words = new ReservedWordsBuilder() + .loadCaseInsensitiveWords(getClass().getResource("words.txt")) + .build(); + + basicWordTestTest(words, "_"); + assertThat(words.escape("Boolean"), equalTo("_Boolean")); + } + + @Test + public void loadsWordsWithCustomEscaper() { + ReservedWords words = new ReservedWordsBuilder() + .loadWords(getClass().getResource("words.txt"), word -> "$" + word) + .build(); + + basicWordTestTest(words, "$"); + assertThat(words.escape("Boolean"), equalTo("Boolean")); + } + + @Test + public void loadsCaseInsensitiveWordsWithCustomEscaper() { + ReservedWords words = new ReservedWordsBuilder() + .loadCaseInsensitiveWords(getClass().getResource("words.txt"), word -> "$" + word) + .build(); + + basicWordTestTest(words, "$"); + assertThat(words.escape("Boolean"), equalTo("$Boolean")); + } +} diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsTest.java index 5f07f204e17..c81655491a2 100644 --- a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsTest.java +++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/ReservedWordsTest.java @@ -24,8 +24,8 @@ public class ReservedWordsTest { @Test public void composesImplementations() { - ReservedWords a = MappedReservedWords.builder().put("void", "_void").build(); - ReservedWords b = MappedReservedWords.builder().put("foo", "_foo").build(); + ReservedWords a = new ReservedWordsBuilder().put("void", "_void").build(); + ReservedWords b = new ReservedWordsBuilder().put("foo", "_foo").build(); ReservedWords composed = ReservedWords.compose(a, b); assertThat(composed.isReserved("void"), is(true)); diff --git a/codegen/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/words.txt b/codegen/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/words.txt new file mode 100644 index 00000000000..e9d5d24801d --- /dev/null +++ b/codegen/smithy-codegen-core/src/test/resources/software/amazon/smithy/codegen/core/words.txt @@ -0,0 +1,8 @@ +# This comment is ignored. The following blank line is ignored too. + +undefined +null + +# Another comment +string +boolean diff --git a/config/checkstyle/suppressions.xml b/config/checkstyle/suppressions.xml index 21281cc0192..cf12f067b26 100644 --- a/config/checkstyle/suppressions.xml +++ b/config/checkstyle/suppressions.xml @@ -19,4 +19,5 @@ "http://www.puppycrawl.com/dtds/suppressions_1_1.dtd"> + diff --git a/smithy-utils/src/main/java/software/amazon/smithy/utils/StringUtils.java b/smithy-utils/src/main/java/software/amazon/smithy/utils/StringUtils.java index cd0e9ed84ce..6157e9ec809 100644 --- a/smithy-utils/src/main/java/software/amazon/smithy/utils/StringUtils.java +++ b/smithy-utils/src/main/java/software/amazon/smithy/utils/StringUtils.java @@ -102,6 +102,11 @@ public final class StringUtils { */ private static final int PAD_LIMIT = 8192; + /** + * Represents a failed index search. + */ + private static final int INDEX_NOT_FOUND = -1; + /** *

{@code StringUtils} instances should NOT be constructed in * standard programming. Instead, the class should be used as @@ -896,6 +901,160 @@ public static String leftPad(final String str, final int size, String padStr) { } } + // Stripping + //----------------------------------------------------------------------- + /** + *

Strips any of a set of characters from the start and end of a String. + * This is similar to {@link String#trim()} but allows the characters + * to be stripped to be controlled.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}. + * + *

+     * StringUtils.strip(null, *)          = null
+     * StringUtils.strip("", *)            = ""
+     * StringUtils.strip("abc", null)      = "abc"
+     * StringUtils.strip("  abc", null)    = "abc"
+     * StringUtils.strip("abc  ", null)    = "abc"
+     * StringUtils.strip(" abc ", null)    = "abc"
+     * StringUtils.strip("  abcyx", "xyz") = "  abc"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + * @see Source + */ + public static String strip(String str, final String stripChars) { + if (isEmpty(str)) { + return str; + } + str = stripStart(str, stripChars); + return stripEnd(str, stripChars); + } + + /** + *

Strips any of a set of characters from the end of a String.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripEnd(null, *)          = null
+     * StringUtils.stripEnd("", *)            = ""
+     * StringUtils.stripEnd("abc", "")        = "abc"
+     * StringUtils.stripEnd("abc", null)      = "abc"
+     * StringUtils.stripEnd("  abc", null)    = "  abc"
+     * StringUtils.stripEnd("abc  ", null)    = "abc"
+     * StringUtils.stripEnd(" abc ", null)    = " abc"
+     * StringUtils.stripEnd("  abcyx", "xyz") = "  abc"
+     * StringUtils.stripEnd("120.00", ".0")   = "12"
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the set of characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + * @see Source + */ + public static String stripEnd(final String str, final String stripChars) { + int end; + if (str == null || (end = str.length()) == 0) { + return str; + } + + if (stripChars == null) { + while (end != 0 && Character.isWhitespace(str.charAt(end - 1))) { + end--; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (end != 0 && stripChars.indexOf(str.charAt(end - 1)) != INDEX_NOT_FOUND) { + end--; + } + } + return str.substring(0, end); + } + + /** + *

Strips any of a set of characters from the start of a String.

+ * + *

A {@code null} input String returns {@code null}. + * An empty string ("") input returns the empty string.

+ * + *

If the stripChars String is {@code null}, whitespace is + * stripped as defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripStart(null, *)          = null
+     * StringUtils.stripStart("", *)            = ""
+     * StringUtils.stripStart("abc", "")        = "abc"
+     * StringUtils.stripStart("abc", null)      = "abc"
+     * StringUtils.stripStart("  abc", null)    = "abc"
+     * StringUtils.stripStart("abc  ", null)    = "abc  "
+     * StringUtils.stripStart(" abc ", null)    = "abc "
+     * StringUtils.stripStart("yxabc  ", "xyz") = "abc  "
+     * 
+ * + * @param str the String to remove characters from, may be null + * @param stripChars the characters to remove, null treated as whitespace + * @return the stripped String, {@code null} if null String input + * @see Source + */ + public static String stripStart(final String str, final String stripChars) { + int strLen; + if (str == null || (strLen = str.length()) == 0) { + return str; + } + int start = 0; + if (stripChars == null) { + while (start != strLen && Character.isWhitespace(str.charAt(start))) { + start++; + } + } else if (stripChars.isEmpty()) { + return str; + } else { + while (start != strLen && stripChars.indexOf(str.charAt(start)) != INDEX_NOT_FOUND) { + start++; + } + } + return str.substring(start); + } + + /** + *

Strips whitespace from the start and end of a String returning + * an empty String if {@code null} input.

+ * + *

This is similar to {@link #trimToEmpty(String)} but removes whitespace. + * Whitespace is defined by {@link Character#isWhitespace(char)}.

+ * + *
+     * StringUtils.stripToEmpty(null)     = ""
+     * StringUtils.stripToEmpty("")       = ""
+     * StringUtils.stripToEmpty("   ")    = ""
+     * StringUtils.stripToEmpty("abc")    = "abc"
+     * StringUtils.stripToEmpty("  abc")  = "abc"
+     * StringUtils.stripToEmpty("abc  ")  = "abc"
+     * StringUtils.stripToEmpty(" abc ")  = "abc"
+     * StringUtils.stripToEmpty(" ab c ") = "ab c"
+     * 
+ * + * @param str the String to be stripped, may be null + * @return the trimmed String, or an empty String if {@code null} input + * @since 2.0 + * @see Source + */ + public static String stripToEmpty(final String str) { + return str == null ? EMPTY : strip(str, null); + } + // Wrapping //-----------------------------------------------------------------------