You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by ch...@apache.org on 2016/11/13 20:34:04 UTC

[01/14] [text] TEXT-23: adding unit test for AggregateTranslator

Repository: commons-text
Updated Branches:
  refs/heads/master ad7ee397a -> a408a7cfa


TEXT-23: adding unit test for AggregateTranslator


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/2f55bd15
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/2f55bd15
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/2f55bd15

Branch: refs/heads/master
Commit: 2f55bd1520d73c3c46d48b9066107d37f38926c4
Parents: e330798
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 08:31:45 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 08:31:45 2016 -0500

----------------------------------------------------------------------
 .../text/translate/AggregateTranslatorTest.java | 56 ++++++++++++++++++++
 1 file changed, 56 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/2f55bd15/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java b/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java
new file mode 100644
index 0000000..799c3e9
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/translate/AggregateTranslatorTest.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text.translate;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.lang.reflect.Field;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+/**
+ * Unit tests for {@link org.apache.commons.text.translate.AggregateTranslator}.
+ */
+public class AggregateTranslatorTest {
+
+    @Test
+    public void testNullConstructor() throws NoSuchFieldException, IllegalAccessException {
+        org.apache.commons.text.translate.AggregateTranslator subject = new org.apache.commons.text.translate.AggregateTranslator(null);
+        Field field = AggregateTranslator.class.getDeclaredField("translators");
+        field.setAccessible(Boolean.TRUE);
+        assertNull(field.get(subject));
+    }
+
+    @Test
+    public void testNonNull() throws IOException{
+        CharSequenceTranslator translator1 = new org.apache.commons.text.translate.LookupTranslator(new CharSequence[][] { { "one", "two" } });
+        CharSequenceTranslator translator2 = new org.apache.commons.text.translate.LookupTranslator(new CharSequence[][] { { "three", "four" } });
+        org.apache.commons.text.translate.AggregateTranslator subject = new org.apache.commons.text.translate.AggregateTranslator(translator1, translator2);
+        final StringWriter out1 = new StringWriter();
+        final int result1 = subject.translate(new StringBuffer("one"), 0, out1);
+        assertEquals("Incorrect codepoint consumption", 3, result1);
+        assertEquals("Incorrect value", "two", out1.toString());
+        final StringWriter out2 = new StringWriter();
+        final int result2 = subject.translate(new StringBuffer("three"), 0, out2);
+        assertEquals("Incorrect codepoint consumption", 5, result2);
+        assertEquals("Incorrect value", "four", out2.toString());
+    }
+
+}


[09/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/StrBuilder.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/StrBuilder.java b/src/main/java/org/apache/commons/text/StrBuilder.java
new file mode 100644
index 0000000..5aa4a31
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/StrBuilder.java
@@ -0,0 +1,3117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Serializable;
+import java.io.Writer;
+import java.nio.CharBuffer;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Builds a string from constituent parts providing a more flexible and powerful API
+ * than StringBuffer.
+ * <p>
+ * The main differences from StringBuffer/StringBuilder are:
+ * </p>
+ * <ul>
+ * <li>Not synchronized</li>
+ * <li>Not final</li>
+ * <li>Subclasses have direct access to character array</li>
+ * <li>Additional methods
+ *  <ul>
+ *   <li>appendWithSeparators - adds an array of values, with a separator</li>
+ *   <li>appendPadding - adds a length padding characters</li>
+ *   <li>appendFixedLength - adds a fixed width field to the builder</li>
+ *   <li>toCharArray/getChars - simpler ways to get a range of the character array</li>
+ *   <li>delete - delete char or string</li>
+ *   <li>replace - search and replace for a char or string</li>
+ *   <li>leftString/rightString/midString - substring without exceptions</li>
+ *   <li>contains - whether the builder contains a char or string</li>
+ *   <li>size/clear/isEmpty - collections style API methods</li>
+ *  </ul>
+ * </li>
+ * <li>Views
+ *  <ul>
+ *   <li>asTokenizer - uses the internal buffer as the source of a StrTokenizer</li>
+ *   <li>asReader - uses the internal buffer as the source of a Reader</li>
+ *   <li>asWriter - allows a Writer to write directly to the internal buffer</li>
+ *  </ul>
+ * </li>
+ * </ul>
+ * <p>
+ * The aim has been to provide an API that mimics very closely what StringBuffer
+ * provides, but with additional methods. It should be noted that some edge cases,
+ * with invalid indices or null input, have been altered - see individual methods.
+ * The biggest of these changes is that by default, null will not output the text
+ * 'null'. This can be controlled by a property, {@link #setNullText(String)}.
+ * <p>
+ *
+ */
+public class StrBuilder implements CharSequence, Appendable, Serializable, Builder<String> {
+
+    /**
+     * The extra capacity for new builders.
+     */
+    static final int CAPACITY = 32;
+
+    /**
+     * Required for serialization support.
+     * 
+     * @see java.io.Serializable
+     */
+    private static final long serialVersionUID = 7628716375283629643L;
+
+    /** Internal data storage. */
+    protected char[] buffer; // TODO make private?
+    /** Current size of the buffer. */
+    protected int size; // TODO make private?
+    /** The new line. */
+    private String newLine;
+    /** The null text. */
+    private String nullText;
+
+    //-----------------------------------------------------------------------
+    /**
+     * Constructor that creates an empty builder initial capacity 32 characters.
+     */
+    public StrBuilder() {
+        this(CAPACITY);
+    }
+
+    /**
+     * Constructor that creates an empty builder the specified initial capacity.
+     *
+     * @param initialCapacity  the initial capacity, zero or less will be converted to 32
+     */
+    public StrBuilder(int initialCapacity) {
+        super();
+        if (initialCapacity <= 0) {
+            initialCapacity = CAPACITY;
+        }
+        buffer = new char[initialCapacity];
+    }
+
+    /**
+     * Constructor that creates a builder from the string, allocating
+     * 32 extra characters for growth.
+     *
+     * @param str  the string to copy, null treated as blank string
+     */
+    public StrBuilder(final String str) {
+        super();
+        if (str == null) {
+            buffer = new char[CAPACITY];
+        } else {
+            buffer = new char[str.length() + CAPACITY];
+            append(str);
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the text to be appended when a new line is added.
+     *
+     * @return the new line text, null means use system default
+     */
+    public String getNewLineText() {
+        return newLine;
+    }
+
+    /**
+     * Sets the text to be appended when a new line is added.
+     *
+     * @param newLine  the new line text, null means use system default
+     * @return this, to enable chaining
+     */
+    public StrBuilder setNewLineText(final String newLine) {
+        this.newLine = newLine;
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the text to be appended when null is added.
+     *
+     * @return the null text, null means no append
+     */
+    public String getNullText() {
+        return nullText;
+    }
+
+    /**
+     * Sets the text to be appended when null is added.
+     *
+     * @param nullText  the null text, null means no append
+     * @return this, to enable chaining
+     */
+    public StrBuilder setNullText(String nullText) {
+        if (nullText != null && nullText.isEmpty()) {
+            nullText = null;
+        }
+        this.nullText = nullText;
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the length of the string builder.
+     *
+     * @return the length
+     */
+    @Override
+    public int length() {
+        return size;
+    }
+
+    /**
+     * Updates the length of the builder by either dropping the last characters
+     * or adding filler of Unicode zero.
+     *
+     * @param length  the length to set to, must be zero or positive
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the length is negative
+     */
+    public StrBuilder setLength(final int length) {
+        if (length < 0) {
+            throw new StringIndexOutOfBoundsException(length);
+        }
+        if (length < size) {
+            size = length;
+        } else if (length > size) {
+            ensureCapacity(length);
+            final int oldEnd = size;
+            final int newEnd = length;
+            size = length;
+            for (int i = oldEnd; i < newEnd; i++) {
+                buffer[i] = '\0';
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the current size of the internal character array buffer.
+     *
+     * @return the capacity
+     */
+    public int capacity() {
+        return buffer.length;
+    }
+
+    /**
+     * Checks the capacity and ensures that it is at least the size specified.
+     *
+     * @param capacity  the capacity to ensure
+     * @return this, to enable chaining
+     */
+    public StrBuilder ensureCapacity(final int capacity) {
+        if (capacity > buffer.length) {
+            final char[] old = buffer;
+            buffer = new char[capacity * 2];
+            System.arraycopy(old, 0, buffer, 0, size);
+        }
+        return this;
+    }
+
+    /**
+     * Minimizes the capacity to the actual length of the string.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder minimizeCapacity() {
+        if (buffer.length > length()) {
+            final char[] old = buffer;
+            buffer = new char[length()];
+            System.arraycopy(old, 0, buffer, 0, size);
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the length of the string builder.
+     * <p>
+     * This method is the same as {@link #length()} and is provided to match the
+     * API of Collections.
+     *
+     * @return the length
+     */
+    public int size() {
+        return size;
+    }
+
+    /**
+     * Checks is the string builder is empty (convenience Collections API style method).
+     * <p>
+     * This method is the same as checking {@link #length()} and is provided to match the
+     * API of Collections.
+     *
+     * @return <code>true</code> if the size is <code>0</code>.
+     */
+    public boolean isEmpty() {
+        return size == 0;
+    }
+
+    /**
+     * Clears the string builder (convenience Collections API style method).
+     * <p>
+     * This method does not reduce the size of the internal character buffer.
+     * To do that, call <code>clear()</code> followed by {@link #minimizeCapacity()}.
+     * <p>
+     * This method is the same as {@link #setLength(int)} called with zero
+     * and is provided to match the API of Collections.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder clear() {
+        size = 0;
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the character at the specified index.
+     *
+     * @see #setCharAt(int, char)
+     * @see #deleteCharAt(int)
+     * @param index  the index to retrieve, must be valid
+     * @return the character at the index
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    @Override
+    public char charAt(final int index) {
+        if (index < 0 || index >= length()) {
+            throw new StringIndexOutOfBoundsException(index);
+        }
+        return buffer[index];
+    }
+
+    /**
+     * Sets the character at the specified index.
+     *
+     * @see #charAt(int)
+     * @see #deleteCharAt(int)
+     * @param index  the index to set
+     * @param ch  the new character
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder setCharAt(final int index, final char ch) {
+        if (index < 0 || index >= length()) {
+            throw new StringIndexOutOfBoundsException(index);
+        }
+        buffer[index] = ch;
+        return this;
+    }
+
+    /**
+     * Deletes the character at the specified index.
+     *
+     * @see #charAt(int)
+     * @see #setCharAt(int, char)
+     * @param index  the index to delete
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder deleteCharAt(final int index) {
+        if (index < 0 || index >= size) {
+            throw new StringIndexOutOfBoundsException(index);
+        }
+        deleteImpl(index, index + 1, 1);
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Copies the builder's character array into a new character array.
+     *
+     * @return a new array that represents the contents of the builder
+     */
+    public char[] toCharArray() {
+        if (size == 0) {
+            return new char[0];
+        }
+        final char chars[] = new char[size];
+        System.arraycopy(buffer, 0, chars, 0, size);
+        return chars;
+    }
+
+    /**
+     * Copies part of the builder's character array into a new character array.
+     *
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param endIndex  the end index, exclusive, must be valid except that
+     *  if too large it is treated as end of string
+     * @return a new array that holds part of the contents of the builder
+     * @throws IndexOutOfBoundsException if startIndex is invalid,
+     *  or if endIndex is invalid (but endIndex greater than size is valid)
+     */
+    public char[] toCharArray(final int startIndex, int endIndex) {
+        endIndex = validateRange(startIndex, endIndex);
+        final int len = endIndex - startIndex;
+        if (len == 0) {
+            return new char[0];
+        }
+        final char chars[] = new char[len];
+        System.arraycopy(buffer, startIndex, chars, 0, len);
+        return chars;
+    }
+
+    /**
+     * Copies the character array into the specified array.
+     *
+     * @param destination  the destination array, null will cause an array to be created
+     * @return the input array, unless that was null or too small
+     */
+    public char[] getChars(char[] destination) {
+        final int len = length();
+        if (destination == null || destination.length < len) {
+            destination = new char[len];
+        }
+        System.arraycopy(buffer, 0, destination, 0, len);
+        return destination;
+    }
+
+    /**
+     * Copies the character array into the specified array.
+     *
+     * @param startIndex  first index to copy, inclusive, must be valid
+     * @param endIndex  last index, exclusive, must be valid
+     * @param destination  the destination array, must not be null or too small
+     * @param destinationIndex  the index to start copying in destination
+     * @throws NullPointerException if the array is null
+     * @throws IndexOutOfBoundsException if any index is invalid
+     */
+    public void getChars(final int startIndex, final int endIndex, final char destination[], final int destinationIndex) {
+        if (startIndex < 0) {
+            throw new StringIndexOutOfBoundsException(startIndex);
+        }
+        if (endIndex < 0 || endIndex > length()) {
+            throw new StringIndexOutOfBoundsException(endIndex);
+        }
+        if (startIndex > endIndex) {
+            throw new StringIndexOutOfBoundsException("end < start");
+        }
+        System.arraycopy(buffer, startIndex, destination, destinationIndex, endIndex - startIndex);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * If possible, reads chars from the provided {@link Readable} directly into underlying
+     * character buffer without making extra copies.
+     *
+     * @param readable  object to read from
+     * @return the number of characters read
+     * @throws IOException if an I/O error occurs
+     *
+     * @since 3.4
+     * @see #appendTo(Appendable)
+     */
+    public int readFrom(final Readable readable) throws IOException {
+        final int oldSize = size;
+        if (readable instanceof Reader) {
+            final Reader r = (Reader) readable;
+            ensureCapacity(size + 1);
+            int read;
+            while ((read = r.read(buffer, size, buffer.length - size)) != -1) {
+                size += read;
+                ensureCapacity(size + 1);
+            }
+        } else if (readable instanceof CharBuffer) {
+            final CharBuffer cb = (CharBuffer) readable;
+            final int remaining = cb.remaining();
+            ensureCapacity(size + remaining);
+            cb.get(buffer, size, remaining);
+            size += remaining;
+        } else {
+            while (true) {
+                ensureCapacity(size + 1);
+                final CharBuffer buf = CharBuffer.wrap(buffer, size, buffer.length - size);
+                final int read = readable.read(buf);
+                if (read == -1) {
+                    break;
+                }
+                size += read;
+            }
+        }
+        return size - oldSize;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends the new line string to this string builder.
+     * <p>
+     * The new line string can be altered using {@link #setNewLineText(String)}.
+     * This might be used to force the output to always use Unix line endings
+     * even when on Windows.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendNewLine() {
+        if (newLine == null)  {
+            append(System.getProperty("line.separator"));
+            return this;
+        }
+        return append(newLine);
+    }
+
+    /**
+     * Appends the text representing <code>null</code> to this string builder.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendNull() {
+        if (nullText == null)  {
+            return this;
+        }
+        return append(nullText);
+    }
+
+    /**
+     * Appends an object to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param obj  the object to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final Object obj) {
+        if (obj == null) {
+            return appendNull();
+        }
+        if (obj instanceof CharSequence) {
+            return append((CharSequence) obj);
+        }
+        return append(obj.toString());
+    }
+
+    /**
+     * Appends a CharSequence to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param seq  the CharSequence to append
+     * @return this, to enable chaining
+     * @since 3.0
+     */
+    @Override
+    public StrBuilder append(final CharSequence seq) {
+        if (seq == null) {
+            return appendNull();
+        }
+        if (seq instanceof StrBuilder) {
+            return append((StrBuilder) seq);
+        }
+        if (seq instanceof StringBuilder) {
+            return append((StringBuilder) seq);
+        }
+        if (seq instanceof StringBuffer) {
+            return append((StringBuffer) seq);
+        }
+        if (seq instanceof CharBuffer) {
+            return append((CharBuffer) seq);
+        }
+        return append(seq.toString());
+    }
+
+    /**
+     * Appends part of a CharSequence to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param seq  the CharSequence to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 3.0
+     */
+    @Override
+    public StrBuilder append(final CharSequence seq, final int startIndex, final int length) {
+        if (seq == null) {
+            return appendNull();
+        }
+        return append(seq.toString(), startIndex, length);
+    }
+
+    /**
+     * Appends a string to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final String str) {
+        if (str == null) {
+            return appendNull();
+        }
+        final int strLen = str.length();
+        if (strLen > 0) {
+            final int len = length();
+            ensureCapacity(len + strLen);
+            str.getChars(0, strLen, buffer, len);
+            size += strLen;
+        }
+        return this;
+    }
+
+
+    /**
+     * Appends part of a string to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final String str, final int startIndex, final int length) {
+        if (str == null) {
+            return appendNull();
+        }
+        if (startIndex < 0 || startIndex > str.length()) {
+            throw new StringIndexOutOfBoundsException("startIndex must be valid");
+        }
+        if (length < 0 || (startIndex + length) > str.length()) {
+            throw new StringIndexOutOfBoundsException("length must be valid");
+        }
+        if (length > 0) {
+            final int len = length();
+            ensureCapacity(len + length);
+            str.getChars(startIndex, startIndex + length, buffer, len);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Calls {@link String#format(String, Object...)} and appends the result.
+     *
+     * @param format the format string
+     * @param objs the objects to use in the format string
+     * @return {@code this} to enable chaining
+     * @see String#format(String, Object...)
+     * @since 3.2
+     */
+    public StrBuilder append(final String format, final Object... objs) {
+        return append(String.format(format, objs));
+    }
+
+    /**
+     * Appends the contents of a char buffer to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param buf  the char buffer to append
+     * @return this, to enable chaining
+     * @since 3.4
+     */
+    public StrBuilder append(final CharBuffer buf) {
+        if (buf == null) {
+            return appendNull();
+        }
+        if (buf.hasArray()) {
+            final int length = buf.remaining();
+            final int len = length();
+            ensureCapacity(len + length);
+            System.arraycopy(buf.array(), buf.arrayOffset() + buf.position(), buffer, len, length);
+            size += length;
+        } else {
+            append(buf.toString());
+        }
+        return this;
+    }
+
+    /**
+     * Appends the contents of a char buffer to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param buf  the char buffer to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 3.4
+     */
+    public StrBuilder append(final CharBuffer buf, final int startIndex, final int length) {
+        if (buf == null) {
+            return appendNull();
+        }
+        if (buf.hasArray()) {
+            final int totalLength = buf.remaining();
+            if (startIndex < 0 || startIndex > totalLength) {
+                throw new StringIndexOutOfBoundsException("startIndex must be valid");
+            }
+            if (length < 0 || (startIndex + length) > totalLength) {
+                throw new StringIndexOutOfBoundsException("length must be valid");
+            }
+            final int len = length();
+            ensureCapacity(len + length);
+            System.arraycopy(buf.array(), buf.arrayOffset() + buf.position() + startIndex, buffer, len, length);
+            size += length;
+        } else {
+            append(buf.toString(), startIndex, length);
+        }
+        return this;
+    }
+
+    /**
+     * Appends a string buffer to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string buffer to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final StringBuffer str) {
+        if (str == null) {
+            return appendNull();
+        }
+        final int strLen = str.length();
+        if (strLen > 0) {
+            final int len = length();
+            ensureCapacity(len + strLen);
+            str.getChars(0, strLen, buffer, len);
+            size += strLen;
+        }
+        return this;
+    }
+
+    /**
+     * Appends part of a string buffer to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final StringBuffer str, final int startIndex, final int length) {
+        if (str == null) {
+            return appendNull();
+        }
+        if (startIndex < 0 || startIndex > str.length()) {
+            throw new StringIndexOutOfBoundsException("startIndex must be valid");
+        }
+        if (length < 0 || (startIndex + length) > str.length()) {
+            throw new StringIndexOutOfBoundsException("length must be valid");
+        }
+        if (length > 0) {
+            final int len = length();
+            ensureCapacity(len + length);
+            str.getChars(startIndex, startIndex + length, buffer, len);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Appends a StringBuilder to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str the StringBuilder to append
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrBuilder append(final StringBuilder str) {
+        if (str == null) {
+            return appendNull();
+        }
+        final int strLen = str.length();
+        if (strLen > 0) {
+            final int len = length();
+            ensureCapacity(len + strLen);
+            str.getChars(0, strLen, buffer, len);
+            size += strLen;
+        }
+        return this;
+    }
+
+    /**
+     * Appends part of a StringBuilder to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str the StringBuilder to append
+     * @param startIndex the start index, inclusive, must be valid
+     * @param length the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrBuilder append(final StringBuilder str, final int startIndex, final int length) {
+        if (str == null) {
+            return appendNull();
+        }
+        if (startIndex < 0 || startIndex > str.length()) {
+            throw new StringIndexOutOfBoundsException("startIndex must be valid");
+        }
+        if (length < 0 || (startIndex + length) > str.length()) {
+            throw new StringIndexOutOfBoundsException("length must be valid");
+        }
+        if (length > 0) {
+            final int len = length();
+            ensureCapacity(len + length);
+            str.getChars(startIndex, startIndex + length, buffer, len);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Appends another string builder to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string builder to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final StrBuilder str) {
+        if (str == null) {
+            return appendNull();
+        }
+        final int strLen = str.length();
+        if (strLen > 0) {
+            final int len = length();
+            ensureCapacity(len + strLen);
+            System.arraycopy(str.buffer, 0, buffer, len, strLen);
+            size += strLen;
+        }
+        return this;
+    }
+
+    /**
+     * Appends part of a string builder to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final StrBuilder str, final int startIndex, final int length) {
+        if (str == null) {
+            return appendNull();
+        }
+        if (startIndex < 0 || startIndex > str.length()) {
+            throw new StringIndexOutOfBoundsException("startIndex must be valid");
+        }
+        if (length < 0 || (startIndex + length) > str.length()) {
+            throw new StringIndexOutOfBoundsException("length must be valid");
+        }
+        if (length > 0) {
+            final int len = length();
+            ensureCapacity(len + length);
+            str.getChars(startIndex, startIndex + length, buffer, len);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Appends a char array to the string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param chars  the char array to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final char[] chars) {
+        if (chars == null) {
+            return appendNull();
+        }
+        final int strLen = chars.length;
+        if (strLen > 0) {
+            final int len = length();
+            ensureCapacity(len + strLen);
+            System.arraycopy(chars, 0, buffer, len, strLen);
+            size += strLen;
+        }
+        return this;
+    }
+
+    /**
+     * Appends a char array to the string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param chars  the char array to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final char[] chars, final int startIndex, final int length) {
+        if (chars == null) {
+            return appendNull();
+        }
+        if (startIndex < 0 || startIndex > chars.length) {
+            throw new StringIndexOutOfBoundsException("Invalid startIndex: " + length);
+        }
+        if (length < 0 || (startIndex + length) > chars.length) {
+            throw new StringIndexOutOfBoundsException("Invalid length: " + length);
+        }
+        if (length > 0) {
+            final int len = length();
+            ensureCapacity(len + length);
+            System.arraycopy(chars, startIndex, buffer, len, length);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Appends a boolean value to the string builder.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final boolean value) {
+        if (value) {
+            ensureCapacity(size + 4);
+            buffer[size++] = 't';
+            buffer[size++] = 'r';
+            buffer[size++] = 'u';
+            buffer[size++] = 'e';
+        } else {
+            ensureCapacity(size + 5);
+            buffer[size++] = 'f';
+            buffer[size++] = 'a';
+            buffer[size++] = 'l';
+            buffer[size++] = 's';
+            buffer[size++] = 'e';
+        }
+        return this;
+    }
+
+    /**
+     * Appends a char value to the string builder.
+     *
+     * @param ch  the value to append
+     * @return this, to enable chaining
+     * @since 3.0
+     */
+    @Override
+    public StrBuilder append(final char ch) {
+        final int len = length();
+        ensureCapacity(len + 1);
+        buffer[size++] = ch;
+        return this;
+    }
+
+    /**
+     * Appends an int value to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final int value) {
+        return append(String.valueOf(value));
+    }
+
+    /**
+     * Appends a long value to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final long value) {
+        return append(String.valueOf(value));
+    }
+
+    /**
+     * Appends a float value to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final float value) {
+        return append(String.valueOf(value));
+    }
+
+    /**
+     * Appends a double value to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder append(final double value) {
+        return append(String.valueOf(value));
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends an object followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param obj  the object to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final Object obj) {
+        return append(obj).appendNewLine();
+    }
+
+    /**
+     * Appends a string followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final String str) {
+        return append(str).appendNewLine();
+    }
+
+    /**
+     * Appends part of a string followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final String str, final int startIndex, final int length) {
+        return append(str, startIndex, length).appendNewLine();
+    }
+
+    /**
+     * Calls {@link String#format(String, Object...)} and appends the result.
+     *
+     * @param format the format string
+     * @param objs the objects to use in the format string
+     * @return {@code this} to enable chaining
+     * @see String#format(String, Object...)
+     * @since 3.2
+     */
+    public StrBuilder appendln(final String format, final Object... objs) {
+        return append(format, objs).appendNewLine();
+    }
+
+    /**
+     * Appends a string buffer followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string buffer to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final StringBuffer str) {
+        return append(str).appendNewLine();
+    }
+
+    /**
+     * Appends a string builder followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string builder to append
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrBuilder appendln(final StringBuilder str) {
+        return append(str).appendNewLine();
+    }
+
+    /**
+     * Appends part of a string builder followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string builder to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrBuilder appendln(final StringBuilder str, final int startIndex, final int length) {
+        return append(str, startIndex, length).appendNewLine();
+    }
+
+    /**
+     * Appends part of a string buffer followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final StringBuffer str, final int startIndex, final int length) {
+        return append(str, startIndex, length).appendNewLine();
+    }
+
+    /**
+     * Appends another string builder followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string builder to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final StrBuilder str) {
+        return append(str).appendNewLine();
+    }
+
+    /**
+     * Appends part of a string builder followed by a new line to this string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param str  the string to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final StrBuilder str, final int startIndex, final int length) {
+        return append(str, startIndex, length).appendNewLine();
+    }
+
+    /**
+     * Appends a char array followed by a new line to the string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param chars  the char array to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final char[] chars) {
+        return append(chars).appendNewLine();
+    }
+
+    /**
+     * Appends a char array followed by a new line to the string builder.
+     * Appending null will call {@link #appendNull()}.
+     *
+     * @param chars  the char array to append
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param length  the length to append, must be valid
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final char[] chars, final int startIndex, final int length) {
+        return append(chars, startIndex, length).appendNewLine();
+    }
+
+    /**
+     * Appends a boolean value followed by a new line to the string builder.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final boolean value) {
+        return append(value).appendNewLine();
+    }
+
+    /**
+     * Appends a char value followed by a new line to the string builder.
+     *
+     * @param ch  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final char ch) {
+        return append(ch).appendNewLine();
+    }
+
+    /**
+     * Appends an int value followed by a new line to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final int value) {
+        return append(value).appendNewLine();
+    }
+
+    /**
+     * Appends a long value followed by a new line to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final long value) {
+        return append(value).appendNewLine();
+    }
+
+    /**
+     * Appends a float value followed by a new line to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final float value) {
+        return append(value).appendNewLine();
+    }
+
+    /**
+     * Appends a double value followed by a new line to the string builder using <code>String.valueOf</code>.
+     *
+     * @param value  the value to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendln(final double value) {
+        return append(value).appendNewLine();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends each item in an array to the builder without any separators.
+     * Appending a null array will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param <T>  the element type
+     * @param array  the array to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public <T> StrBuilder appendAll(final T... array) {
+        if (array != null && array.length > 0) {
+            for (final Object element : array) {
+                append(element);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Appends each item in an iterable to the builder without any separators.
+     * Appending a null iterable will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param iterable  the iterable to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendAll(final Iterable<?> iterable) {
+        if (iterable != null) {
+            for (final Object o : iterable) {
+                append(o);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Appends each item in an iterator to the builder without any separators.
+     * Appending a null iterator will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param it  the iterator to append
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendAll(final Iterator<?> it) {
+        if (it != null) {
+            while (it.hasNext()) {
+                append(it.next());
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends an array placing separators between each value, but
+     * not before the first or after the last.
+     * Appending a null array will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param array  the array to append
+     * @param separator  the separator to use, null means no separator
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendWithSeparators(final Object[] array, final String separator) {
+        if (array != null && array.length > 0) {
+            final String sep = Objects.toString(separator, "");
+            append(array[0]);
+            for (int i = 1; i < array.length; i++) {
+                append(sep);
+                append(array[i]);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Appends an iterable placing separators between each value, but
+     * not before the first or after the last.
+     * Appending a null iterable will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param iterable  the iterable to append
+     * @param separator  the separator to use, null means no separator
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendWithSeparators(final Iterable<?> iterable, final String separator) {
+        if (iterable != null) {
+            final String sep = Objects.toString(separator, "");
+            final Iterator<?> it = iterable.iterator();
+            while (it.hasNext()) {
+                append(it.next());
+                if (it.hasNext()) {
+                    append(sep);
+                }
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Appends an iterator placing separators between each value, but
+     * not before the first or after the last.
+     * Appending a null iterator will have no effect.
+     * Each object is appended using {@link #append(Object)}.
+     *
+     * @param it  the iterator to append
+     * @param separator  the separator to use, null means no separator
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendWithSeparators(final Iterator<?> it, final String separator) {
+        if (it != null) {
+            final String sep = Objects.toString(separator, "");
+            while (it.hasNext()) {
+                append(it.next());
+                if (it.hasNext()) {
+                    append(sep);
+                }
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends a separator if the builder is currently non-empty.
+     * Appending a null separator will have no effect.
+     * The separator is appended using {@link #append(String)}.
+     * <p>
+     * This method is useful for adding a separator each time around the
+     * loop except the first.
+     * <pre>
+     * for (Iterator it = list.iterator(); it.hasNext(); ) {
+     *   appendSeparator(",");
+     *   append(it.next());
+     * }
+     * </pre>
+     * Note that for this simple example, you should use
+     * {@link #appendWithSeparators(Iterable, String)}.
+     *
+     * @param separator  the separator to use, null means no separator
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendSeparator(final String separator) {
+        return appendSeparator(separator, null);
+    }
+
+    /**
+     * Appends one of both separators to the StrBuilder.
+     * If the builder is currently empty it will append the defaultIfEmpty-separator
+     * Otherwise it will append the standard-separator
+     *
+     * Appending a null separator will have no effect.
+     * The separator is appended using {@link #append(String)}.
+     * <p>
+     * This method is for example useful for constructing queries
+     * <pre>
+     * StrBuilder whereClause = new StrBuilder();
+     * if(searchCommand.getPriority() != null) {
+     *  whereClause.appendSeparator(" and", " where");
+     *  whereClause.append(" priority = ?")
+     * }
+     * if(searchCommand.getComponent() != null) {
+     *  whereClause.appendSeparator(" and", " where");
+     *  whereClause.append(" component = ?")
+     * }
+     * selectClause.append(whereClause)
+     * </pre>
+     *
+     * @param standard the separator if builder is not empty, null means no separator
+     * @param defaultIfEmpty the separator if builder is empty, null means no separator
+     * @return this, to enable chaining
+     * @since 2.5
+     */
+    public StrBuilder appendSeparator(final String standard, final String defaultIfEmpty) {
+        final String str = isEmpty() ? defaultIfEmpty : standard;
+        if (str != null) {
+            append(str);
+        }
+        return this;
+    }
+
+    /**
+     * Appends a separator if the builder is currently non-empty.
+     * The separator is appended using {@link #append(char)}.
+     * <p>
+     * This method is useful for adding a separator each time around the
+     * loop except the first.
+     * <pre>
+     * for (Iterator it = list.iterator(); it.hasNext(); ) {
+     *   appendSeparator(',');
+     *   append(it.next());
+     * }
+     * </pre>
+     * Note that for this simple example, you should use
+     * {@link #appendWithSeparators(Iterable, String)}.
+     *
+     * @param separator  the separator to use
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendSeparator(final char separator) {
+        if (size() > 0) {
+            append(separator);
+        }
+        return this;
+    }
+
+    /**
+     * Append one of both separators to the builder
+     * If the builder is currently empty it will append the defaultIfEmpty-separator
+     * Otherwise it will append the standard-separator
+     *
+     * The separator is appended using {@link #append(char)}.
+     * @param standard the separator if builder is not empty
+     * @param defaultIfEmpty the separator if builder is empty
+     * @return this, to enable chaining
+     * @since 2.5
+     */
+    public StrBuilder appendSeparator(final char standard, final char defaultIfEmpty) {
+        if (size() > 0) {
+            append(standard);
+        } else {
+            append(defaultIfEmpty);
+        }
+        return this;
+    }
+    /**
+     * Appends a separator to the builder if the loop index is greater than zero.
+     * Appending a null separator will have no effect.
+     * The separator is appended using {@link #append(String)}.
+     * <p>
+     * This method is useful for adding a separator each time around the
+     * loop except the first.
+     * </p>
+     * <pre>
+     * for (int i = 0; i &lt; list.size(); i++) {
+     *   appendSeparator(",", i);
+     *   append(list.get(i));
+     * }
+     * </pre>
+     * Note that for this simple example, you should use
+     * {@link #appendWithSeparators(Iterable, String)}.
+     *
+     * @param separator  the separator to use, null means no separator
+     * @param loopIndex  the loop index
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendSeparator(final String separator, final int loopIndex) {
+        if (separator != null && loopIndex > 0) {
+            append(separator);
+        }
+        return this;
+    }
+
+    /**
+     * Appends a separator to the builder if the loop index is greater than zero.
+     * The separator is appended using {@link #append(char)}.
+     * <p>
+     * This method is useful for adding a separator each time around the
+     * loop except the first.
+     * </p>
+     * <pre>
+     * for (int i = 0; i &lt; list.size(); i++) {
+     *   appendSeparator(",", i);
+     *   append(list.get(i));
+     * }
+     * </pre>
+     * Note that for this simple example, you should use
+     * {@link #appendWithSeparators(Iterable, String)}.
+     *
+     * @param separator  the separator to use
+     * @param loopIndex  the loop index
+     * @return this, to enable chaining
+     * @since 2.3
+     */
+    public StrBuilder appendSeparator(final char separator, final int loopIndex) {
+        if (loopIndex > 0) {
+            append(separator);
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends the pad character to the builder the specified number of times.
+     *
+     * @param length  the length to append, negative means no append
+     * @param padChar  the character to append
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendPadding(final int length, final char padChar) {
+        if (length >= 0) {
+            ensureCapacity(size + length);
+            for (int i = 0; i < length; i++) {
+                buffer[size++] = padChar;
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Appends an object to the builder padding on the left to a fixed width.
+     * The <code>toString</code> of the object is used.
+     * If the object is larger than the length, the left hand side is lost.
+     * If the object is null, the null text value is used.
+     *
+     * @param obj  the object to append, null uses null text
+     * @param width  the fixed field width, zero or negative has no effect
+     * @param padChar  the pad character to use
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendFixedWidthPadLeft(final Object obj, final int width, final char padChar) {
+        if (width > 0) {
+            ensureCapacity(size + width);
+            String str = (obj == null ? getNullText() : obj.toString());
+            if (str == null) {
+                str = "";
+            }
+            final int strLen = str.length();
+            if (strLen >= width) {
+                str.getChars(strLen - width, strLen, buffer, size);
+            } else {
+                final int padLen = width - strLen;
+                for (int i = 0; i < padLen; i++) {
+                    buffer[size + i] = padChar;
+                }
+                str.getChars(0, strLen, buffer, size + padLen);
+            }
+            size += width;
+        }
+        return this;
+    }
+
+    /**
+     * Appends an object to the builder padding on the left to a fixed width.
+     * The <code>String.valueOf</code> of the <code>int</code> value is used.
+     * If the formatted value is larger than the length, the left hand side is lost.
+     *
+     * @param value  the value to append
+     * @param width  the fixed field width, zero or negative has no effect
+     * @param padChar  the pad character to use
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendFixedWidthPadLeft(final int value, final int width, final char padChar) {
+        return appendFixedWidthPadLeft(String.valueOf(value), width, padChar);
+    }
+
+    /**
+     * Appends an object to the builder padding on the right to a fixed length.
+     * The <code>toString</code> of the object is used.
+     * If the object is larger than the length, the right hand side is lost.
+     * If the object is null, null text value is used.
+     *
+     * @param obj  the object to append, null uses null text
+     * @param width  the fixed field width, zero or negative has no effect
+     * @param padChar  the pad character to use
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendFixedWidthPadRight(final Object obj, final int width, final char padChar) {
+        if (width > 0) {
+            ensureCapacity(size + width);
+            String str = (obj == null ? getNullText() : obj.toString());
+            if (str == null) {
+                str = "";
+            }
+            final int strLen = str.length();
+            if (strLen >= width) {
+                str.getChars(0, width, buffer, size);
+            } else {
+                final int padLen = width - strLen;
+                str.getChars(0, strLen, buffer, size);
+                for (int i = 0; i < padLen; i++) {
+                    buffer[size + strLen + i] = padChar;
+                }
+            }
+            size += width;
+        }
+        return this;
+    }
+
+    /**
+     * Appends an object to the builder padding on the right to a fixed length.
+     * The <code>String.valueOf</code> of the <code>int</code> value is used.
+     * If the object is larger than the length, the right hand side is lost.
+     *
+     * @param value  the value to append
+     * @param width  the fixed field width, zero or negative has no effect
+     * @param padChar  the pad character to use
+     * @return this, to enable chaining
+     */
+    public StrBuilder appendFixedWidthPadRight(final int value, final int width, final char padChar) {
+        return appendFixedWidthPadRight(String.valueOf(value), width, padChar);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Inserts the string representation of an object into this builder.
+     * Inserting null will use the stored null text value.
+     *
+     * @param index  the index to add at, must be valid
+     * @param obj  the object to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final Object obj) {
+        if (obj == null) {
+            return insert(index, nullText);
+        }
+        return insert(index, obj.toString());
+    }
+
+    /**
+     * Inserts the string into this builder.
+     * Inserting null will use the stored null text value.
+     *
+     * @param index  the index to add at, must be valid
+     * @param str  the string to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, String str) {
+        validateIndex(index);
+        if (str == null) {
+            str = nullText;
+        }
+        if (str != null) {
+            final int strLen = str.length();
+            if (strLen > 0) {
+                final int newSize = size + strLen;
+                ensureCapacity(newSize);
+                System.arraycopy(buffer, index, buffer, index + strLen, size - index);
+                size = newSize;
+                str.getChars(0, strLen, buffer, index);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Inserts the character array into this builder.
+     * Inserting null will use the stored null text value.
+     *
+     * @param index  the index to add at, must be valid
+     * @param chars  the char array to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final char chars[]) {
+        validateIndex(index);
+        if (chars == null) {
+            return insert(index, nullText);
+        }
+        final int len = chars.length;
+        if (len > 0) {
+            ensureCapacity(size + len);
+            System.arraycopy(buffer, index, buffer, index + len, size - index);
+            System.arraycopy(chars, 0, buffer, index, len);
+            size += len;
+        }
+        return this;
+    }
+
+    /**
+     * Inserts part of the character array into this builder.
+     * Inserting null will use the stored null text value.
+     *
+     * @param index  the index to add at, must be valid
+     * @param chars  the char array to insert
+     * @param offset  the offset into the character array to start at, must be valid
+     * @param length  the length of the character array part to copy, must be positive
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if any index is invalid
+     */
+    public StrBuilder insert(final int index, final char chars[], final int offset, final int length) {
+        validateIndex(index);
+        if (chars == null) {
+            return insert(index, nullText);
+        }
+        if (offset < 0 || offset > chars.length) {
+            throw new StringIndexOutOfBoundsException("Invalid offset: " + offset);
+        }
+        if (length < 0 || offset + length > chars.length) {
+            throw new StringIndexOutOfBoundsException("Invalid length: " + length);
+        }
+        if (length > 0) {
+            ensureCapacity(size + length);
+            System.arraycopy(buffer, index, buffer, index + length, size - index);
+            System.arraycopy(chars, offset, buffer, index, length);
+            size += length;
+        }
+        return this;
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(int index, final boolean value) {
+        validateIndex(index);
+        if (value) {
+            ensureCapacity(size + 4);
+            System.arraycopy(buffer, index, buffer, index + 4, size - index);
+            buffer[index++] = 't';
+            buffer[index++] = 'r';
+            buffer[index++] = 'u';
+            buffer[index] = 'e';
+            size += 4;
+        } else {
+            ensureCapacity(size + 5);
+            System.arraycopy(buffer, index, buffer, index + 5, size - index);
+            buffer[index++] = 'f';
+            buffer[index++] = 'a';
+            buffer[index++] = 'l';
+            buffer[index++] = 's';
+            buffer[index] = 'e';
+            size += 5;
+        }
+        return this;
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final char value) {
+        validateIndex(index);
+        ensureCapacity(size + 1);
+        System.arraycopy(buffer, index, buffer, index + 1, size - index);
+        buffer[index] = value;
+        size++;
+        return this;
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final int value) {
+        return insert(index, String.valueOf(value));
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final long value) {
+        return insert(index, String.valueOf(value));
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final float value) {
+        return insert(index, String.valueOf(value));
+    }
+
+    /**
+     * Inserts the value into this builder.
+     *
+     * @param index  the index to add at, must be valid
+     * @param value  the value to insert
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder insert(final int index, final double value) {
+        return insert(index, String.valueOf(value));
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Internal method to delete a range without validation.
+     *
+     * @param startIndex  the start index, must be valid
+     * @param endIndex  the end index (exclusive), must be valid
+     * @param len  the length, must be valid
+     * @throws IndexOutOfBoundsException if any index is invalid
+     */
+    private void deleteImpl(final int startIndex, final int endIndex, final int len) {
+        System.arraycopy(buffer, endIndex, buffer, startIndex, size - endIndex);
+        size -= len;
+    }
+
+    /**
+     * Deletes the characters between the two specified indices.
+     *
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param endIndex  the end index, exclusive, must be valid except
+     *  that if too large it is treated as end of string
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder delete(final int startIndex, int endIndex) {
+        endIndex = validateRange(startIndex, endIndex);
+        final int len = endIndex - startIndex;
+        if (len > 0) {
+            deleteImpl(startIndex, endIndex, len);
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Deletes the character wherever it occurs in the builder.
+     *
+     * @param ch  the character to delete
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteAll(final char ch) {
+        for (int i = 0; i < size; i++) {
+            if (buffer[i] == ch) {
+                final int start = i;
+                while (++i < size) {
+                    if (buffer[i] != ch) {
+                        break;
+                    }
+                }
+                final int len = i - start;
+                deleteImpl(start, i, len);
+                i -= len;
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Deletes the character wherever it occurs in the builder.
+     *
+     * @param ch  the character to delete
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteFirst(final char ch) {
+        for (int i = 0; i < size; i++) {
+            if (buffer[i] == ch) {
+                deleteImpl(i, i + 1, 1);
+                break;
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Deletes the string wherever it occurs in the builder.
+     *
+     * @param str  the string to delete, null causes no action
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteAll(final String str) {
+        final int len = (str == null ? 0 : str.length());
+        if (len > 0) {
+            int index = indexOf(str, 0);
+            while (index >= 0) {
+                deleteImpl(index, index + len, len);
+                index = indexOf(str, index);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Deletes the string wherever it occurs in the builder.
+     *
+     * @param str  the string to delete, null causes no action
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteFirst(final String str) {
+        final int len = (str == null ? 0 : str.length());
+        if (len > 0) {
+            final int index = indexOf(str, 0);
+            if (index >= 0) {
+                deleteImpl(index, index + len, len);
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Deletes all parts of the builder that the matcher matches.
+     * <p>
+     * Matchers can be used to perform advanced deletion behaviour.
+     * For example you could write a matcher to delete all occurrences
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteAll(final StrMatcher matcher) {
+        return replace(matcher, null, 0, size, -1);
+    }
+
+    /**
+     * Deletes the first match within the builder using the specified matcher.
+     * <p>
+     * Matchers can be used to perform advanced deletion behaviour.
+     * For example you could write a matcher to delete
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @return this, to enable chaining
+     */
+    public StrBuilder deleteFirst(final StrMatcher matcher) {
+        return replace(matcher, null, 0, size, 1);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Internal method to delete a range without validation.
+     *
+     * @param startIndex  the start index, must be valid
+     * @param endIndex  the end index (exclusive), must be valid
+     * @param removeLen  the length to remove (endIndex - startIndex), must be valid
+     * @param insertStr  the string to replace with, null means delete range
+     * @param insertLen  the length of the insert string, must be valid
+     * @throws IndexOutOfBoundsException if any index is invalid
+     */
+    private void replaceImpl(final int startIndex, final int endIndex, final int removeLen, final String insertStr, final int insertLen) {
+        final int newSize = size - removeLen + insertLen;
+        if (insertLen != removeLen) {
+            ensureCapacity(newSize);
+            System.arraycopy(buffer, endIndex, buffer, startIndex + insertLen, size - endIndex);
+            size = newSize;
+        }
+        if (insertLen > 0) {
+            insertStr.getChars(0, insertLen, buffer, startIndex);
+        }
+    }
+
+    /**
+     * Replaces a portion of the string builder with another string.
+     * The length of the inserted string does not have to match the removed length.
+     *
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param endIndex  the end index, exclusive, must be valid except
+     *  that if too large it is treated as end of string
+     * @param replaceStr  the string to replace with, null means delete range
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public StrBuilder replace(final int startIndex, int endIndex, final String replaceStr) {
+        endIndex = validateRange(startIndex, endIndex);
+        final int insertLen = (replaceStr == null ? 0 : replaceStr.length());
+        replaceImpl(startIndex, endIndex, endIndex - startIndex, replaceStr, insertLen);
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces the search character with the replace character
+     * throughout the builder.
+     *
+     * @param search  the search character
+     * @param replace  the replace character
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceAll(final char search, final char replace) {
+        if (search != replace) {
+            for (int i = 0; i < size; i++) {
+                if (buffer[i] == search) {
+                    buffer[i] = replace;
+                }
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Replaces the first instance of the search character with the
+     * replace character in the builder.
+     *
+     * @param search  the search character
+     * @param replace  the replace character
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceFirst(final char search, final char replace) {
+        if (search != replace) {
+            for (int i = 0; i < size; i++) {
+                if (buffer[i] == search) {
+                    buffer[i] = replace;
+                    break;
+                }
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces the search string with the replace string throughout the builder.
+     *
+     * @param searchStr  the search string, null causes no action to occur
+     * @param replaceStr  the replace string, null is equivalent to an empty string
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceAll(final String searchStr, final String replaceStr) {
+        final int searchLen = (searchStr == null ? 0 : searchStr.length());
+        if (searchLen > 0) {
+            final int replaceLen = (replaceStr == null ? 0 : replaceStr.length());
+            int index = indexOf(searchStr, 0);
+            while (index >= 0) {
+                replaceImpl(index, index + searchLen, searchLen, replaceStr, replaceLen);
+                index = indexOf(searchStr, index + replaceLen);
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Replaces the first instance of the search string with the replace string.
+     *
+     * @param searchStr  the search string, null causes no action to occur
+     * @param replaceStr  the replace string, null is equivalent to an empty string
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceFirst(final String searchStr, final String replaceStr) {
+        final int searchLen = (searchStr == null ? 0 : searchStr.length());
+        if (searchLen > 0) {
+            final int index = indexOf(searchStr, 0);
+            if (index >= 0) {
+                final int replaceLen = (replaceStr == null ? 0 : replaceStr.length());
+                replaceImpl(index, index + searchLen, searchLen, replaceStr, replaceLen);
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all matches within the builder with the replace string.
+     * <p>
+     * Matchers can be used to perform advanced replace behaviour.
+     * For example you could write a matcher to replace all occurrences
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @param replaceStr  the replace string, null is equivalent to an empty string
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceAll(final StrMatcher matcher, final String replaceStr) {
+        return replace(matcher, replaceStr, 0, size, -1);
+    }
+
+    /**
+     * Replaces the first match within the builder with the replace string.
+     * <p>
+     * Matchers can be used to perform advanced replace behaviour.
+     * For example you could write a matcher to replace
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @param replaceStr  the replace string, null is equivalent to an empty string
+     * @return this, to enable chaining
+     */
+    public StrBuilder replaceFirst(final StrMatcher matcher, final String replaceStr) {
+        return replace(matcher, replaceStr, 0, size, 1);
+    }
+
+    // -----------------------------------------------------------------------
+    /**
+     * Advanced search and replaces within the builder using a matcher.
+     * <p>
+     * Matchers can be used to perform advanced behaviour.
+     * For example you could write a matcher to delete all occurrences
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @param replaceStr  the string to replace the match with, null is a delete
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param endIndex  the end index, exclusive, must be valid except
+     *  that if too large it is treated as end of string
+     * @param replaceCount  the number of times to replace, -1 for replace all
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if start index is invalid
+     */
+    public StrBuilder replace(
+            final StrMatcher matcher, final String replaceStr,
+            final int startIndex, int endIndex, final int replaceCount) {
+        endIndex = validateRange(startIndex, endIndex);
+        return replaceImpl(matcher, replaceStr, startIndex, endIndex, replaceCount);
+    }
+
+    /**
+     * Replaces within the builder using a matcher.
+     * <p>
+     * Matchers can be used to perform advanced behaviour.
+     * For example you could write a matcher to delete all occurrences
+     * where the character 'a' is followed by a number.
+     *
+     * @param matcher  the matcher to use to find the deletion, null causes no action
+     * @param replaceStr  the string to replace the match with, null is a delete
+     * @param from  the start index, must be valid
+     * @param to  the end index (exclusive), must be valid
+     * @param replaceCount  the number of times to replace, -1 for replace all
+     * @return this, to enable chaining
+     * @throws IndexOutOfBoundsException if any index is invalid
+     */
+    private StrBuilder replaceImpl(
+            final StrMatcher matcher, final String replaceStr,
+            final int from, int to, int replaceCount) {
+        if (matcher == null || size == 0) {
+            return this;
+        }
+        final int replaceLen = (replaceStr == null ? 0 : replaceStr.length());
+        for (int i = from; i < to && replaceCount != 0; i++) {
+            final char[] buf = buffer;
+            final int removeLen = matcher.isMatch(buf, i, from, to);
+            if (removeLen > 0) {
+                replaceImpl(i, i + removeLen, removeLen, replaceStr, replaceLen);
+                to = to - removeLen + replaceLen;
+                i = i + replaceLen - 1;
+                if (replaceCount > 0) {
+                    replaceCount--;
+                }
+            }
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Reverses the string builder placing each character in the opposite index.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder reverse() {
+        if (size == 0) {
+            return this;
+        }
+
+        final int half = size / 2;
+        final char[] buf = buffer;
+        for (int leftIdx = 0, rightIdx = size - 1; leftIdx < half; leftIdx++,rightIdx--) {
+            final char swap = buf[leftIdx];
+            buf[leftIdx] = buf[rightIdx];
+            buf[rightIdx] = swap;
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Trims the builder by removing characters less than or equal to a space
+     * from the beginning and end.
+     *
+     * @return this, to enable chaining
+     */
+    public StrBuilder trim() {
+        if (size == 0) {
+            return this;
+        }
+        int len = size;
+        final char[] buf = buffer;
+        int pos = 0;
+        while (pos < len && buf[pos] <= ' ') {
+            pos++;
+        }
+        while (pos < len && buf[len - 1] <= ' ') {
+            len--;
+        }
+        if (len < size) {
+            delete(len, size);
+        }
+        if (pos > 0) {
+            delete(0, pos);
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Checks whether this builder starts with the specified string.
+     * <p>
+     * Note that this method handles null input quietly, unlike String.
+     *
+     * @param str  the string to search for, null returns false
+     * @return true if the builder starts with the string
+     */
+    public boolean startsWith(final String str) {
+        if (str == null) {
+            return false;
+        }
+        final int len = str.length();
+        if (len == 0) {
+            return true;
+        }
+        if (len > size) {
+            return false;
+        }
+        for (int i = 0; i < len; i++) {
+            if (buffer[i] != str.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Checks whether this builder ends with the specified string.
+     * <p>
+     * Note that this method handles null input quietly, unlike String.
+     *
+     * @param str  the string to search for, null returns false
+     * @return true if the builder ends with the string
+     */
+    public boolean endsWith(final String str) {
+        if (str == null) {
+            return false;
+        }
+        final int len = str.length();
+        if (len == 0) {
+            return true;
+        }
+        if (len > size) {
+            return false;
+        }
+        int pos = size - len;
+        for (int i = 0; i < len; i++,pos++) {
+            if (buffer[pos] != str.charAt(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * {@inheritDoc}
+     * @since 3.0
+     */
+    @Override
+    public CharSequence subSequence(final int startIndex, final int endIndex) {
+      if (startIndex < 0) {
+          throw new StringIndexOutOfBoundsException(startIndex);
+      }
+      if (endIndex > size) {
+          throw new StringIndexOutOfBoundsException(endIndex);
+      }
+      if (startIndex > endIndex) {
+          throw new StringIndexOutOfBoundsException(endIndex - startIndex);
+      }
+      return substring(startIndex, endIndex);
+    }
+
+    /**
+     * Extracts a portion of this string builder as a string.
+     *
+     * @param start  the start index, inclusive, must be valid
+     * @return the new string
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public String substring(final int start) {
+        return substring(start, size);
+    }
+
+    /**
+     * Extracts a portion of this string builder as a string.
+     * <p>
+     * Note: This method treats an endIndex greater than the length of the
+     * builder as equal to the length of the builder, and continues
+     * without error, unlike StringBuffer or String.
+     *
+     * @param startIndex  the start index, inclusive, must be valid
+     * @param endIndex  the end index, exclusive, must be valid except
+     *  that if too large it is treated as end of string
+     * @return the new string
+     * @throws IndexOutOfBoundsException if the index is invalid
+     */
+    public String substring(final int startIndex, int endIndex) {
+        endIndex = validateRange(startIndex, endIndex);
+        return new String(buffer, startIndex, endIndex - startIndex);
+    }
+
+    /**
+     * Extracts the leftmost characters from the string builder without
+     * throwing an exception.
+     * <p>
+     * This method extracts the left <code>length</code> characters from
+     * the builder. If this many characters are not available, the whole
+     * builder is returned. Thus the returned string may be shorter than the
+     * length requested.
+     *
+     * @param length  the number of characters to extract, negative returns empty string
+     * @return the new string
+     */
+    public String leftString(final int length) {
+        if (length <= 0) {
+            return "";
+        } else if (length >= size) {
+            return new String(buffer, 0, size);
+        } else {
+            return new String(buffer, 0, length);
+        }
+    }
+
+    /**
+     * Extracts the rightmost characters from the string builder without
+     * throwing an exception.
+     * <p>
+     * This method extracts the right <code>length</code> characters from
+     * the builder. If this many characters are not available, the whole
+     * builder is returned. Thus the returned string may be shorter than the
+     * length requested.
+     *
+     * @param length  the number of characters to extract, negative returns empty string
+     * @return the new string
+     */
+    public String rightString(final int length) {
+        if (length <= 0) {
+            return "";
+        } else if (length >= size) {
+            return new String(buffer, 0, size);
+        } else {
+            return new String(buffer, size - length, length);
+        }
+    }
+
+    /**
+     * Extracts some characters from the middle of the string builder without
+     * throwing an exception.
+     * <p>
+     * This method extracts <code>length</code> characters from the builder
+     * at the specified index.
+     * If the index is negative it is treated as zero.
+     * If the index is greater than the builder size, it is treated as the builder size.
+     * If the length is negative, the empty string is returned.
+     * If insufficient characters are available in the builder, as much as possible is returned.
+     * Thus the returned string may be shorter than the length requested.
+     *
+     * @param index  the index to start at, negative means zero
+     * @param length  the number of characters to extract, negative returns empty string
+     * @return the new string
+     */
+    public String midString(int index, final int length) {
+        if (index < 0) {
+            index = 0;
+        }
+        if (length <= 0 || index >= size) {
+            return "";
+        }
+        if (size <= index + length) {
+            return new String(buffer, index, size - index);
+        }
+        return new String(buffer, index, length);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Checks if the string builder contains the specified char.
+     *
+     * @param ch  the character to find
+     * @return true if the builder contains the character
+     */
+    public boolean contains(final char ch) {
+        final char[] thisBuf = buffer;
+        for (int i = 0; i < this.size; i++) {
+            if (thisBuf[i] == ch) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks if the string builder contains the specified string.
+     *
+     * @param str  the string to find
+     * @return true if the builder contains the string
+     */
+    public boolean contains(final String str) {
+        return indexOf(str, 0) >= 0;
+    }
+
+    /**
+     * Checks if the string builder contains a string matched using the
+     * specified matcher.
+     * <p>
+     * Matchers can be used to perform advanced searching behaviour.
+     * For example you could write a matcher to search for the character
+     * 'a' followed by a number.
+     *
+     * @param matcher  the matcher to use, null returns -1
+     * @return true if the matcher finds a match in the builder
+     */
+    public boolean contains(final StrMatcher matcher) {
+        return indexOf(matcher, 0) >= 0;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Searches the string builder to find the first reference to the specified char.
+     *
+     * @param ch  the character to find
+     * @return the first index of the character, or -1 if not found
+     */
+    public int indexOf(final char ch) {
+        return indexOf(ch, 0);
+    }
+
+    /**
+     * Searches the string builder to find the first reference to the specified char.
+     *
+     * @param ch  the character to find
+     * @param startIndex  the index to start at, invalid index rounded to edge
+     * @return the first index of the character, or -1 if not found
+     */
+    public int indexOf(final char ch, int startIndex) {
+        startIndex = (startIndex < 0 ? 0 : startIndex);
+        if (startIndex >= size) {
+            return -1;
+        }
+        final char[] thisBuf = buffer;
+        for (int i = startIndex; i < size; i++) {
+            if (thisBuf[i] == ch) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Searches the string builder to find the first reference to the specified string.
+     * <p>
+     * Note that a null input string will return -1, whereas the JDK throws an exception.
+     *
+     * @param str  the string to find, null returns -1
+     * @return the first index of the string, or -1 if not found
+     */
+    public int indexOf(final String str) {
+        return indexOf(str, 0);
+    }
+
+    /**
+     * Searches the string builder to find the first reference to the specified
+     * string starting searching from the given index.
+     * <p>
+     * Note that a null input string will return -1, whereas the JDK throws an exception.
+     *
+     * @param str  the string to find, null returns -1
+     * @param startIndex  the index to start at, invalid index rounded to edge
+     * @return the first index of the string, or -1 if not found
+     */
+    public int indexOf(final String str, int startIndex) {
+        startIndex = (startIndex < 0 ? 0 : startIndex);
+        if (str == null || startIndex >= size) {
+            return -1;
+        }
+        final int strLen = str.length();
+        if (strLen == 1) {
+            return indexOf(str.charAt(0), startIndex);
+        }
+        if (strLen == 0) {
+            return startIndex;
+        }
+        if (strLen > size) {
+            return -1;
+        }
+        final char[] thisBuf = buffer;
+        final int len = size - strLen + 1;
+        outer:
+        for (int i = startIndex; i < len; i++) {
+            for (int j = 0; j < strLen; j++) {
+                if (str.charAt(j) != thisBuf[i + j]) {
+                    continue outer;
+                }
+            }
+            return i;
+        }
+        return -1;
+    }
+
+    /**
+     * Searches the string builder using the matcher to find the first match.
+     * <p>
+     * Matchers can be used to perform advanced searching behaviour.
+     * For example you could write a matcher to find the character 'a'
+     * followed by a number.
+     *
+     * @param matcher  the matcher to use, null returns -1
+     * @return the first index matched, or -1 if not found
+     */
+    public int indexOf(final StrMatcher matcher) {
+        return indexOf(matcher, 0);
+    }
+
+    /**
+     * Searches the string builder using the matcher to find the first
+     * match searching from the given index.
+     * <p>
+     * Matchers can be used to perform advanced searching behaviour.
+     * For example you could write a matcher to find the character 'a'
+     * followed by a number.
+     *
+     * @param matcher  the matcher to use, null returns -1
+     * @param startIndex  the index to start at, invalid index rounded to edge
+     * @return the first index matched, or -1 if not found
+     */
+    public int indexOf(final StrMatcher matcher, int startIndex) {
+        startIndex = (startIndex < 0 ? 0 : startIndex);
+        if (matcher == null || startIndex >= size) {
+            return -1;
+        }
+        final int len = size;
+        final c

<TRUNCATED>

[11/14] [text] TEXT-23: fix assertEquals, SrtBuilderTest diff objs

Posted by ch...@apache.org.
TEXT-23: fix assertEquals, SrtBuilderTest diff objs


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/7adf1d5e
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/7adf1d5e
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/7adf1d5e

Branch: refs/heads/master
Commit: 7adf1d5ea33fc1a67196f96ce71c17ebdb28efed
Parents: 6f24aa4
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 16:00:03 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 16:13:57 2016 -0500

----------------------------------------------------------------------
 src/test/java/org/apache/commons/text/StrBuilderTest.java | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/7adf1d5e/src/test/java/org/apache/commons/text/StrBuilderTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrBuilderTest.java b/src/test/java/org/apache/commons/text/StrBuilderTest.java
index 1920984..e44c767 100644
--- a/src/test/java/org/apache/commons/text/StrBuilderTest.java
+++ b/src/test/java/org/apache/commons/text/StrBuilderTest.java
@@ -467,7 +467,7 @@ public class StrBuilderTest {
     @Test
     public void testToCharArray() {
         final StrBuilder sb = new StrBuilder();
-        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY, sb.toCharArray());
+        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY.length, sb.toCharArray().length);
 
         char[] a = sb.toCharArray();
         assertNotNull("toCharArray() result is null", a);
@@ -482,7 +482,7 @@ public class StrBuilderTest {
     @Test
     public void testToCharArrayIntInt() {
         final StrBuilder sb = new StrBuilder();
-        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY, sb.toCharArray(0, 0));
+        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY.length, sb.toCharArray(0, 0).length);
 
         sb.append("junit");
         char[] a = sb.toCharArray(0, 20); // too large test


[14/14] [text] Merge branch 'TEXT-23' of github.com:chtompki/commons-text into TEXT-23

Posted by ch...@apache.org.
Merge branch 'TEXT-23' of github.com:chtompki/commons-text into TEXT-23


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/a408a7cf
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/a408a7cf
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/a408a7cf

Branch: refs/heads/master
Commit: a408a7cfa9a436d84954003e6977abea1bf0a3cd
Parents: ad7ee39 56c0a07
Author: Rob Tompkins <ch...@gmail.com>
Authored: Sun Nov 13 15:32:01 2016 -0500
Committer: Rob Tompkins <ch...@gmail.com>
Committed: Sun Nov 13 15:32:01 2016 -0500

----------------------------------------------------------------------
 src/changes/changes.xml                         |    1 +
 .../java/org/apache/commons/text/Builder.java   |   87 +
 .../apache/commons/text/CompositeFormat.java    |  114 +
 .../commons/text/ExtendedMessageFormat.java     |  529 +++
 .../org/apache/commons/text/FormatFactory.java  |   39 +
 .../apache/commons/text/FormattableUtils.java   |  151 +
 .../org/apache/commons/text/StrBuilder.java     | 3117 ++++++++++++++++++
 .../java/org/apache/commons/text/StrLookup.java |  178 +
 .../org/apache/commons/text/StrMatcher.java     |  437 +++
 .../org/apache/commons/text/StrSubstitutor.java | 1227 +++++++
 .../org/apache/commons/text/StrTokenizer.java   | 1116 +++++++
 .../java/org/apache/commons/text/WordUtils.java |  733 ++++
 .../text/translate/AggregateTranslator.java     |    2 -
 .../text/translate/CharSequenceTranslator.java  |    6 +-
 .../text/translate/CodePointTranslator.java     |    2 -
 .../commons/text/translate/EntityArrays.java    |    2 -
 .../text/translate/JavaUnicodeEscaper.java      |   14 +-
 .../text/translate/LookupTranslator.java        |    2 -
 .../text/translate/NumericEntityEscaper.java    |   14 +-
 .../text/translate/NumericEntityUnescaper.java  |    2 -
 .../commons/text/translate/OctalUnescaper.java  |    2 -
 .../commons/text/translate/UnicodeEscaper.java  |   14 +-
 .../text/translate/UnicodeUnescaper.java        |    2 -
 .../commons/text/translate/package-info.java    |    2 -
 .../commons/text/CompositeFormatTest.java       |   85 +
 .../commons/text/ExtendedMessageFormatTest.java |  484 +++
 .../commons/text/FormattableUtilsTest.java      |  115 +
 .../text/StrBuilderAppendInsertTest.java        | 1607 +++++++++
 .../org/apache/commons/text/StrBuilderTest.java | 2009 +++++++++++
 .../org/apache/commons/text/StrLookupTest.java  |  115 +
 .../org/apache/commons/text/StrMatcherTest.java |  214 ++
 .../apache/commons/text/StrSubstitutorTest.java |  740 +++++
 .../apache/commons/text/StrTokenizerTest.java   |  879 +++++
 .../org/apache/commons/text/WordUtilsTest.java  |  421 +++
 .../text/translate/AggregateTranslatorTest.java |   56 +
 .../text/translate/EntityArraysTest.java        |   37 +-
 .../text/translate/JavaUnicodeEscaperTest.java  |   65 +
 37 files changed, 14575 insertions(+), 45 deletions(-)
----------------------------------------------------------------------



[07/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/StrTokenizer.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/StrTokenizer.java b/src/main/java/org/apache/commons/text/StrTokenizer.java
new file mode 100644
index 0000000..a980bf9
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/StrTokenizer.java
@@ -0,0 +1,1116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.NoSuchElementException;
+
+/**
+ * Tokenizes a string based based on delimiters (separators)
+ * and supporting quoting and ignored character concepts.
+ * <p>
+ * This class can split a String into many smaller strings. It aims
+ * to do a similar job to {@link java.util.StringTokenizer StringTokenizer},
+ * however it offers much more control and flexibility including implementing
+ * the <code>ListIterator</code> interface. By default, it is set up
+ * like <code>StringTokenizer</code>.
+ * <p>
+ * The input String is split into a number of <i>tokens</i>.
+ * Each token is separated from the next String by a <i>delimiter</i>.
+ * One or more delimiter characters must be specified.
+ * <p>
+ * Each token may be surrounded by quotes.
+ * The <i>quote</i> matcher specifies the quote character(s).
+ * A quote may be escaped within a quoted section by duplicating itself.
+ * <p>
+ * Between each token and the delimiter are potentially characters that need trimming.
+ * The <i>trimmer</i> matcher specifies these characters.
+ * One usage might be to trim whitespace characters.
+ * <p>
+ * At any point outside the quotes there might potentially be invalid characters.
+ * The <i>ignored</i> matcher specifies these characters to be removed.
+ * One usage might be to remove new line characters.
+ * <p>
+ * Empty tokens may be removed or returned as null.
+ * <pre>
+ * "a,b,c"         - Three tokens "a","b","c"   (comma delimiter)
+ * " a, b , c "    - Three tokens "a","b","c"   (default CSV processing trims whitespace)
+ * "a, ", b ,", c" - Three tokens "a, " , " b ", ", c" (quoted text untouched)
+ * </pre>
+ * <p>
+ *
+ * This tokenizer has the following properties and options:
+ *
+ * <table summary="Tokenizer Properties">
+ *  <tr>
+ *   <th>Property</th><th>Type</th><th>Default</th>
+ *  </tr>
+ *  <tr>
+ *   <td>delim</td><td>CharSetMatcher</td><td>{ \t\n\r\f}</td>
+ *  </tr>
+ *  <tr>
+ *   <td>quote</td><td>NoneMatcher</td><td>{}</td>
+ *  </tr>
+ *  <tr>
+ *   <td>ignore</td><td>NoneMatcher</td><td>{}</td>
+ *  </tr>
+ *  <tr>
+ *   <td>emptyTokenAsNull</td><td>boolean</td><td>false</td>
+ *  </tr>
+ *  <tr>
+ *   <td>ignoreEmptyTokens</td><td>boolean</td><td>true</td>
+ *  </tr>
+ * </table>
+ */
+public class StrTokenizer implements ListIterator<String>, Cloneable {
+
+    private static final StrTokenizer CSV_TOKENIZER_PROTOTYPE;
+    private static final StrTokenizer TSV_TOKENIZER_PROTOTYPE;
+    static {
+        CSV_TOKENIZER_PROTOTYPE = new StrTokenizer();
+        CSV_TOKENIZER_PROTOTYPE.setDelimiterMatcher(StrMatcher.commaMatcher());
+        CSV_TOKENIZER_PROTOTYPE.setQuoteMatcher(StrMatcher.doubleQuoteMatcher());
+        CSV_TOKENIZER_PROTOTYPE.setIgnoredMatcher(StrMatcher.noneMatcher());
+        CSV_TOKENIZER_PROTOTYPE.setTrimmerMatcher(StrMatcher.trimMatcher());
+        CSV_TOKENIZER_PROTOTYPE.setEmptyTokenAsNull(false);
+        CSV_TOKENIZER_PROTOTYPE.setIgnoreEmptyTokens(false);
+
+        TSV_TOKENIZER_PROTOTYPE = new StrTokenizer();
+        TSV_TOKENIZER_PROTOTYPE.setDelimiterMatcher(StrMatcher.tabMatcher());
+        TSV_TOKENIZER_PROTOTYPE.setQuoteMatcher(StrMatcher.doubleQuoteMatcher());
+        TSV_TOKENIZER_PROTOTYPE.setIgnoredMatcher(StrMatcher.noneMatcher());
+        TSV_TOKENIZER_PROTOTYPE.setTrimmerMatcher(StrMatcher.trimMatcher());
+        TSV_TOKENIZER_PROTOTYPE.setEmptyTokenAsNull(false);
+        TSV_TOKENIZER_PROTOTYPE.setIgnoreEmptyTokens(false);
+    }
+
+    /** The text to work on. */
+    private char chars[];
+    /** The parsed tokens */
+    private String tokens[];
+    /** The current iteration position */
+    private int tokenPos;
+
+    /** The delimiter matcher */
+    private StrMatcher delimMatcher = StrMatcher.splitMatcher();
+    /** The quote matcher */
+    private StrMatcher quoteMatcher = StrMatcher.noneMatcher();
+    /** The ignored matcher */
+    private StrMatcher ignoredMatcher = StrMatcher.noneMatcher();
+    /** The trimmer matcher */
+    private StrMatcher trimmerMatcher = StrMatcher.noneMatcher();
+
+    /** Whether to return empty tokens as null */
+    private boolean emptyAsNull = false;
+    /** Whether to ignore empty tokens */
+    private boolean ignoreEmptyTokens = true;
+
+    //-----------------------------------------------------------------------
+
+    /**
+     * Returns a clone of <code>CSV_TOKENIZER_PROTOTYPE</code>.
+     *
+     * @return a clone of <code>CSV_TOKENIZER_PROTOTYPE</code>.
+     */
+    private static StrTokenizer getCSVClone() {
+        return (StrTokenizer) CSV_TOKENIZER_PROTOTYPE.clone();
+    }
+
+    /**
+     * Gets a new tokenizer instance which parses Comma Separated Value strings
+     * initializing it with the given input.  The default for CSV processing
+     * will be trim whitespace from both ends (which can be overridden with
+     * the setTrimmer method).
+     * <p>
+     * You must call a "reset" method to set the string which you want to parse.
+     * @return a new tokenizer instance which parses Comma Separated Value strings
+     */
+    public static StrTokenizer getCSVInstance() {
+        return getCSVClone();
+    }
+
+    /**
+     * Gets a new tokenizer instance which parses Comma Separated Value strings
+     * initializing it with the given input.  The default for CSV processing
+     * will be trim whitespace from both ends (which can be overridden with
+     * the setTrimmer method).
+     *
+     * @param input  the text to parse
+     * @return a new tokenizer instance which parses Comma Separated Value strings
+     */
+    public static StrTokenizer getCSVInstance(final String input) {
+        final StrTokenizer tok = getCSVClone();
+        tok.reset(input);
+        return tok;
+    }
+
+    /**
+     * Gets a new tokenizer instance which parses Comma Separated Value strings
+     * initializing it with the given input.  The default for CSV processing
+     * will be trim whitespace from both ends (which can be overridden with
+     * the setTrimmer method).
+     *
+     * @param input  the text to parse
+     * @return a new tokenizer instance which parses Comma Separated Value strings
+     */
+    public static StrTokenizer getCSVInstance(final char[] input) {
+        final StrTokenizer tok = getCSVClone();
+        tok.reset(input);
+        return tok;
+    }
+
+    /**
+     * Returns a clone of <code>TSV_TOKENIZER_PROTOTYPE</code>.
+     *
+     * @return a clone of <code>TSV_TOKENIZER_PROTOTYPE</code>.
+     */
+    private static StrTokenizer getTSVClone() {
+        return (StrTokenizer) TSV_TOKENIZER_PROTOTYPE.clone();
+    }
+
+
+    /**
+     * Gets a new tokenizer instance which parses Tab Separated Value strings.
+     * The default for CSV processing will be trim whitespace from both ends
+     * (which can be overridden with the setTrimmer method).
+     * <p>
+     * You must call a "reset" method to set the string which you want to parse.
+     * @return a new tokenizer instance which parses Tab Separated Value strings.
+     */
+    public static StrTokenizer getTSVInstance() {
+        return getTSVClone();
+    }
+
+    /**
+     * Gets a new tokenizer instance which parses Tab Separated Value strings.
+     * The default for CSV processing will be trim whitespace from both ends
+     * (which can be overridden with the setTrimmer method).
+     * @param input  the string to parse
+     * @return a new tokenizer instance which parses Tab Separated Value strings.
+     */
+    public static StrTokenizer getTSVInstance(final String input) {
+        final StrTokenizer tok = getTSVClone();
+        tok.reset(input);
+        return tok;
+    }
+
+    /**
+     * Gets a new tokenizer instance which parses Tab Separated Value strings.
+     * The default for CSV processing will be trim whitespace from both ends
+     * (which can be overridden with the setTrimmer method).
+     * @param input  the string to parse
+     * @return a new tokenizer instance which parses Tab Separated Value strings.
+     */
+    public static StrTokenizer getTSVInstance(final char[] input) {
+        final StrTokenizer tok = getTSVClone();
+        tok.reset(input);
+        return tok;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Constructs a tokenizer splitting on space, tab, newline and formfeed
+     * as per StringTokenizer, but with no text to tokenize.
+     * <p>
+     * This constructor is normally used with {@link #reset(String)}.
+     */
+    public StrTokenizer() {
+        super();
+        this.chars = null;
+    }
+
+    /**
+     * Constructs a tokenizer splitting on space, tab, newline and formfeed
+     * as per StringTokenizer.
+     *
+     * @param input  the string which is to be parsed
+     */
+    public StrTokenizer(final String input) {
+        super();
+        if (input != null) {
+            chars = input.toCharArray();
+        } else {
+            chars = null;
+        }
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified delimiter character.
+     *
+     * @param input  the string which is to be parsed
+     * @param delim  the field delimiter character
+     */
+    public StrTokenizer(final String input, final char delim) {
+        this(input);
+        setDelimiterChar(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified delimiter string.
+     *
+     * @param input  the string which is to be parsed
+     * @param delim  the field delimiter string
+     */
+    public StrTokenizer(final String input, final String delim) {
+        this(input);
+        setDelimiterString(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting using the specified delimiter matcher.
+     *
+     * @param input  the string which is to be parsed
+     * @param delim  the field delimiter matcher
+     */
+    public StrTokenizer(final String input, final StrMatcher delim) {
+        this(input);
+        setDelimiterMatcher(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified delimiter character
+     * and handling quotes using the specified quote character.
+     *
+     * @param input  the string which is to be parsed
+     * @param delim  the field delimiter character
+     * @param quote  the field quoted string character
+     */
+    public StrTokenizer(final String input, final char delim, final char quote) {
+        this(input, delim);
+        setQuoteChar(quote);
+    }
+
+    /**
+     * Constructs a tokenizer splitting using the specified delimiter matcher
+     * and handling quotes using the specified quote matcher.
+     *
+     * @param input  the string which is to be parsed
+     * @param delim  the field delimiter matcher
+     * @param quote  the field quoted string matcher
+     */
+    public StrTokenizer(final String input, final StrMatcher delim, final StrMatcher quote) {
+        this(input, delim);
+        setQuoteMatcher(quote);
+    }
+
+    /**
+     * Constructs a tokenizer splitting on space, tab, newline and formfeed
+     * as per StringTokenizer.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     */
+    public StrTokenizer(final char[] input) {
+        super();
+        if (input == null) {
+            this.chars = null;
+        } else {
+            this.chars = input.clone();
+        }
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified character.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     * @param delim the field delimiter character
+     */
+    public StrTokenizer(final char[] input, final char delim) {
+        this(input);
+        setDelimiterChar(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified string.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     * @param delim the field delimiter string
+     */
+    public StrTokenizer(final char[] input, final String delim) {
+        this(input);
+        setDelimiterString(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting using the specified delimiter matcher.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     * @param delim  the field delimiter matcher
+     */
+    public StrTokenizer(final char[] input, final StrMatcher delim) {
+        this(input);
+        setDelimiterMatcher(delim);
+    }
+
+    /**
+     * Constructs a tokenizer splitting on the specified delimiter character
+     * and handling quotes using the specified quote character.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     * @param delim  the field delimiter character
+     * @param quote  the field quoted string character
+     */
+    public StrTokenizer(final char[] input, final char delim, final char quote) {
+        this(input, delim);
+        setQuoteChar(quote);
+    }
+
+    /**
+     * Constructs a tokenizer splitting using the specified delimiter matcher
+     * and handling quotes using the specified quote matcher.
+     *
+     * @param input  the string which is to be parsed, not cloned
+     * @param delim  the field delimiter character
+     * @param quote  the field quoted string character
+     */
+    public StrTokenizer(final char[] input, final StrMatcher delim, final StrMatcher quote) {
+        this(input, delim);
+        setQuoteMatcher(quote);
+    }
+
+    // API
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the number of tokens found in the String.
+     *
+     * @return the number of matched tokens
+     */
+    public int size() {
+        checkTokenized();
+        return tokens.length;
+    }
+
+    /**
+     * Gets the next token from the String.
+     * Equivalent to {@link #next()} except it returns null rather than
+     * throwing {@link NoSuchElementException} when no tokens remain.
+     *
+     * @return the next sequential token, or null when no more tokens are found
+     */
+    public String nextToken() {
+        if (hasNext()) {
+            return tokens[tokenPos++];
+        }
+        return null;
+    }
+
+    /**
+     * Gets the previous token from the String.
+     *
+     * @return the previous sequential token, or null when no more tokens are found
+     */
+    public String previousToken() {
+        if (hasPrevious()) {
+            return tokens[--tokenPos];
+        }
+        return null;
+    }
+
+    /**
+     * Gets a copy of the full token list as an independent modifiable array.
+     *
+     * @return the tokens as a String array
+     */
+    public String[] getTokenArray() {
+        checkTokenized();
+        return tokens.clone();
+    }
+
+    /**
+     * Gets a copy of the full token list as an independent modifiable list.
+     *
+     * @return the tokens as a String array
+     */
+    public List<String> getTokenList() {
+        checkTokenized();
+        final List<String> list = new ArrayList<>(tokens.length);
+        for (final String element : tokens) {
+            list.add(element);
+        }
+        return list;
+    }
+
+    /**
+     * Resets this tokenizer, forgetting all parsing and iteration already completed.
+     * <p>
+     * This method allows the same tokenizer to be reused for the same String.
+     *
+     * @return this, to enable chaining
+     */
+    public org.apache.commons.text.StrTokenizer reset() {
+        tokenPos = 0;
+        tokens = null;
+        return this;
+    }
+
+    /**
+     * Reset this tokenizer, giving it a new input string to parse.
+     * In this manner you can re-use a tokenizer with the same settings
+     * on multiple input lines.
+     *
+     * @param input  the new string to tokenize, null sets no text to parse
+     * @return this, to enable chaining
+     */
+    public org.apache.commons.text.StrTokenizer reset(final String input) {
+        reset();
+        if (input != null) {
+            this.chars = input.toCharArray();
+        } else {
+            this.chars = null;
+        }
+        return this;
+    }
+
+    /**
+     * Reset this tokenizer, giving it a new input string to parse.
+     * In this manner you can re-use a tokenizer with the same settings
+     * on multiple input lines.
+     *
+     * @param input  the new character array to tokenize, not cloned, null sets no text to parse
+     * @return this, to enable chaining
+     */
+    public org.apache.commons.text.StrTokenizer reset(final char[] input) {
+        reset();
+        if (input != null) {
+            this.chars = input;
+        } else {
+            this.chars = null;
+        }
+        return this;
+    }
+
+    // ListIterator
+    //-----------------------------------------------------------------------
+    /**
+     * Checks whether there are any more tokens.
+     *
+     * @return true if there are more tokens
+     */
+    @Override
+    public boolean hasNext() {
+        checkTokenized();
+        return tokenPos < tokens.length;
+    }
+
+    /**
+     * Gets the next token.
+     *
+     * @return the next String token
+     * @throws NoSuchElementException if there are no more elements
+     */
+    @Override
+    public String next() {
+        if (hasNext()) {
+            return tokens[tokenPos++];
+        }
+        throw new NoSuchElementException();
+    }
+
+    /**
+     * Gets the index of the next token to return.
+     *
+     * @return the next token index
+     */
+    @Override
+    public int nextIndex() {
+        return tokenPos;
+    }
+
+    /**
+     * Checks whether there are any previous tokens that can be iterated to.
+     *
+     * @return true if there are previous tokens
+     */
+    @Override
+    public boolean hasPrevious() {
+        checkTokenized();
+        return tokenPos > 0;
+    }
+
+    /**
+     * Gets the token previous to the last returned token.
+     *
+     * @return the previous token
+     */
+    @Override
+    public String previous() {
+        if (hasPrevious()) {
+            return tokens[--tokenPos];
+        }
+        throw new NoSuchElementException();
+    }
+
+    /**
+     * Gets the index of the previous token.
+     *
+     * @return the previous token index
+     */
+    @Override
+    public int previousIndex() {
+        return tokenPos - 1;
+    }
+
+    /**
+     * Unsupported ListIterator operation.
+     *
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    public void remove() {
+        throw new UnsupportedOperationException("remove() is unsupported");
+    }
+
+    /**
+     * Unsupported ListIterator operation.
+     * @param obj this parameter ignored.
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    public void set(final String obj) {
+        throw new UnsupportedOperationException("set() is unsupported");
+    }
+
+    /**
+     * Unsupported ListIterator operation.
+     * @param obj this parameter ignored.
+     * @throws UnsupportedOperationException always
+     */
+    @Override
+    public void add(final String obj) {
+        throw new UnsupportedOperationException("add() is unsupported");
+    }
+
+    // Implementation
+    //-----------------------------------------------------------------------
+    /**
+     * Checks if tokenization has been done, and if not then do it.
+     */
+    private void checkTokenized() {
+        if (tokens == null) {
+            if (chars == null) {
+                // still call tokenize as subclass may do some work
+                final List<String> split = tokenize(null, 0, 0);
+                tokens = split.toArray(new String[split.size()]);
+            } else {
+                final List<String> split = tokenize(chars, 0, chars.length);
+                tokens = split.toArray(new String[split.size()]);
+            }
+        }
+    }
+
+    /**
+     * Internal method to performs the tokenization.
+     * <p>
+     * Most users of this class do not need to call this method. This method
+     * will be called automatically by other (public) methods when required.
+     * <p>
+     * This method exists to allow subclasses to add code before or after the
+     * tokenization. For example, a subclass could alter the character array,
+     * offset or count to be parsed, or call the tokenizer multiple times on
+     * multiple strings. It is also be possible to filter the results.
+     * <p>
+     * <code>StrTokenizer</code> will always pass a zero offset and a count
+     * equal to the length of the array to this method, however a subclass
+     * may pass other values, or even an entirely different array.
+     *
+     * @param srcChars  the character array being tokenized, may be null
+     * @param offset  the start position within the character array, must be valid
+     * @param count  the number of characters to tokenize, must be valid
+     * @return the modifiable list of String tokens, unmodifiable if null array or zero count
+     */
+    protected List<String> tokenize(final char[] srcChars, final int offset, final int count) {
+        if (srcChars == null || count == 0) {
+            return Collections.emptyList();
+        }
+        final StrBuilder buf = new StrBuilder();
+        final List<String> tokenList = new ArrayList<>();
+        int pos = offset;
+
+        // loop around the entire buffer
+        while (pos >= 0 && pos < count) {
+            // find next token
+            pos = readNextToken(srcChars, pos, count, buf, tokenList);
+
+            // handle case where end of string is a delimiter
+            if (pos >= count) {
+                addToken(tokenList, "");
+            }
+        }
+        return tokenList;
+    }
+
+    /**
+     * Adds a token to a list, paying attention to the parameters we've set.
+     *
+     * @param list  the list to add to
+     * @param tok  the token to add
+     */
+    private void addToken(final List<String> list, String tok) {
+        if (tok == null || tok.length() == 0) {
+            if (isIgnoreEmptyTokens()) {
+                return;
+            }
+            if (isEmptyTokenAsNull()) {
+                tok = null;
+            }
+        }
+        list.add(tok);
+    }
+
+    /**
+     * Reads character by character through the String to get the next token.
+     *
+     * @param srcChars  the character array being tokenized
+     * @param start  the first character of field
+     * @param len  the length of the character array being tokenized
+     * @param workArea  a temporary work area
+     * @param tokenList  the list of parsed tokens
+     * @return the starting position of the next field (the character
+     *  immediately after the delimiter), or -1 if end of string found
+     */
+    private int readNextToken(final char[] srcChars, int start, final int len, final StrBuilder workArea, final List<String> tokenList) {
+        // skip all leading whitespace, unless it is the
+        // field delimiter or the quote character
+        while (start < len) {
+            final int removeLen = Math.max(
+                    getIgnoredMatcher().isMatch(srcChars, start, start, len),
+                    getTrimmerMatcher().isMatch(srcChars, start, start, len));
+            if (removeLen == 0 ||
+                getDelimiterMatcher().isMatch(srcChars, start, start, len) > 0 ||
+                getQuoteMatcher().isMatch(srcChars, start, start, len) > 0) {
+                break;
+            }
+            start += removeLen;
+        }
+
+        // handle reaching end
+        if (start >= len) {
+            addToken(tokenList, "");
+            return -1;
+        }
+
+        // handle empty token
+        final int delimLen = getDelimiterMatcher().isMatch(srcChars, start, start, len);
+        if (delimLen > 0) {
+            addToken(tokenList, "");
+            return start + delimLen;
+        }
+
+        // handle found token
+        final int quoteLen = getQuoteMatcher().isMatch(srcChars, start, start, len);
+        if (quoteLen > 0) {
+            return readWithQuotes(srcChars, start + quoteLen, len, workArea, tokenList, start, quoteLen);
+        }
+        return readWithQuotes(srcChars, start, len, workArea, tokenList, 0, 0);
+    }
+
+    /**
+     * Reads a possibly quoted string token.
+     *
+     * @param srcChars  the character array being tokenized
+     * @param start  the first character of field
+     * @param len  the length of the character array being tokenized
+     * @param workArea  a temporary work area
+     * @param tokenList  the list of parsed tokens
+     * @param quoteStart  the start position of the matched quote, 0 if no quoting
+     * @param quoteLen  the length of the matched quote, 0 if no quoting
+     * @return the starting position of the next field (the character
+     *  immediately after the delimiter, or if end of string found,
+     *  then the length of string
+     */
+    private int readWithQuotes(final char[] srcChars, final int start, final int len, final StrBuilder workArea,
+                               final List<String> tokenList, final int quoteStart, final int quoteLen) {
+        // Loop until we've found the end of the quoted
+        // string or the end of the input
+        workArea.clear();
+        int pos = start;
+        boolean quoting = quoteLen > 0;
+        int trimStart = 0;
+
+        while (pos < len) {
+            // quoting mode can occur several times throughout a string
+            // we must switch between quoting and non-quoting until we
+            // encounter a non-quoted delimiter, or end of string
+            if (quoting) {
+                // In quoting mode
+
+                // If we've found a quote character, see if it's
+                // followed by a second quote.  If so, then we need
+                // to actually put the quote character into the token
+                // rather than end the token.
+                if (isQuote(srcChars, pos, len, quoteStart, quoteLen)) {
+                    if (isQuote(srcChars, pos + quoteLen, len, quoteStart, quoteLen)) {
+                        // matched pair of quotes, thus an escaped quote
+                        workArea.append(srcChars, pos, quoteLen);
+                        pos += quoteLen * 2;
+                        trimStart = workArea.size();
+                        continue;
+                    }
+
+                    // end of quoting
+                    quoting = false;
+                    pos += quoteLen;
+                    continue;
+                }
+
+                // copy regular character from inside quotes
+                workArea.append(srcChars[pos++]);
+                trimStart = workArea.size();
+
+            } else {
+                // Not in quoting mode
+
+                // check for delimiter, and thus end of token
+                final int delimLen = getDelimiterMatcher().isMatch(srcChars, pos, start, len);
+                if (delimLen > 0) {
+                    // return condition when end of token found
+                    addToken(tokenList, workArea.substring(0, trimStart));
+                    return pos + delimLen;
+                }
+
+                // check for quote, and thus back into quoting mode
+                if (quoteLen > 0 && isQuote(srcChars, pos, len, quoteStart, quoteLen)) {
+                    quoting = true;
+                    pos += quoteLen;
+                    continue;
+                }
+
+                // check for ignored (outside quotes), and ignore
+                final int ignoredLen = getIgnoredMatcher().isMatch(srcChars, pos, start, len);
+                if (ignoredLen > 0) {
+                    pos += ignoredLen;
+                    continue;
+                }
+
+                // check for trimmed character
+                // don't yet know if its at the end, so copy to workArea
+                // use trimStart to keep track of trim at the end
+                final int trimmedLen = getTrimmerMatcher().isMatch(srcChars, pos, start, len);
+                if (trimmedLen > 0) {
+                    workArea.append(srcChars, pos, trimmedLen);
+                    pos += trimmedLen;
+                    continue;
+                }
+
+                // copy regular character from outside quotes
+                workArea.append(srcChars[pos++]);
+                trimStart = workArea.size();
+            }
+        }
+
+        // return condition when end of string found
+        addToken(tokenList, workArea.substring(0, trimStart));
+        return -1;
+    }
+
+    /**
+     * Checks if the characters at the index specified match the quote
+     * already matched in readNextToken().
+     *
+     * @param srcChars  the character array being tokenized
+     * @param pos  the position to check for a quote
+     * @param len  the length of the character array being tokenized
+     * @param quoteStart  the start position of the matched quote, 0 if no quoting
+     * @param quoteLen  the length of the matched quote, 0 if no quoting
+     * @return true if a quote is matched
+     */
+    private boolean isQuote(final char[] srcChars, final int pos, final int len, final int quoteStart, final int quoteLen) {
+        for (int i = 0; i < quoteLen; i++) {
+            if (pos + i >= len || srcChars[pos + i] != srcChars[quoteStart + i]) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // Delimiter
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the field delimiter matcher.
+     *
+     * @return the delimiter matcher in use
+     */
+    public StrMatcher getDelimiterMatcher() {
+        return this.delimMatcher;
+    }
+
+    /**
+     * Sets the field delimiter matcher.
+     * <p>
+     * The delimitier is used to separate one token from another.
+     *
+     * @param delim  the delimiter matcher to use
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setDelimiterMatcher(final StrMatcher delim) {
+        if (delim == null) {
+            this.delimMatcher = StrMatcher.noneMatcher();
+        } else {
+            this.delimMatcher = delim;
+        }
+        return this;
+    }
+
+    /**
+     * Sets the field delimiter character.
+     *
+     * @param delim  the delimiter character to use
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setDelimiterChar(final char delim) {
+        return setDelimiterMatcher(StrMatcher.charMatcher(delim));
+    }
+
+    /**
+     * Sets the field delimiter string.
+     *
+     * @param delim  the delimiter string to use
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setDelimiterString(final String delim) {
+        return setDelimiterMatcher(StrMatcher.stringMatcher(delim));
+    }
+
+    // Quote
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the quote matcher currently in use.
+     * <p>
+     * The quote character is used to wrap data between the tokens.
+     * This enables delimiters to be entered as data.
+     * The default value is '"' (double quote).
+     *
+     * @return the quote matcher in use
+     */
+    public StrMatcher getQuoteMatcher() {
+        return quoteMatcher;
+    }
+
+    /**
+     * Set the quote matcher to use.
+     * <p>
+     * The quote character is used to wrap data between the tokens.
+     * This enables delimiters to be entered as data.
+     *
+     * @param quote  the quote matcher to use, null ignored
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setQuoteMatcher(final StrMatcher quote) {
+        if (quote != null) {
+            this.quoteMatcher = quote;
+        }
+        return this;
+    }
+
+    /**
+     * Sets the quote character to use.
+     * <p>
+     * The quote character is used to wrap data between the tokens.
+     * This enables delimiters to be entered as data.
+     *
+     * @param quote  the quote character to use
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setQuoteChar(final char quote) {
+        return setQuoteMatcher(StrMatcher.charMatcher(quote));
+    }
+
+    // Ignored
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the ignored character matcher.
+     * <p>
+     * These characters are ignored when parsing the String, unless they are
+     * within a quoted region.
+     * The default value is not to ignore anything.
+     *
+     * @return the ignored matcher in use
+     */
+    public StrMatcher getIgnoredMatcher() {
+        return ignoredMatcher;
+    }
+
+    /**
+     * Set the matcher for characters to ignore.
+     * <p>
+     * These characters are ignored when parsing the String, unless they are
+     * within a quoted region.
+     *
+     * @param ignored  the ignored matcher to use, null ignored
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setIgnoredMatcher(final StrMatcher ignored) {
+        if (ignored != null) {
+            this.ignoredMatcher = ignored;
+        }
+        return this;
+    }
+
+    /**
+     * Set the character to ignore.
+     * <p>
+     * This character is ignored when parsing the String, unless it is
+     * within a quoted region.
+     *
+     * @param ignored  the ignored character to use
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setIgnoredChar(final char ignored) {
+        return setIgnoredMatcher(StrMatcher.charMatcher(ignored));
+    }
+
+    // Trimmer
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the trimmer character matcher.
+     * <p>
+     * These characters are trimmed off on each side of the delimiter
+     * until the token or quote is found.
+     * The default value is not to trim anything.
+     *
+     * @return the trimmer matcher in use
+     */
+    public StrMatcher getTrimmerMatcher() {
+        return trimmerMatcher;
+    }
+
+    /**
+     * Sets the matcher for characters to trim.
+     * <p>
+     * These characters are trimmed off on each side of the delimiter
+     * until the token or quote is found.
+     *
+     * @param trimmer  the trimmer matcher to use, null ignored
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setTrimmerMatcher(final StrMatcher trimmer) {
+        if (trimmer != null) {
+            this.trimmerMatcher = trimmer;
+        }
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets whether the tokenizer currently returns empty tokens as null.
+     * The default for this property is false.
+     *
+     * @return true if empty tokens are returned as null
+     */
+    public boolean isEmptyTokenAsNull() {
+        return this.emptyAsNull;
+    }
+
+    /**
+     * Sets whether the tokenizer should return empty tokens as null.
+     * The default for this property is false.
+     *
+     * @param emptyAsNull  whether empty tokens are returned as null
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setEmptyTokenAsNull(final boolean emptyAsNull) {
+        this.emptyAsNull = emptyAsNull;
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets whether the tokenizer currently ignores empty tokens.
+     * The default for this property is true.
+     *
+     * @return true if empty tokens are not returned
+     */
+    public boolean isIgnoreEmptyTokens() {
+        return ignoreEmptyTokens;
+    }
+
+    /**
+     * Sets whether the tokenizer should ignore and not return empty tokens.
+     * The default for this property is true.
+     *
+     * @param ignoreEmptyTokens  whether empty tokens are not returned
+     * @return this, to enable chaining
+     */
+    public StrTokenizer setIgnoreEmptyTokens(final boolean ignoreEmptyTokens) {
+        this.ignoreEmptyTokens = ignoreEmptyTokens;
+        return this;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the String content that the tokenizer is parsing.
+     *
+     * @return the string content being parsed
+     */
+    public String getContent() {
+        if (chars == null) {
+            return null;
+        }
+        return new String(chars);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Creates a new instance of this Tokenizer. The new instance is reset so
+     * that it will be at the start of the token list.
+     * If a {@link CloneNotSupportedException} is caught, return <code>null</code>.
+     * 
+     * @return a new instance of this Tokenizer which has been reset.
+     */
+    @Override
+    public Object clone() {
+        try {
+            return cloneReset();
+        } catch (final CloneNotSupportedException ex) {
+            return null;
+        }
+    }
+
+    /**
+     * Creates a new instance of this Tokenizer. The new instance is reset so that
+     * it will be at the start of the token list.
+     * 
+     * @return a new instance of this Tokenizer which has been reset.
+     * @throws CloneNotSupportedException if there is a problem cloning
+     */
+    Object cloneReset() throws CloneNotSupportedException {
+        // this method exists to enable 100% test coverage
+        final StrTokenizer cloned = (StrTokenizer) super.clone();
+        if (cloned.chars != null) {
+            cloned.chars = cloned.chars.clone();
+        }
+        cloned.reset();
+        return cloned;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the String content that the tokenizer is parsing.
+     *
+     * @return the string content being parsed
+     */
+    @Override
+    public String toString() {
+        if (tokens == null) {
+            return "StrTokenizer[not tokenized yet]";
+        }
+        return "StrTokenizer" + getTokenList();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/WordUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/WordUtils.java b/src/main/java/org/apache/commons/text/WordUtils.java
new file mode 100644
index 0000000..1d0085c
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/WordUtils.java
@@ -0,0 +1,733 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.lang.reflect.Array;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * <p>Operations on Strings that contain words.</p>
+ * 
+ * <p>This class tries to handle <code>null</code> input gracefully.
+ * An exception will not be thrown for a <code>null</code> input.
+ * Each method documents its behaviour in more detail.</p>
+ */
+public class WordUtils {
+
+    /**
+     * <p><code>WordUtils</code> instances should NOT be constructed in
+     * standard programming. Instead, the class should be used as
+     * <code>WordUtils.wrap("foo bar", 20);</code>.</p>
+     *
+     * <p>This constructor is public to permit tools that require a JavaBean
+     * instance to operate.</p>
+     */
+    public WordUtils() {
+      super();
+    }
+
+    // Wrapping
+    //--------------------------------------------------------------------------
+    /**
+     * <p>Wraps a single line of text, identifying words by <code>' '</code>.</p>
+     * 
+     * <p>New lines will be separated by the system property line separator.
+     * Very long words, such as URLs will <i>not</i> be wrapped.</p>
+     * 
+     * <p>Leading spaces on a new line are stripped.
+     * Trailing spaces are not stripped.</p>
+     *
+     * <table border="1" summary="Wrap Results">
+     *  <tr>
+     *   <th>input</th>
+     *   <th>wrapLength</th>
+     *   <th>result</th>
+     *  </tr>
+     *  <tr>
+     *   <td>null</td>
+     *   <td>*</td>
+     *   <td>null</td>
+     *  </tr>
+     *  <tr>
+     *   <td>""</td>
+     *   <td>*</td>
+     *   <td>""</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
+     *   <td>20</td>
+     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org"</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here, http://commons.apache.org, to jump to the commons website"</td>
+     *   <td>20</td>
+     *   <td>"Click here,\nhttp://commons.apache.org,\nto jump to the\ncommons website"</td>
+     *  </tr>
+     * </table>
+     *
+     * (assuming that '\n' is the systems line separator)
+     *
+     * @param str  the String to be word wrapped, may be null
+     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
+     * @return a line with newlines inserted, <code>null</code> if null input
+     */
+    public static String wrap(final String str, final int wrapLength) {
+        return wrap(str, wrapLength, null, false);
+    }
+    
+    /**
+     * <p>Wraps a single line of text, identifying words by <code>' '</code>.</p>
+     * 
+     * <p>Leading spaces on a new line are stripped.
+     * Trailing spaces are not stripped.</p>
+     *
+     * <table border="1" summary="Wrap Results">
+     *  <tr>
+     *   <th>input</th>
+     *   <th>wrapLenght</th>
+     *   <th>newLineString</th>
+     *   <th>wrapLongWords</th>
+     *   <th>result</th>
+     *  </tr>
+     *  <tr>
+     *   <td>null</td>
+     *   <td>*</td>
+     *   <td>*</td>
+     *   <td>true/false</td>
+     *   <td>null</td>
+     *  </tr>
+     *  <tr>
+     *   <td>""</td>
+     *   <td>*</td>
+     *   <td>*</td>
+     *   <td>true/false</td>
+     *   <td>""</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>true/false</td>
+     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>"&lt;br /&gt;"</td>
+     *   <td>true/false</td>
+     *   <td>"Here is one line of&lt;br /&gt;text that is going&lt;br /&gt;to be wrapped after&lt;br /&gt;20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>null</td>
+     *   <td>true/false</td>
+     *   <td>"Here is one line of" + systemNewLine + "text that is going" + systemNewLine + "to be wrapped after" + systemNewLine + "20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>false</td>
+     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org"</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>true</td>
+     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org"</td>
+     *  </tr>
+     * </table>
+     *
+     * @param str  the String to be word wrapped, may be null
+     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
+     * @param newLineStr  the string to insert for a new line, 
+     *  <code>null</code> uses the system property line separator
+     * @param wrapLongWords  true if long words (such as URLs) should be wrapped
+     * @return a line with newlines inserted, <code>null</code> if null input
+     */
+    public static String wrap(final String str, final int wrapLength, final String newLineStr, final boolean wrapLongWords) {
+        return wrap(str, wrapLength, newLineStr, wrapLongWords, " ");
+    }
+
+    /**
+     * <p>Wraps a single line of text, identifying words by <code>wrapOn</code>.</p>
+     *
+     * <p>Leading spaces on a new line are stripped.
+     * Trailing spaces are not stripped.</p>
+     *
+     * <table border="1" summary="Wrap Results">
+     *  <tr>
+     *   <th>input</th>
+     *   <th>wrapLenght</th>
+     *   <th>newLineString</th>
+     *   <th>wrapLongWords</th>
+     *   <th>wrapOn</th>
+     *   <th>result</th>
+     *  </tr>
+     *  <tr>
+     *   <td>null</td>
+     *   <td>*</td>
+     *   <td>*</td>
+     *   <td>true/false</td>
+     *   <td>*</td>
+     *   <td>null</td>
+     *  </tr>
+     *  <tr>
+     *   <td>""</td>
+     *   <td>*</td>
+     *   <td>*</td>
+     *   <td>true/false</td>
+     *   <td>*</td>
+     *   <td>""</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>true/false</td>
+     *   <td>" "</td>
+     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>"&lt;br /&gt;"</td>
+     *   <td>true/false</td>
+     *   <td>" "</td>
+     *   <td>"Here is one line of&lt;br /&gt;text that is going&lt;br /&gt;to be wrapped after&lt;br /&gt;20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
+     *   <td>20</td>
+     *   <td>null</td>
+     *   <td>true/false</td>
+     *   <td>" "</td>
+     *   <td>"Here is one line of" + systemNewLine + "text that is going" + systemNewLine + "to be wrapped after" + systemNewLine + "20 columns."</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>false</td>
+     *   <td>" "</td>
+     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org"</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"Click here to jump to the commons website - http://commons.apache.org"</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>true</td>
+     *   <td>" "</td>
+     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org"</td>
+     *  </tr>
+     *  <tr>
+     *   <td>"flammable/inflammable"</td>
+     *   <td>20</td>
+     *   <td>"\n"</td>
+     *   <td>true</td>
+     *   <td>"/"</td>
+     *   <td>"flammable\ninflammable"</td>
+     *  </tr>
+     * </table>
+     * @param str  the String to be word wrapped, may be null
+     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
+     * @param newLineStr  the string to insert for a new line,
+     *  <code>null</code> uses the system property line separator
+     * @param wrapLongWords  true if long words (such as URLs) should be wrapped
+     * @param wrapOn regex expression to be used as a breakable characters,
+     *               if blank string is provided a space character will be used
+     * @return a line with newlines inserted, <code>null</code> if null input
+     */
+    public static String wrap(final String str, int wrapLength, String newLineStr, final boolean wrapLongWords, String wrapOn) {
+        if (str == null) {
+            return null;
+        }
+        if (newLineStr == null) {
+            newLineStr = System.getProperty("line.separator");
+        }
+        if (wrapLength < 1) {
+            wrapLength = 1;
+        }
+        if (wrapOn == null || wrapOn.length() == 0 || wrapOn.trim().length() == 0) {
+            wrapOn = " ";
+        }
+        final Pattern patternToWrapOn = Pattern.compile(wrapOn);
+        final int inputLineLength = str.length();
+        int offset = 0;
+        final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32);
+
+        while (offset < inputLineLength) {
+            int spaceToWrapAt = -1;
+            Matcher matcher = patternToWrapOn.matcher(str.substring(offset, Math
+                    .min(offset + wrapLength + 1, inputLineLength)));
+            if (matcher.find()) {
+                if (matcher.start() == 0) {
+                    offset += matcher.end();
+                    continue;
+                }else {
+                    spaceToWrapAt = matcher.start();
+                }
+            }
+
+            // only last line without leading spaces is left
+            if(inputLineLength - offset <= wrapLength) {
+                break;
+            }
+
+            while(matcher.find()){
+                spaceToWrapAt = matcher.start() + offset;
+            }
+
+            if (spaceToWrapAt >= offset) {
+                // normal case
+                wrappedLine.append(str.substring(offset, spaceToWrapAt));
+                wrappedLine.append(newLineStr);
+                offset = spaceToWrapAt + 1;
+
+            } else {
+                // really long word or URL
+                if (wrapLongWords) {
+                    // wrap really long word one line at a time
+                    wrappedLine.append(str.substring(offset, wrapLength + offset));
+                    wrappedLine.append(newLineStr);
+                    offset += wrapLength;
+                } else {
+                    // do not wrap really long word, just extend beyond limit
+                    matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength));
+                    if (matcher.find()) {
+                        spaceToWrapAt = matcher.start() + offset + wrapLength;
+                    }
+
+                    if (spaceToWrapAt >= 0) {
+                        wrappedLine.append(str.substring(offset, spaceToWrapAt));
+                        wrappedLine.append(newLineStr);
+                        offset = spaceToWrapAt + 1;
+                    } else {
+                        wrappedLine.append(str.substring(offset));
+                        offset = inputLineLength;
+                    }
+                }
+            }
+        }
+
+        // Whatever is left in line is short enough to just pass through
+        wrappedLine.append(str.substring(offset));
+
+        return wrappedLine.toString();
+    }
+
+    // Capitalizing
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Capitalizes all the whitespace separated words in a String.
+     * Only the first character of each word is changed. To convert the 
+     * rest of each word to lowercase at the same time, 
+     * use {@link #capitalizeFully(String)}.</p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.
+     * Capitalization uses the Unicode title case, normally equivalent to
+     * upper case.</p>
+     *
+     * <pre>
+     * WordUtils.capitalize(null)        = null
+     * WordUtils.capitalize("")          = ""
+     * WordUtils.capitalize("i am FINE") = "I Am FINE"
+     * </pre>
+     * 
+     * @param str  the String to capitalize, may be null
+     * @return capitalized String, <code>null</code> if null String input
+     * @see #uncapitalize(String)
+     * @see #capitalizeFully(String)
+     */
+    public static String capitalize(final String str) {
+        return capitalize(str, null);
+    }
+
+    /**
+     * <p>Capitalizes all the delimiter separated words in a String.
+     * Only the first character of each word is changed. To convert the 
+     * rest of each word to lowercase at the same time, 
+     * use {@link #capitalizeFully(String, char[])}.</p>
+     *
+     * <p>The delimiters represent a set of characters understood to separate words.
+     * The first string character and the first non-delimiter character after a
+     * delimiter will be capitalized. </p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * Capitalization uses the Unicode title case, normally equivalent to
+     * upper case.</p>
+     *
+     * <pre>
+     * WordUtils.capitalize(null, *)            = null
+     * WordUtils.capitalize("", *)              = ""
+     * WordUtils.capitalize(*, new char[0])     = *
+     * WordUtils.capitalize("i am fine", null)  = "I Am Fine"
+     * WordUtils.capitalize("i aM.fine", {'.'}) = "I aM.Fine"
+     * </pre>
+     * 
+     * @param str  the String to capitalize, may be null
+     * @param delimiters  set of characters to determine capitalization, null means whitespace
+     * @return capitalized String, <code>null</code> if null String input
+     * @see #uncapitalize(String)
+     * @see #capitalizeFully(String)
+     * @since 2.1
+     */
+    public static String capitalize(final String str, final char... delimiters) {
+        final int delimLen = delimiters == null ? -1 : delimiters.length;
+        if (str == null || str.length() == 0 || delimLen == 0) {
+            return str;
+        }
+        final char[] buffer = str.toCharArray();
+        boolean capitalizeNext = true;
+        for (int i = 0; i < buffer.length; i++) {
+            final char ch = buffer[i];
+            if (isDelimiter(ch, delimiters)) {
+                capitalizeNext = true;
+            } else if (capitalizeNext) {
+                buffer[i] = Character.toTitleCase(ch);
+                capitalizeNext = false;
+            }
+        }
+        return new String(buffer);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Converts all the whitespace separated words in a String into capitalized words, 
+     * that is each word is made up of a titlecase character and then a series of 
+     * lowercase characters.  </p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.
+     * Capitalization uses the Unicode title case, normally equivalent to
+     * upper case.</p>
+     *
+     * <pre>
+     * WordUtils.capitalizeFully(null)        = null
+     * WordUtils.capitalizeFully("")          = ""
+     * WordUtils.capitalizeFully("i am FINE") = "I Am Fine"
+     * </pre>
+     * 
+     * @param str  the String to capitalize, may be null
+     * @return capitalized String, <code>null</code> if null String input
+     */
+    public static String capitalizeFully(final String str) {
+        return capitalizeFully(str, null);
+    }
+
+    /**
+     * <p>Converts all the delimiter separated words in a String into capitalized words, 
+     * that is each word is made up of a titlecase character and then a series of 
+     * lowercase characters. </p>
+     *
+     * <p>The delimiters represent a set of characters understood to separate words.
+     * The first string character and the first non-delimiter character after a
+     * delimiter will be capitalized. </p>
+     *
+     * <p>A <code>null</code> input String returns <code>null</code>.
+     * Capitalization uses the Unicode title case, normally equivalent to
+     * upper case.</p>
+     *
+     * <pre>
+     * WordUtils.capitalizeFully(null, *)            = null
+     * WordUtils.capitalizeFully("", *)              = ""
+     * WordUtils.capitalizeFully(*, null)            = *
+     * WordUtils.capitalizeFully(*, new char[0])     = *
+     * WordUtils.capitalizeFully("i aM.fine", {'.'}) = "I am.Fine"
+     * </pre>
+     * 
+     * @param str  the String to capitalize, may be null
+     * @param delimiters  set of characters to determine capitalization, null means whitespace
+     * @return capitalized String, <code>null</code> if null String input
+     * @since 2.1
+     */
+    public static String capitalizeFully(String str, final char... delimiters) {
+        final int delimLen = delimiters == null ? -1 : delimiters.length;
+        if (str == null || str.length() == 0 || delimLen == 0) {
+            return str;
+        }
+        str = str.toLowerCase();
+        return capitalize(str, delimiters);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Uncapitalizes all the whitespace separated words in a String.
+     * Only the first character of each word is changed.</p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.</p>
+     *
+     * <pre>
+     * WordUtils.uncapitalize(null)        = null
+     * WordUtils.uncapitalize("")          = ""
+     * WordUtils.uncapitalize("I Am FINE") = "i am fINE"
+     * </pre>
+     * 
+     * @param str  the String to uncapitalize, may be null
+     * @return uncapitalized String, <code>null</code> if null String input
+     * @see #capitalize(String)
+     */
+    public static String uncapitalize(final String str) {
+        return uncapitalize(str, null);
+    }
+
+    /**
+     * <p>Uncapitalizes all the whitespace separated words in a String.
+     * Only the first character of each word is changed.</p>
+     *
+     * <p>The delimiters represent a set of characters understood to separate words.
+     * The first string character and the first non-delimiter character after a
+     * delimiter will be uncapitalized. </p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.</p>
+     *
+     * <pre>
+     * WordUtils.uncapitalize(null, *)            = null
+     * WordUtils.uncapitalize("", *)              = ""
+     * WordUtils.uncapitalize(*, null)            = *
+     * WordUtils.uncapitalize(*, new char[0])     = *
+     * WordUtils.uncapitalize("I AM.FINE", {'.'}) = "i AM.fINE"
+     * </pre>
+     * 
+     * @param str  the String to uncapitalize, may be null
+     * @param delimiters  set of characters to determine uncapitalization, null means whitespace
+     * @return uncapitalized String, <code>null</code> if null String input
+     * @see #capitalize(String)
+     * @since 2.1
+     */
+    public static String uncapitalize(final String str, final char... delimiters) {
+        final int delimLen = delimiters == null ? -1 : delimiters.length;
+        if (str == null || str.length() == 0 || delimLen == 0) {
+            return str;
+        }
+        final char[] buffer = str.toCharArray();
+        boolean uncapitalizeNext = true;
+        for (int i = 0; i < buffer.length; i++) {
+            final char ch = buffer[i];
+            if (isDelimiter(ch, delimiters)) {
+                uncapitalizeNext = true;
+            } else if (uncapitalizeNext) {
+                buffer[i] = Character.toLowerCase(ch);
+                uncapitalizeNext = false;
+            }
+        }
+        return new String(buffer);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Swaps the case of a String using a word based algorithm.</p>
+     * 
+     * <ul>
+     *  <li>Upper case character converts to Lower case</li>
+     *  <li>Title case character converts to Lower case</li>
+     *  <li>Lower case character after Whitespace or at start converts to Title case</li>
+     *  <li>Other Lower case character converts to Upper case</li>
+     * </ul>
+     * 
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.</p>
+     * 
+     * <pre>
+     * StringUtils.swapCase(null)                 = null
+     * StringUtils.swapCase("")                   = ""
+     * StringUtils.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
+     * </pre>
+     * 
+     * @param str  the String to swap case, may be null
+     * @return the changed String, <code>null</code> if null String input
+     */
+    public static String swapCase(final String str) {
+        if (str == null || str.length() == 0) {
+            return str;
+        }
+        final char[] buffer = str.toCharArray();
+
+        boolean whitespace = true;
+
+        for (int i = 0; i < buffer.length; i++) {
+            final char ch = buffer[i];
+            if (Character.isUpperCase(ch)) {
+                buffer[i] = Character.toLowerCase(ch);
+                whitespace = false;
+            } else if (Character.isTitleCase(ch)) {
+                buffer[i] = Character.toLowerCase(ch);
+                whitespace = false;
+            } else if (Character.isLowerCase(ch)) {
+                if (whitespace) {
+                    buffer[i] = Character.toTitleCase(ch);
+                    whitespace = false;
+                } else {
+                    buffer[i] = Character.toUpperCase(ch);
+                }
+            } else {
+                whitespace = Character.isWhitespace(ch);
+            }
+        }
+        return new String(buffer);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Extracts the initial characters from each word in the String.</p>
+     * 
+     * <p>All first characters after whitespace are returned as a new string.
+     * Their case is not changed.</p>
+     *
+     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.</p>
+     *
+     * <pre>
+     * WordUtils.initials(null)             = null
+     * WordUtils.initials("")               = ""
+     * WordUtils.initials("Ben John Lee")   = "BJL"
+     * WordUtils.initials("Ben J.Lee")      = "BJ"
+     * </pre>
+     *
+     * @param str  the String to get initials from, may be null
+     * @return String of initial letters, <code>null</code> if null String input
+     * @see #initials(String,char[])
+     * @since 2.2
+     */
+    public static String initials(final String str) {
+        return initials(str, null);
+    }
+
+    /**
+     * <p>Extracts the initial characters from each word in the String.</p>
+     * 
+     * <p>All first characters after the defined delimiters are returned as a new string.
+     * Their case is not changed.</p>
+     *
+     * <p>If the delimiters array is null, then Whitespace is used.
+     * Whitespace is defined by {@link Character#isWhitespace(char)}.
+     * A <code>null</code> input String returns <code>null</code>.
+     * An empty delimiter array returns an empty String.</p>
+     *
+     * <pre>
+     * WordUtils.initials(null, *)                = null
+     * WordUtils.initials("", *)                  = ""
+     * WordUtils.initials("Ben John Lee", null)   = "BJL"
+     * WordUtils.initials("Ben J.Lee", null)      = "BJ"
+     * WordUtils.initials("Ben J.Lee", [' ','.']) = "BJL"
+     * WordUtils.initials(*, new char[0])         = ""
+     * </pre>
+     * 
+     * @param str  the String to get initials from, may be null
+     * @param delimiters  set of characters to determine words, null means whitespace
+     * @return String of initial characters, <code>null</code> if null String input
+     * @see #initials(String)
+     * @since 2.2
+     */
+    public static String initials(final String str, final char... delimiters) {
+        if (str == null || str.length() == 0) {
+            return str;
+        }
+        if (delimiters != null && delimiters.length == 0) {
+            return "";
+        }
+        final int strLen = str.length();
+        final char[] buf = new char[strLen / 2 + 1];
+        int count = 0;
+        boolean lastWasGap = true;
+        for (int i = 0; i < strLen; i++) {
+            final char ch = str.charAt(i);
+
+            if (isDelimiter(ch, delimiters)) {
+                lastWasGap = true;
+            } else if (lastWasGap) {
+                buf[count++] = ch;
+                lastWasGap = false;
+            } else {
+                continue; // ignore ch
+            }
+        }
+        return new String(buf, 0, count);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * <p>Checks if the String contains all words in the given array.</p>
+     *
+     * <p>
+     * A {@code null} String will return {@code false}. A {@code null}, zero
+     * length search array or if one element of array is null will return {@code false}.
+     * </p>
+     *
+     * <pre>
+     * WordUtils.containsAllWords(null, *)            = false
+     * WordUtils.containsAllWords("", *)              = false
+     * WordUtils.containsAllWords(*, null)            = false
+     * WordUtils.containsAllWords(*, [])              = false
+     * WordUtils.containsAllWords("abcd", "ab", "cd") = false
+     * WordUtils.containsAllWords("abc def", "def", "abc") = true
+     * </pre>
+     *
+     *
+     * @param word The CharSequence to check, may be null
+     * @param words The array of String words to search for, may be null
+     * @return {@code true} if all search words are found, {@code false} otherwise
+     * @since 3.5
+     */
+    public static boolean containsAllWords(final CharSequence word, final CharSequence... words) {
+        if (word == null || word.length() == 0 || words == null || Array.getLength(words) == 0) {
+            return false;
+        }
+        for (final CharSequence w : words) {
+            if (w == null || w.length() == 0 || String.valueOf(w).trim().length() == 0 ) {
+                return false;
+            }
+            final Pattern p = Pattern.compile(".*\\b" + w + "\\b.*");
+            if (!p.matcher(word).matches()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Is the character a delimiter.
+     *
+     * @param ch  the character to check
+     * @param delimiters  the delimiters
+     * @return true if it is a delimiter
+     */
+    private static boolean isDelimiter(final char ch, final char[] delimiters) {
+        if (delimiters == null) {
+            return Character.isWhitespace(ch);
+        }
+        for (final char delimiter : delimiters) {
+            if (ch == delimiter) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java b/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java
index d8b50e8..d209b3c 100644
--- a/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java
+++ b/src/main/java/org/apache/commons/text/translate/AggregateTranslator.java
@@ -22,8 +22,6 @@ import java.io.Writer;
 /**
  * Executes a sequence of translators one after the other. Execution ends whenever 
  * the first translator consumes codepoints from the input.
- * 
- * @since 3.0
  */
 public class AggregateTranslator extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java b/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java
index 16ec02f..0fed939 100644
--- a/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java
+++ b/src/main/java/org/apache/commons/text/translate/CharSequenceTranslator.java
@@ -25,8 +25,6 @@ import java.util.Locale;
  * An API for translating text. 
  * Its core use is to escape and unescape text. Because escaping and unescaping 
  * is completely contextual, the API does not present two separate signatures.
- * 
- * @since 3.0
  */
 public abstract class CharSequenceTranslator {
 
@@ -114,8 +112,8 @@ public abstract class CharSequenceTranslator {
      * @param translators CharSequenceTranslator array of translators to merge with this one
      * @return CharSequenceTranslator merging this translator with the others
      */
-    public final org.apache.commons.text.translate.CharSequenceTranslator with(final org.apache.commons.text.translate.CharSequenceTranslator... translators) {
-        final org.apache.commons.text.translate.CharSequenceTranslator[] newArray = new org.apache.commons.text.translate.CharSequenceTranslator[translators.length + 1];
+    public final CharSequenceTranslator with(final CharSequenceTranslator... translators) {
+        final CharSequenceTranslator[] newArray = new CharSequenceTranslator[translators.length + 1];
         newArray[0] = this;
         System.arraycopy(translators, 0, newArray, 1, translators.length);
         return new AggregateTranslator(newArray);

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java b/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java
index cac3d8f..c63165c 100644
--- a/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java
+++ b/src/main/java/org/apache/commons/text/translate/CodePointTranslator.java
@@ -22,8 +22,6 @@ import java.io.Writer;
 /**
  * Helper subclass to CharSequenceTranslator to allow for translations that 
  * will replace up to one character at a time.
- * 
- * @since 3.0
  */
 public abstract class CodePointTranslator extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/EntityArrays.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/EntityArrays.java b/src/main/java/org/apache/commons/text/translate/EntityArrays.java
index 5c7c4e3..99626e6 100644
--- a/src/main/java/org/apache/commons/text/translate/EntityArrays.java
+++ b/src/main/java/org/apache/commons/text/translate/EntityArrays.java
@@ -20,8 +20,6 @@ package org.apache.commons.text.translate;
  * Class holding various entity data for HTML and XML - generally for use with
  * the LookupTranslator.
  * All arrays are of length [*][2].
- *
- * @since 3.0
  */
 public class EntityArrays {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java b/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java
index a9a186e..8c5b2b6 100644
--- a/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java
+++ b/src/main/java/org/apache/commons/text/translate/JavaUnicodeEscaper.java
@@ -18,8 +18,6 @@ package org.apache.commons.text.translate;
 
 /**
  * Translates codepoints to their Unicode escaped value suitable for Java source.
- * 
- * @since 3.2
  */
 public class JavaUnicodeEscaper extends UnicodeEscaper {
 
@@ -32,7 +30,7 @@ public class JavaUnicodeEscaper extends UnicodeEscaper {
      *            above which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.JavaUnicodeEscaper above(final int codepoint) {
+    public static JavaUnicodeEscaper above(final int codepoint) {
         return outsideOf(0, codepoint);
     }
 
@@ -45,7 +43,7 @@ public class JavaUnicodeEscaper extends UnicodeEscaper {
      *            below which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.JavaUnicodeEscaper below(final int codepoint) {
+    public static JavaUnicodeEscaper below(final int codepoint) {
         return outsideOf(codepoint, Integer.MAX_VALUE);
     }
 
@@ -60,8 +58,8 @@ public class JavaUnicodeEscaper extends UnicodeEscaper {
      *            below which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.JavaUnicodeEscaper between(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.JavaUnicodeEscaper(codepointLow, codepointHigh, true);
+    public static JavaUnicodeEscaper between(final int codepointLow, final int codepointHigh) {
+        return new JavaUnicodeEscaper(codepointLow, codepointHigh, true);
     }
 
     /**
@@ -75,8 +73,8 @@ public class JavaUnicodeEscaper extends UnicodeEscaper {
      *            above which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.JavaUnicodeEscaper outsideOf(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.JavaUnicodeEscaper(codepointLow, codepointHigh, false);
+    public static JavaUnicodeEscaper outsideOf(final int codepointLow, final int codepointHigh) {
+        return new JavaUnicodeEscaper(codepointLow, codepointHigh, false);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/LookupTranslator.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/LookupTranslator.java b/src/main/java/org/apache/commons/text/translate/LookupTranslator.java
index 614c86e..f73f312 100644
--- a/src/main/java/org/apache/commons/text/translate/LookupTranslator.java
+++ b/src/main/java/org/apache/commons/text/translate/LookupTranslator.java
@@ -23,8 +23,6 @@ import java.util.HashSet;
 
 /**
  * Translates a value using a lookup table.
- *
- * @since 3.0
  */
 public class LookupTranslator extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java b/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java
index a3bf24b..3e4bbd0 100644
--- a/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java
+++ b/src/main/java/org/apache/commons/text/translate/NumericEntityEscaper.java
@@ -21,8 +21,6 @@ import java.io.Writer;
 
 /**
  * Translates codepoints to their XML numeric entity escaped value.
- *
- * @since 3.0
  */
 public class NumericEntityEscaper extends CodePointTranslator {
 
@@ -59,7 +57,7 @@ public class NumericEntityEscaper extends CodePointTranslator {
      * @param codepoint below which to escape
      * @return the newly created {@code NumericEntityEscaper} instance
      */
-    public static org.apache.commons.text.translate.NumericEntityEscaper below(final int codepoint) {
+    public static NumericEntityEscaper below(final int codepoint) {
         return outsideOf(codepoint, Integer.MAX_VALUE);
     }
 
@@ -69,7 +67,7 @@ public class NumericEntityEscaper extends CodePointTranslator {
      * @param codepoint above which to escape
      * @return the newly created {@code NumericEntityEscaper} instance
      */
-    public static org.apache.commons.text.translate.NumericEntityEscaper above(final int codepoint) {
+    public static NumericEntityEscaper above(final int codepoint) {
         return outsideOf(0, codepoint);
     }
 
@@ -80,8 +78,8 @@ public class NumericEntityEscaper extends CodePointTranslator {
      * @param codepointHigh below which to escape
      * @return the newly created {@code NumericEntityEscaper} instance
      */
-    public static org.apache.commons.text.translate.NumericEntityEscaper between(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.NumericEntityEscaper(codepointLow, codepointHigh, true);
+    public static NumericEntityEscaper between(final int codepointLow, final int codepointHigh) {
+        return new NumericEntityEscaper(codepointLow, codepointHigh, true);
     }
 
     /**
@@ -91,8 +89,8 @@ public class NumericEntityEscaper extends CodePointTranslator {
      * @param codepointHigh above which to escape
      * @return the newly created {@code NumericEntityEscaper} instance
      */
-    public static org.apache.commons.text.translate.NumericEntityEscaper outsideOf(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.NumericEntityEscaper(codepointLow, codepointHigh, false);
+    public static NumericEntityEscaper outsideOf(final int codepointLow, final int codepointHigh) {
+        return new NumericEntityEscaper(codepointLow, codepointHigh, false);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java b/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java
index cd4605d..6e2016e 100644
--- a/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java
+++ b/src/main/java/org/apache/commons/text/translate/NumericEntityUnescaper.java
@@ -26,8 +26,6 @@ import java.util.EnumSet;
  * the specific codepoint.
  *
  * Note that the semi-colon is optional.
- * 
- * @since 3.0
  */
 public class NumericEntityUnescaper extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java b/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java
index 5801348..f9c0c14 100644
--- a/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java
+++ b/src/main/java/org/apache/commons/text/translate/OctalUnescaper.java
@@ -26,8 +26,6 @@ import java.io.Writer;
  *
  * Note that this currently only supports the viable range of octal for Java; namely 
  * 1 to 377. This is because parsing Java is the main use case.
- * 
- * @since 3.0
  */
 public class OctalUnescaper extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java b/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java
index 398a0e9..0c90f6d 100644
--- a/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java
+++ b/src/main/java/org/apache/commons/text/translate/UnicodeEscaper.java
@@ -21,8 +21,6 @@ import java.io.Writer;
 
 /**
  * Translates codepoints to their Unicode escaped value.
- *
- * @since 3.0
  */
 public class UnicodeEscaper extends CodePointTranslator {
 
@@ -59,7 +57,7 @@ public class UnicodeEscaper extends CodePointTranslator {
      * @param codepoint below which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.UnicodeEscaper below(final int codepoint) {
+    public static UnicodeEscaper below(final int codepoint) {
         return outsideOf(codepoint, Integer.MAX_VALUE);
     }
 
@@ -69,7 +67,7 @@ public class UnicodeEscaper extends CodePointTranslator {
      * @param codepoint above which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.UnicodeEscaper above(final int codepoint) {
+    public static UnicodeEscaper above(final int codepoint) {
         return outsideOf(0, codepoint);
     }
 
@@ -80,8 +78,8 @@ public class UnicodeEscaper extends CodePointTranslator {
      * @param codepointHigh above which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.UnicodeEscaper outsideOf(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.UnicodeEscaper(codepointLow, codepointHigh, false);
+    public static UnicodeEscaper outsideOf(final int codepointLow, final int codepointHigh) {
+        return new UnicodeEscaper(codepointLow, codepointHigh, false);
     }
 
     /**
@@ -91,8 +89,8 @@ public class UnicodeEscaper extends CodePointTranslator {
      * @param codepointHigh below which to escape
      * @return the newly created {@code UnicodeEscaper} instance
      */
-    public static org.apache.commons.text.translate.UnicodeEscaper between(final int codepointLow, final int codepointHigh) {
-        return new org.apache.commons.text.translate.UnicodeEscaper(codepointLow, codepointHigh, true);
+    public static UnicodeEscaper between(final int codepointLow, final int codepointHigh) {
+        return new UnicodeEscaper(codepointLow, codepointHigh, true);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java b/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java
index b92f421..e8fda68 100644
--- a/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java
+++ b/src/main/java/org/apache/commons/text/translate/UnicodeUnescaper.java
@@ -23,8 +23,6 @@ import java.io.Writer;
  * Translates escaped Unicode values of the form \\u+\d\d\d\d back to 
  * Unicode. It supports multiple 'u' characters and will work with or 
  * without the +.
- * 
- * @since 3.0
  */
 public class UnicodeUnescaper extends CharSequenceTranslator {
 

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/translate/package-info.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/translate/package-info.java b/src/main/java/org/apache/commons/text/translate/package-info.java
index 722d7c9..3ef4d48 100644
--- a/src/main/java/org/apache/commons/text/translate/package-info.java
+++ b/src/main/java/org/apache/commons/text/translate/package-info.java
@@ -18,7 +18,5 @@
  * <p> An API for creating text translation routines from a set of smaller building blocks. Initially created to make it
  * possible for the user to customize the rules in the StringEscapeUtils class.</p>
  * <p>These classes are immutable, and therefore thread-safe.</p>
- *
- * @since 3.0
  */
 package org.apache.commons.text.translate;

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/CompositeFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/CompositeFormatTest.java b/src/test/java/org/apache/commons/text/CompositeFormatTest.java
new file mode 100644
index 0000000..e9b8064
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/CompositeFormatTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParsePosition;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+
+/**
+ * Unit tests for {@link org.apache.commons.text.CompositeFormat}.
+ */
+public class CompositeFormatTest {
+
+    /**
+     * Ensures that the parse/format separation is correctly maintained. 
+     */
+    @Test
+    public void testCompositeFormat() {
+
+        final Format parser = new Format() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
+                throw new UnsupportedOperationException("Not implemented");
+            }
+
+            @Override
+            public Object parseObject(final String source, final ParsePosition pos) {
+                return null;    // do nothing
+            }
+        };
+
+        final Format formatter = new Format() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
+                return null;    // do nothing
+            }
+
+            @Override
+            public Object parseObject(final String source, final ParsePosition pos) {
+                throw new UnsupportedOperationException("Not implemented");
+            }
+        };
+
+        final CompositeFormat composite = new CompositeFormat(parser, formatter);
+
+        composite.parseObject("", null);
+        composite.format(new Object(), new StringBuffer(), null);
+        assertEquals( "Parser get method incorrectly implemented", parser, composite.getParser() );
+        assertEquals( "Formatter get method incorrectly implemented", formatter, composite.getFormatter() );
+    }
+
+    @Test
+    public void testUsage() throws Exception {
+        final Format f1 = new SimpleDateFormat("MMddyyyy", Locale.ENGLISH);
+        final Format f2 = new SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH);
+        final CompositeFormat c = new CompositeFormat(f1, f2);
+        final String testString = "January 3, 2005";
+        assertEquals(testString, c.format(c.parseObject("01032005")));
+        assertEquals(testString, c.reformat("01032005"));
+    }
+
+}


[05/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrBuilderTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrBuilderTest.java b/src/test/java/org/apache/commons/text/StrBuilderTest.java
new file mode 100644
index 0000000..1920984
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrBuilderTest.java
@@ -0,0 +1,2009 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.nio.CharBuffer;
+import java.util.Arrays;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * Unit tests for {@link StrBuilder}.
+ */
+public class StrBuilderTest {
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructors() {
+        final StrBuilder sb0 = new StrBuilder();
+        assertEquals(32, sb0.capacity());
+        assertEquals(0, sb0.length());
+        assertEquals(0, sb0.size());
+
+        final StrBuilder sb1 = new StrBuilder(32);
+        assertEquals(32, sb1.capacity());
+        assertEquals(0, sb1.length());
+        assertEquals(0, sb1.size());
+
+        final StrBuilder sb2 = new StrBuilder(0);
+        assertEquals(32, sb2.capacity());
+        assertEquals(0, sb2.length());
+        assertEquals(0, sb2.size());
+
+        final StrBuilder sb3 = new StrBuilder(-1);
+        assertEquals(32, sb3.capacity());
+        assertEquals(0, sb3.length());
+        assertEquals(0, sb3.size());
+
+        final StrBuilder sb4 = new StrBuilder(1);
+        assertEquals(1, sb4.capacity());
+        assertEquals(0, sb4.length());
+        assertEquals(0, sb4.size());
+
+        final StrBuilder sb5 = new StrBuilder((String) null);
+        assertEquals(32, sb5.capacity());
+        assertEquals(0, sb5.length());
+        assertEquals(0, sb5.size());
+
+        final StrBuilder sb6 = new StrBuilder("");
+        assertEquals(32, sb6.capacity());
+        assertEquals(0, sb6.length());
+        assertEquals(0, sb6.size());
+
+        final StrBuilder sb7 = new StrBuilder("foo");
+        assertEquals(35, sb7.capacity());
+        assertEquals(3, sb7.length());
+        assertEquals(3, sb7.size());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testChaining() {
+        final StrBuilder sb = new StrBuilder();
+        assertSame(sb, sb.setNewLineText(null));
+        assertSame(sb, sb.setNullText(null));
+        assertSame(sb, sb.setLength(1));
+        assertSame(sb, sb.setCharAt(0, 'a'));
+        assertSame(sb, sb.ensureCapacity(0));
+        assertSame(sb, sb.minimizeCapacity());
+        assertSame(sb, sb.clear());
+        assertSame(sb, sb.reverse());
+        assertSame(sb, sb.trim());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReadFromReader() throws Exception {
+        String s = "";
+        for (int i = 0; i < 100; ++i) {
+            final StrBuilder sb = new StrBuilder();
+            final int len = sb.readFrom(new StringReader(s));
+
+            assertEquals(s.length(), len);
+            assertEquals(s, sb.toString());
+
+            s += Integer.toString(i);
+        }
+    }
+
+    @Test
+    public void testReadFromReaderAppendsToEnd() throws Exception {
+        final StrBuilder sb = new StrBuilder("Test");
+        sb.readFrom(new StringReader(" 123"));
+        assertEquals("Test 123", sb.toString());
+    }
+
+    @Test
+    public void testReadFromCharBuffer() throws Exception {
+        String s = "";
+        for (int i = 0; i < 100; ++i) {
+            final StrBuilder sb = new StrBuilder();
+            final int len = sb.readFrom(CharBuffer.wrap(s));
+
+            assertEquals(s.length(), len);
+            assertEquals(s, sb.toString());
+
+            s += Integer.toString(i);
+        }
+    }
+
+    @Test
+    public void testReadFromCharBufferAppendsToEnd() throws Exception {
+        final StrBuilder sb = new StrBuilder("Test");
+        sb.readFrom(CharBuffer.wrap(" 123"));
+        assertEquals("Test 123", sb.toString());
+    }
+
+    @Test
+    public void testReadFromReadable() throws Exception {
+        String s = "";
+        for (int i = 0; i < 100; ++i) {
+            final StrBuilder sb = new StrBuilder();
+            final int len = sb.readFrom(new MockReadable(s));
+
+            assertEquals(s.length(), len);
+            assertEquals(s, sb.toString());
+
+            s += Integer.toString(i);
+        }
+    }
+
+    @Test
+    public void testReadFromReadableAppendsToEnd() throws Exception {
+        final StrBuilder sb = new StrBuilder("Test");
+        sb.readFrom(new MockReadable(" 123"));
+        assertEquals("Test 123", sb.toString());
+    }
+
+    private static class MockReadable implements Readable {
+
+        private final CharBuffer src;
+
+        public MockReadable(final String src) {
+            this.src = CharBuffer.wrap(src);
+        }
+
+        @Override
+        public int read(final CharBuffer cb) throws IOException {
+            return src.read(cb);
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testGetSetNewLineText() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(null, sb.getNewLineText());
+
+        sb.setNewLineText("#");
+        assertEquals("#", sb.getNewLineText());
+
+        sb.setNewLineText("");
+        assertEquals("", sb.getNewLineText());
+
+        sb.setNewLineText((String) null);
+        assertEquals(null, sb.getNewLineText());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testGetSetNullText() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(null, sb.getNullText());
+
+        sb.setNullText("null");
+        assertEquals("null", sb.getNullText());
+
+        sb.setNullText("");
+        assertEquals(null, sb.getNullText());
+
+        sb.setNullText("NULL");
+        assertEquals("NULL", sb.getNullText());
+
+        sb.setNullText((String) null);
+        assertEquals(null, sb.getNullText());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCapacityAndLength() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(32, sb.capacity());
+        assertEquals(0, sb.length());
+        assertEquals(0, sb.size());
+        assertTrue(sb.isEmpty());
+
+        sb.minimizeCapacity();
+        assertEquals(0, sb.capacity());
+        assertEquals(0, sb.length());
+        assertEquals(0, sb.size());
+        assertTrue(sb.isEmpty());
+
+        sb.ensureCapacity(32);
+        assertTrue(sb.capacity() >= 32);
+        assertEquals(0, sb.length());
+        assertEquals(0, sb.size());
+        assertTrue(sb.isEmpty());
+
+        sb.append("foo");
+        assertTrue(sb.capacity() >= 32);
+        assertEquals(3, sb.length());
+        assertEquals(3, sb.size());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.clear();
+        assertTrue(sb.capacity() >= 32);
+        assertEquals(0, sb.length());
+        assertEquals(0, sb.size());
+        assertTrue(sb.isEmpty());
+
+        sb.append("123456789012345678901234567890123");
+        assertTrue(sb.capacity() > 32);
+        assertEquals(33, sb.length());
+        assertEquals(33, sb.size());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.ensureCapacity(16);
+        assertTrue(sb.capacity() > 16);
+        assertEquals(33, sb.length());
+        assertEquals(33, sb.size());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.minimizeCapacity();
+        assertEquals(33, sb.capacity());
+        assertEquals(33, sb.length());
+        assertEquals(33, sb.size());
+        assertTrue(sb.isEmpty() == false);
+
+        try {
+            sb.setLength(-1);
+            fail("setLength(-1) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.setLength(33);
+        assertEquals(33, sb.capacity());
+        assertEquals(33, sb.length());
+        assertEquals(33, sb.size());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.setLength(16);
+        assertTrue(sb.capacity() >= 16);
+        assertEquals(16, sb.length());
+        assertEquals(16, sb.size());
+        assertEquals("1234567890123456", sb.toString());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.setLength(32);
+        assertTrue(sb.capacity() >= 32);
+        assertEquals(32, sb.length());
+        assertEquals(32, sb.size());
+        assertEquals("1234567890123456\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", sb.toString());
+        assertTrue(sb.isEmpty() == false);
+
+        sb.setLength(0);
+        assertTrue(sb.capacity() >= 32);
+        assertEquals(0, sb.length());
+        assertEquals(0, sb.size());
+        assertTrue(sb.isEmpty());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testLength() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(0, sb.length());
+        
+        sb.append("Hello");
+        assertEquals(5, sb.length());
+    }
+
+    @Test
+    public void testSetLength() {
+        final StrBuilder sb = new StrBuilder();
+        sb.append("Hello");
+        sb.setLength(2);  // shorten
+        assertEquals("He", sb.toString());
+        sb.setLength(2);  // no change
+        assertEquals("He", sb.toString());
+        sb.setLength(3);  // lengthen
+        assertEquals("He\0", sb.toString());
+
+        try {
+            sb.setLength(-1);
+            fail("setLength(-1) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCapacity() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(sb.buffer.length, sb.capacity());
+        
+        sb.append("HelloWorldHelloWorldHelloWorldHelloWorld");
+        assertEquals(sb.buffer.length, sb.capacity());
+    }
+
+    @Test
+    public void testEnsureCapacity() {
+        final StrBuilder sb = new StrBuilder();
+        sb.ensureCapacity(2);
+        assertTrue(sb.capacity() >= 2);
+        
+        sb.ensureCapacity(-1);
+        assertTrue(sb.capacity() >= 0);
+        
+        sb.append("HelloWorld");
+        sb.ensureCapacity(40);
+        assertTrue(sb.capacity() >= 40);
+    }
+
+    @Test
+    public void testMinimizeCapacity() {
+        final StrBuilder sb = new StrBuilder();
+        sb.minimizeCapacity();
+        assertEquals(0, sb.capacity());
+        
+        sb.append("HelloWorld");
+        sb.minimizeCapacity();
+        assertEquals(10, sb.capacity());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSize() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(0, sb.size());
+        
+        sb.append("Hello");
+        assertEquals(5, sb.size());
+    }
+
+    @Test
+    public void testIsEmpty() {
+        final StrBuilder sb = new StrBuilder();
+        assertTrue(sb.isEmpty());
+        
+        sb.append("Hello");
+        assertFalse(sb.isEmpty());
+        
+        sb.clear();
+        assertTrue(sb.isEmpty());
+    }
+
+    @Test
+    public void testClear() {
+        final StrBuilder sb = new StrBuilder();
+        sb.append("Hello");
+        sb.clear();
+        assertEquals(0, sb.length());
+        assertTrue(sb.buffer.length >= 5);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCharAt() {
+        final StrBuilder sb = new StrBuilder();
+        try {
+            sb.charAt(0);
+            fail("charAt(0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        try {
+            sb.charAt(-1);
+            fail("charAt(-1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        sb.append("foo");
+        assertEquals('f', sb.charAt(0));
+        assertEquals('o', sb.charAt(1));
+        assertEquals('o', sb.charAt(2));
+        try {
+            sb.charAt(-1);
+            fail("charAt(-1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        try {
+            sb.charAt(3);
+            fail("charAt(3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSetCharAt() {
+        final StrBuilder sb = new StrBuilder();
+        try {
+            sb.setCharAt(0, 'f');
+            fail("setCharAt(0,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        try {
+            sb.setCharAt(-1, 'f');
+            fail("setCharAt(-1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        sb.append("foo");
+        sb.setCharAt(0, 'b');
+        sb.setCharAt(1, 'a');
+        sb.setCharAt(2, 'r');
+        try {
+            sb.setCharAt(3, '!');
+            fail("setCharAt(3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+        assertEquals("bar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testDeleteCharAt() {
+        final StrBuilder sb = new StrBuilder("abc");
+        sb.deleteCharAt(0);
+        assertEquals("bc", sb.toString()); 
+        
+        try {
+            sb.deleteCharAt(1000);
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testToCharArray() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY, sb.toCharArray());
+
+        char[] a = sb.toCharArray();
+        assertNotNull("toCharArray() result is null", a);
+        assertEquals("toCharArray() result is too large", 0, a.length);
+
+        sb.append("junit");
+        a = sb.toCharArray();
+        assertEquals("toCharArray() result incorrect length", 5, a.length);
+        assertTrue("toCharArray() result does not match", Arrays.equals("junit".toCharArray(), a));
+    }
+
+    @Test
+    public void testToCharArrayIntInt() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(ArrayUtils.EMPTY_CHAR_ARRAY, sb.toCharArray(0, 0));
+
+        sb.append("junit");
+        char[] a = sb.toCharArray(0, 20); // too large test
+        assertEquals("toCharArray(int,int) result incorrect length", 5, a.length);
+        assertTrue("toCharArray(int,int) result does not match", Arrays.equals("junit".toCharArray(), a));
+
+        a = sb.toCharArray(0, 4);
+        assertEquals("toCharArray(int,int) result incorrect length", 4, a.length);
+        assertTrue("toCharArray(int,int) result does not match", Arrays.equals("juni".toCharArray(), a));
+
+        a = sb.toCharArray(0, 4);
+        assertEquals("toCharArray(int,int) result incorrect length", 4, a.length);
+        assertTrue("toCharArray(int,int) result does not match", Arrays.equals("juni".toCharArray(), a));
+
+        a = sb.toCharArray(0, 1);
+        assertNotNull("toCharArray(int,int) result is null", a);
+
+        try {
+            sb.toCharArray(-1, 5);
+            fail("no string index out of bound on -1");
+        } catch (final IndexOutOfBoundsException e) {
+        }
+
+        try {
+            sb.toCharArray(6, 5);
+            fail("no string index out of bound on -1");
+        } catch (final IndexOutOfBoundsException e) {
+        }
+    }
+
+    @Test
+    public void testGetChars ( ) {
+        final StrBuilder sb = new StrBuilder();
+        
+        char[] input = new char[10];
+        char[] a = sb.getChars(input);
+        assertSame (input, a);
+        assertTrue(Arrays.equals(new char[10], a));
+        
+        sb.append("junit");
+        a = sb.getChars(input);
+        assertSame(input, a);
+        assertTrue(Arrays.equals(new char[] {'j','u','n','i','t',0,0,0,0,0},a));
+        
+        a = sb.getChars(null);
+        assertNotSame(input,a);
+        assertEquals(5,a.length);
+        assertTrue(Arrays.equals("junit".toCharArray(),a));
+        
+        input = new char[5];
+        a = sb.getChars(input);
+        assertSame(input, a);
+        
+        input = new char[4];
+        a = sb.getChars(input);
+        assertNotSame(input, a);
+    }
+
+    @Test
+    public void testGetCharsIntIntCharArrayInt( ) {
+        final StrBuilder sb = new StrBuilder();
+               
+        sb.append("junit");
+        char[] a = new char[5];
+        sb.getChars(0,5,a,0);
+        assertTrue(Arrays.equals(new char[] {'j','u','n','i','t'},a));
+        
+        a = new char[5];
+        sb.getChars(0,2,a,3);
+        assertTrue(Arrays.equals(new char[] {0,0,0,'j','u'},a));
+        
+        try {
+            sb.getChars(-1,0,a,0);
+            fail("no exception");
+        }
+        catch (final IndexOutOfBoundsException e) {
+        }
+        
+        try {
+            sb.getChars(0,-1,a,0);
+            fail("no exception");
+        }
+        catch (final IndexOutOfBoundsException e) {
+        }
+        
+        try {
+            sb.getChars(0,20,a,0);
+            fail("no exception");
+        }
+        catch (final IndexOutOfBoundsException e) {
+        }
+        
+        try {
+            sb.getChars(4,2,a,0);
+            fail("no exception");
+        }
+        catch (final IndexOutOfBoundsException e) {
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testDeleteIntInt() {
+        StrBuilder sb = new StrBuilder("abc");
+        sb.delete(0, 1);
+        assertEquals("bc", sb.toString()); 
+        sb.delete(1, 2);
+        assertEquals("b", sb.toString());
+        sb.delete(0, 1);
+        assertEquals("", sb.toString()); 
+        sb.delete(0, 1000);
+        assertEquals("", sb.toString()); 
+        
+        try {
+            sb.delete(1, 2);
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+        try {
+            sb.delete(-1, 1);
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        sb = new StrBuilder("anything");
+        try {
+            sb.delete(2, 1);
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testDeleteAll_char() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.deleteAll('X');
+        assertEquals("abcbccba", sb.toString());
+        sb.deleteAll('a');
+        assertEquals("bcbccb", sb.toString());
+        sb.deleteAll('c');
+        assertEquals("bbb", sb.toString());
+        sb.deleteAll('b');
+        assertEquals("", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteAll('b');
+        assertEquals("", sb.toString());
+    }
+
+    @Test
+    public void testDeleteFirst_char() {
+        StrBuilder sb = new StrBuilder("abcba");
+        sb.deleteFirst('X');
+        assertEquals("abcba", sb.toString());
+        sb.deleteFirst('a');
+        assertEquals("bcba", sb.toString());
+        sb.deleteFirst('c');
+        assertEquals("bba", sb.toString());
+        sb.deleteFirst('b');
+        assertEquals("ba", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteFirst('b');
+        assertEquals("", sb.toString());
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testDeleteAll_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.deleteAll((String) null);
+        assertEquals("abcbccba", sb.toString());
+        sb.deleteAll("");
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.deleteAll("X");
+        assertEquals("abcbccba", sb.toString());
+        sb.deleteAll("a");
+        assertEquals("bcbccb", sb.toString());
+        sb.deleteAll("c");
+        assertEquals("bbb", sb.toString());
+        sb.deleteAll("b");
+        assertEquals("", sb.toString());
+
+        sb = new StrBuilder("abcbccba");
+        sb.deleteAll("bc");
+        assertEquals("acba", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteAll("bc");
+        assertEquals("", sb.toString());
+    }
+
+    @Test
+    public void testDeleteFirst_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.deleteFirst((String) null);
+        assertEquals("abcbccba", sb.toString());
+        sb.deleteFirst("");
+        assertEquals("abcbccba", sb.toString());
+
+        sb.deleteFirst("X");
+        assertEquals("abcbccba", sb.toString());
+        sb.deleteFirst("a");
+        assertEquals("bcbccba", sb.toString());
+        sb.deleteFirst("c");
+        assertEquals("bbccba", sb.toString());
+        sb.deleteFirst("b");
+        assertEquals("bccba", sb.toString());
+
+        sb = new StrBuilder("abcbccba");
+        sb.deleteFirst("bc");
+        assertEquals("abccba", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteFirst("bc");
+        assertEquals("", sb.toString());
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testDeleteAll_StrMatcher() {
+        StrBuilder sb = new StrBuilder("A0xA1A2yA3");
+        sb.deleteAll((StrMatcher) null);
+        assertEquals("A0xA1A2yA3", sb.toString());
+        sb.deleteAll(A_NUMBER_MATCHER);
+        assertEquals("xy", sb.toString());
+
+        sb = new StrBuilder("Ax1");
+        sb.deleteAll(A_NUMBER_MATCHER);
+        assertEquals("Ax1", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteAll(A_NUMBER_MATCHER);
+        assertEquals("", sb.toString());
+    }
+
+    @Test
+    public void testDeleteFirst_StrMatcher() {
+        StrBuilder sb = new StrBuilder("A0xA1A2yA3");
+        sb.deleteFirst((StrMatcher) null);
+        assertEquals("A0xA1A2yA3", sb.toString());
+        sb.deleteFirst(A_NUMBER_MATCHER);
+        assertEquals("xA1A2yA3", sb.toString());
+
+        sb = new StrBuilder("Ax1");
+        sb.deleteFirst(A_NUMBER_MATCHER);
+        assertEquals("Ax1", sb.toString());
+
+        sb = new StrBuilder("");
+        sb.deleteFirst(A_NUMBER_MATCHER);
+        assertEquals("", sb.toString());
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testReplace_int_int_String() {
+        StrBuilder sb = new StrBuilder("abc");
+        sb.replace(0, 1, "d");
+        assertEquals("dbc", sb.toString());
+        sb.replace(0, 1, "aaa");
+        assertEquals("aaabc", sb.toString());
+        sb.replace(0, 3, "");
+        assertEquals("bc", sb.toString());
+        sb.replace(1, 2, (String) null);
+        assertEquals("b", sb.toString());
+        sb.replace(1, 1000, "text");
+        assertEquals("btext", sb.toString());
+        sb.replace(0, 1000, "text");
+        assertEquals("text", sb.toString());
+        
+        sb = new StrBuilder("atext");
+        sb.replace(1, 1, "ny");
+        assertEquals("anytext", sb.toString());
+        try {
+            sb.replace(2, 1, "anything");
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        sb = new StrBuilder();
+        try {
+            sb.replace(1, 2, "anything");
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+        try {
+            sb.replace(-1, 1, "anything");
+            fail("Expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {}
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReplaceAll_char_char() {
+        final StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceAll('x', 'y');
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll('a', 'd');
+        assertEquals("dbcbccbd", sb.toString());
+        sb.replaceAll('b', 'e');
+        assertEquals("dececced", sb.toString());
+        sb.replaceAll('c', 'f');
+        assertEquals("defeffed", sb.toString());
+        sb.replaceAll('d', 'd');
+        assertEquals("defeffed", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReplaceFirst_char_char() {
+        final StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceFirst('x', 'y');
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst('a', 'd');
+        assertEquals("dbcbccba", sb.toString());
+        sb.replaceFirst('b', 'e');
+        assertEquals("decbccba", sb.toString());
+        sb.replaceFirst('c', 'f');
+        assertEquals("defbccba", sb.toString());
+        sb.replaceFirst('d', 'd');
+        assertEquals("defbccba", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReplaceAll_String_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceAll((String) null, null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll((String) null, "anything");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll("", null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll("", "anything");
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.replaceAll("x", "y");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll("a", "d");
+        assertEquals("dbcbccbd", sb.toString());
+        sb.replaceAll("d", null);
+        assertEquals("bcbccb", sb.toString());
+        sb.replaceAll("cb", "-");
+        assertEquals("b-c-", sb.toString());
+        
+        sb = new StrBuilder("abcba");
+        sb.replaceAll("b", "xbx");
+        assertEquals("axbxcxbxa", sb.toString());
+        
+        sb = new StrBuilder("bb");
+        sb.replaceAll("b", "xbx");
+        assertEquals("xbxxbx", sb.toString());
+    }
+
+    @Test
+    public void testReplaceFirst_String_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceFirst((String) null, null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst((String) null, "anything");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst("", null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst("", "anything");
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.replaceFirst("x", "y");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst("a", "d");
+        assertEquals("dbcbccba", sb.toString());
+        sb.replaceFirst("d", null);
+        assertEquals("bcbccba", sb.toString());
+        sb.replaceFirst("cb", "-");
+        assertEquals("b-ccba", sb.toString());
+        
+        sb = new StrBuilder("abcba");
+        sb.replaceFirst("b", "xbx");
+        assertEquals("axbxcba", sb.toString());
+        
+        sb = new StrBuilder("bb");
+        sb.replaceFirst("b", "xbx");
+        assertEquals("xbxb", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReplaceAll_StrMatcher_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceAll((StrMatcher) null, null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll((StrMatcher) null, "anything");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll(StrMatcher.noneMatcher(), null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll(StrMatcher.noneMatcher(), "anything");
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.replaceAll(StrMatcher.charMatcher('x'), "y");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceAll(StrMatcher.charMatcher('a'), "d");
+        assertEquals("dbcbccbd", sb.toString());
+        sb.replaceAll(StrMatcher.charMatcher('d'), null);
+        assertEquals("bcbccb", sb.toString());
+        sb.replaceAll(StrMatcher.stringMatcher("cb"), "-");
+        assertEquals("b-c-", sb.toString());
+        
+        sb = new StrBuilder("abcba");
+        sb.replaceAll(StrMatcher.charMatcher('b'), "xbx");
+        assertEquals("axbxcxbxa", sb.toString());
+        
+        sb = new StrBuilder("bb");
+        sb.replaceAll(StrMatcher.charMatcher('b'), "xbx");
+        assertEquals("xbxxbx", sb.toString());
+        
+        sb = new StrBuilder("A1-A2A3-A4");
+        sb.replaceAll(A_NUMBER_MATCHER, "***");
+        assertEquals("***-******-***", sb.toString());
+
+        sb = new StrBuilder("Dear X, hello X.");
+        sb.replaceAll(StrMatcher.stringMatcher("X"), "012345678901234567");
+        assertEquals("Dear 012345678901234567, hello 012345678901234567.", sb.toString());
+    }
+
+    @Test
+    public void testReplaceFirst_StrMatcher_String() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replaceFirst((StrMatcher) null, null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst((StrMatcher) null, "anything");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst(StrMatcher.noneMatcher(), null);
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst(StrMatcher.noneMatcher(), "anything");
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.replaceFirst(StrMatcher.charMatcher('x'), "y");
+        assertEquals("abcbccba", sb.toString());
+        sb.replaceFirst(StrMatcher.charMatcher('a'), "d");
+        assertEquals("dbcbccba", sb.toString());
+        sb.replaceFirst(StrMatcher.charMatcher('d'), null);
+        assertEquals("bcbccba", sb.toString());
+        sb.replaceFirst(StrMatcher.stringMatcher("cb"), "-");
+        assertEquals("b-ccba", sb.toString());
+        
+        sb = new StrBuilder("abcba");
+        sb.replaceFirst(StrMatcher.charMatcher('b'), "xbx");
+        assertEquals("axbxcba", sb.toString());
+        
+        sb = new StrBuilder("bb");
+        sb.replaceFirst(StrMatcher.charMatcher('b'), "xbx");
+        assertEquals("xbxb", sb.toString());
+        
+        sb = new StrBuilder("A1-A2A3-A4");
+        sb.replaceFirst(A_NUMBER_MATCHER, "***");
+        assertEquals("***-A2A3-A4", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReplace_StrMatcher_String_int_int_int_VaryMatcher() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replace((StrMatcher) null, "x", 0, sb.length(), -1);
+        assertEquals("abcbccba", sb.toString());
+        
+        sb.replace(StrMatcher.charMatcher('a'), "x", 0, sb.length(), -1);
+        assertEquals("xbcbccbx", sb.toString());
+        
+        sb.replace(StrMatcher.stringMatcher("cb"), "x", 0, sb.length(), -1);
+        assertEquals("xbxcxx", sb.toString());
+        
+        sb = new StrBuilder("A1-A2A3-A4");
+        sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1);
+        assertEquals("***-******-***", sb.toString());
+        
+        sb = new StrBuilder();
+        sb.replace(A_NUMBER_MATCHER, "***", 0, sb.length(), -1);
+        assertEquals("", sb.toString());
+    }
+
+    @Test
+    public void testReplace_StrMatcher_String_int_int_int_VaryReplace() {
+        StrBuilder sb = new StrBuilder("abcbccba");
+        sb.replace(StrMatcher.stringMatcher("cb"), "cb", 0, sb.length(), -1);
+        assertEquals("abcbccba", sb.toString());
+        
+        sb = new StrBuilder("abcbccba");
+        sb.replace(StrMatcher.stringMatcher("cb"), "-", 0, sb.length(), -1);
+        assertEquals("ab-c-a", sb.toString());
+        
+        sb = new StrBuilder("abcbccba");
+        sb.replace(StrMatcher.stringMatcher("cb"), "+++", 0, sb.length(), -1);
+        assertEquals("ab+++c+++a", sb.toString());
+        
+        sb = new StrBuilder("abcbccba");
+        sb.replace(StrMatcher.stringMatcher("cb"), "", 0, sb.length(), -1);
+        assertEquals("abca", sb.toString());
+        
+        sb = new StrBuilder("abcbccba");
+        sb.replace(StrMatcher.stringMatcher("cb"), null, 0, sb.length(), -1);
+        assertEquals("abca", sb.toString());
+    }
+
+    @Test
+    public void testReplace_StrMatcher_String_int_int_int_VaryStartIndex() {
+        StrBuilder sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, sb.length(), -1);
+        assertEquals("-x--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 1, sb.length(), -1);
+        assertEquals("aax--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 2, sb.length(), -1);
+        assertEquals("aax--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 3, sb.length(), -1);
+        assertEquals("aax--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 4, sb.length(), -1);
+        assertEquals("aaxa-ay-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 5, sb.length(), -1);
+        assertEquals("aaxaa-y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 6, sb.length(), -1);
+        assertEquals("aaxaaaay-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 7, sb.length(), -1);
+        assertEquals("aaxaaaay-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 8, sb.length(), -1);
+        assertEquals("aaxaaaay-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 9, sb.length(), -1);
+        assertEquals("aaxaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 10, sb.length(), -1);
+        assertEquals("aaxaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        try {
+            sb.replace(StrMatcher.stringMatcher("aa"), "-", 11, sb.length(), -1);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        assertEquals("aaxaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        try {
+            sb.replace(StrMatcher.stringMatcher("aa"), "-", -1, sb.length(), -1);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        assertEquals("aaxaaaayaa", sb.toString());
+    }
+
+    @Test
+    public void testReplace_StrMatcher_String_int_int_int_VaryEndIndex() {
+        StrBuilder sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 0, -1);
+        assertEquals("aaxaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 2, -1);
+        assertEquals("-xaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 3, -1);
+        assertEquals("-xaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 4, -1);
+        assertEquals("-xaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 5, -1);
+        assertEquals("-x-aayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 6, -1);
+        assertEquals("-x-aayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 7, -1);
+        assertEquals("-x--yaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 8, -1);
+        assertEquals("-x--yaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 9, -1);
+        assertEquals("-x--yaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, -1);
+        assertEquals("-x--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 1000, -1);
+        assertEquals("-x--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        try {
+            sb.replace(StrMatcher.stringMatcher("aa"), "-", 2, 1, -1);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        assertEquals("aaxaaaayaa", sb.toString());
+    }
+
+    @Test
+    public void testReplace_StrMatcher_String_int_int_int_VaryCount() {
+        StrBuilder sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, -1);
+        assertEquals("-x--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 0);
+        assertEquals("aaxaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 1);
+        assertEquals("-xaaaayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 2);
+        assertEquals("-x-aayaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 3);
+        assertEquals("-x--yaa", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 4);
+        assertEquals("-x--y-", sb.toString());
+        
+        sb = new StrBuilder("aaxaaaayaa");
+        sb.replace(StrMatcher.stringMatcher("aa"), "-", 0, 10, 5);
+        assertEquals("-x--y-", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReverse() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals("", sb.reverse().toString());
+        
+        sb.clear().append(true);
+        assertEquals("eurt", sb.reverse().toString());
+        assertEquals("true", sb.reverse().toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTrim() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals("", sb.reverse().toString());
+        
+        sb.clear().append(" \u0000 ");
+        assertEquals("", sb.trim().toString());
+        
+        sb.clear().append(" \u0000 a b c");
+        assertEquals("a b c", sb.trim().toString());
+        
+        sb.clear().append("a b c \u0000 ");
+        assertEquals("a b c", sb.trim().toString());
+        
+        sb.clear().append(" \u0000 a b c \u0000 ");
+        assertEquals("a b c", sb.trim().toString());
+        
+        sb.clear().append("a b c");
+        assertEquals("a b c", sb.trim().toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testStartsWith() {
+        final StrBuilder sb = new StrBuilder();
+        assertFalse(sb.startsWith("a"));
+        assertFalse(sb.startsWith(null));
+        assertTrue(sb.startsWith(""));
+        sb.append("abc");
+        assertTrue(sb.startsWith("a"));
+        assertTrue(sb.startsWith("ab"));
+        assertTrue(sb.startsWith("abc"));
+        assertFalse(sb.startsWith("cba"));
+    }
+
+    @Test
+    public void testEndsWith() {
+        final StrBuilder sb = new StrBuilder();
+        assertFalse(sb.endsWith("a"));
+        assertFalse(sb.endsWith("c"));
+        assertTrue(sb.endsWith(""));
+        assertFalse(sb.endsWith(null));
+        sb.append("abc");
+        assertTrue(sb.endsWith("c"));
+        assertTrue(sb.endsWith("bc"));
+        assertTrue(sb.endsWith("abc"));
+        assertFalse(sb.endsWith("cba"));
+        assertFalse(sb.endsWith("abcd"));
+        assertFalse(sb.endsWith(" abc"));
+        assertFalse(sb.endsWith("abc "));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSubSequenceIntInt() {
+       final StrBuilder sb = new StrBuilder ("hello goodbye");
+       // Start index is negative
+       try {
+            sb.subSequence(-1, 5);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        // End index is negative
+       try {
+            sb.subSequence(2, -1);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        // End index greater than length()
+        try {
+            sb.subSequence(2, sb.length() + 1);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        // Start index greater then end index
+        try {
+            sb.subSequence(3, 2);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        // Normal cases
+        assertEquals ("hello", sb.subSequence(0, 5));
+        assertEquals ("hello goodbye".subSequence(0, 6), sb.subSequence(0, 6));
+        assertEquals ("goodbye", sb.subSequence(6, 13));
+        assertEquals ("hello goodbye".subSequence(6,13), sb.subSequence(6, 13));
+    }
+
+    @Test
+    public void testSubstringInt() {
+        final StrBuilder sb = new StrBuilder ("hello goodbye");
+        assertEquals ("goodbye", sb.substring(6));
+        assertEquals ("hello goodbye".substring(6), sb.substring(6));
+        assertEquals ("hello goodbye", sb.substring(0));
+        assertEquals ("hello goodbye".substring(0), sb.substring(0));
+        try {
+            sb.substring(-1);
+            fail ();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        try {
+            sb.substring(15);
+            fail ();
+        } catch (final IndexOutOfBoundsException e) {}
+    
+    }
+    
+    @Test
+    public void testSubstringIntInt() {
+        final StrBuilder sb = new StrBuilder ("hello goodbye");
+        assertEquals ("hello", sb.substring(0, 5));
+        assertEquals ("hello goodbye".substring(0, 6), sb.substring(0, 6));
+        
+        assertEquals ("goodbye", sb.substring(6, 13));
+        assertEquals ("hello goodbye".substring(6,13), sb.substring(6, 13));
+        
+        assertEquals ("goodbye", sb.substring(6, 20));
+        
+        try {
+            sb.substring(-1, 5);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+        
+        try {
+            sb.substring(15, 20);
+            fail();
+        } catch (final IndexOutOfBoundsException e) {}
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testMidString() {
+        final StrBuilder sb = new StrBuilder("hello goodbye hello");
+        assertEquals("goodbye", sb.midString(6, 7));
+        assertEquals("hello", sb.midString(0, 5));
+        assertEquals("hello", sb.midString(-5, 5));
+        assertEquals("", sb.midString(0, -1));
+        assertEquals("", sb.midString(20, 2));
+        assertEquals("hello", sb.midString(14, 22));
+    }
+
+    @Test
+    public void testRightString() {
+        final StrBuilder sb = new StrBuilder("left right");
+        assertEquals("right", sb.rightString(5));
+        assertEquals("", sb.rightString(0));
+        assertEquals("", sb.rightString(-5));
+        assertEquals("left right", sb.rightString(15));
+    }
+
+    @Test
+    public void testLeftString() {
+        final StrBuilder sb = new StrBuilder("left right");
+        assertEquals("left", sb.leftString(4));
+        assertEquals("", sb.leftString(0));
+        assertEquals("", sb.leftString(-5));
+        assertEquals("left right", sb.leftString(15));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testContains_char() {
+        final StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz");
+        assertTrue(sb.contains('a'));
+        assertTrue(sb.contains('o'));
+        assertTrue(sb.contains('z'));
+        assertFalse(sb.contains('1'));
+    }
+
+    @Test
+    public void testContains_String() {
+        final StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz");
+        assertTrue(sb.contains("a"));
+        assertTrue(sb.contains("pq"));
+        assertTrue(sb.contains("z"));
+        assertFalse(sb.contains("zyx"));
+        assertFalse(sb.contains((String) null));
+    }
+
+    @Test
+    public void testContains_StrMatcher() {
+        StrBuilder sb = new StrBuilder("abcdefghijklmnopqrstuvwxyz");
+        assertTrue(sb.contains(StrMatcher.charMatcher('a')));
+        assertTrue(sb.contains(StrMatcher.stringMatcher("pq")));
+        assertTrue(sb.contains(StrMatcher.charMatcher('z')));
+        assertFalse(sb.contains(StrMatcher.stringMatcher("zy")));
+        assertFalse(sb.contains((StrMatcher) null));
+
+        sb = new StrBuilder();
+        assertFalse(sb.contains(A_NUMBER_MATCHER));
+        sb.append("B A1 C");
+        assertTrue(sb.contains(A_NUMBER_MATCHER));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testIndexOf_char() {
+        final StrBuilder sb = new StrBuilder("abab");
+        assertEquals(0, sb.indexOf('a'));
+        
+        // should work like String#indexOf
+        assertEquals("abab".indexOf('a'), sb.indexOf('a'));
+
+        assertEquals(1, sb.indexOf('b'));
+        assertEquals("abab".indexOf('b'), sb.indexOf('b'));
+
+        assertEquals(-1, sb.indexOf('z'));
+    }
+
+    @Test
+    public void testIndexOf_char_int() {
+        StrBuilder sb = new StrBuilder("abab");
+        assertEquals(0, sb.indexOf('a', -1));
+        assertEquals(0, sb.indexOf('a', 0));
+        assertEquals(2, sb.indexOf('a', 1));
+        assertEquals(-1, sb.indexOf('a', 4));
+        assertEquals(-1, sb.indexOf('a', 5));
+
+        // should work like String#indexOf
+        assertEquals("abab".indexOf('a', 1), sb.indexOf('a', 1));
+
+        assertEquals(3, sb.indexOf('b', 2));
+        assertEquals("abab".indexOf('b', 2), sb.indexOf('b', 2));
+
+        assertEquals(-1, sb.indexOf('z', 2));
+
+        sb = new StrBuilder("xyzabc");
+        assertEquals(2, sb.indexOf('z', 0));
+        assertEquals(-1, sb.indexOf('z', 3));
+    }
+
+    @Test
+    public void testLastIndexOf_char() {
+        final StrBuilder sb = new StrBuilder("abab");
+        
+        assertEquals (2, sb.lastIndexOf('a'));
+        //should work like String#lastIndexOf
+        assertEquals ("abab".lastIndexOf('a'), sb.lastIndexOf('a'));
+        
+        assertEquals(3, sb.lastIndexOf('b'));
+        assertEquals ("abab".lastIndexOf('b'), sb.lastIndexOf('b'));
+        
+        assertEquals (-1, sb.lastIndexOf('z'));
+    }
+
+    @Test
+    public void testLastIndexOf_char_int() {
+        StrBuilder sb = new StrBuilder("abab");
+        assertEquals(-1, sb.lastIndexOf('a', -1));
+        assertEquals(0, sb.lastIndexOf('a', 0));
+        assertEquals(0, sb.lastIndexOf('a', 1));
+
+        // should work like String#lastIndexOf
+        assertEquals("abab".lastIndexOf('a', 1), sb.lastIndexOf('a', 1));
+
+        assertEquals(1, sb.lastIndexOf('b', 2));
+        assertEquals("abab".lastIndexOf('b', 2), sb.lastIndexOf('b', 2));
+
+        assertEquals(-1, sb.lastIndexOf('z', 2));
+
+        sb = new StrBuilder("xyzabc");
+        assertEquals(2, sb.lastIndexOf('z', sb.length()));
+        assertEquals(-1, sb.lastIndexOf('z', 1));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testIndexOf_String() {
+        final StrBuilder sb = new StrBuilder("abab");
+        
+        assertEquals(0, sb.indexOf("a"));
+        //should work like String#indexOf
+        assertEquals("abab".indexOf("a"), sb.indexOf("a"));
+        
+        assertEquals(0, sb.indexOf("ab"));
+        //should work like String#indexOf
+        assertEquals("abab".indexOf("ab"), sb.indexOf("ab"));
+        
+        assertEquals(1, sb.indexOf("b"));
+        assertEquals("abab".indexOf("b"), sb.indexOf("b"));
+        
+        assertEquals(1, sb.indexOf("ba"));
+        assertEquals("abab".indexOf("ba"), sb.indexOf("ba"));
+        
+        assertEquals(-1, sb.indexOf("z"));
+        
+        assertEquals(-1, sb.indexOf((String) null));
+    }
+
+    @Test
+    public void testIndexOf_String_int() {
+        StrBuilder sb = new StrBuilder("abab");
+        assertEquals(0, sb.indexOf("a", -1));
+        assertEquals(0, sb.indexOf("a", 0));
+        assertEquals(2, sb.indexOf("a", 1));
+        assertEquals(2, sb.indexOf("a", 2));
+        assertEquals(-1, sb.indexOf("a", 3));
+        assertEquals(-1, sb.indexOf("a", 4));
+        assertEquals(-1, sb.indexOf("a", 5));
+        
+        assertEquals(-1, sb.indexOf("abcdef", 0));
+        assertEquals(0, sb.indexOf("", 0));
+        assertEquals(1, sb.indexOf("", 1));
+        
+        //should work like String#indexOf
+        assertEquals ("abab".indexOf("a", 1), sb.indexOf("a", 1));
+        
+        assertEquals(2, sb.indexOf("ab", 1));
+        //should work like String#indexOf
+        assertEquals("abab".indexOf("ab", 1), sb.indexOf("ab", 1));
+        
+        assertEquals(3, sb.indexOf("b", 2));
+        assertEquals("abab".indexOf("b", 2), sb.indexOf("b", 2));
+        
+        assertEquals(1, sb.indexOf("ba", 1));
+        assertEquals("abab".indexOf("ba", 2), sb.indexOf("ba", 2));
+        
+        assertEquals(-1, sb.indexOf("z", 2));
+        
+        sb = new StrBuilder("xyzabc");
+        assertEquals(2, sb.indexOf("za", 0));
+        assertEquals(-1, sb.indexOf("za", 3));
+        
+        assertEquals(-1, sb.indexOf((String) null, 2));
+    }
+
+    @Test
+    public void testLastIndexOf_String() {
+        final StrBuilder sb = new StrBuilder("abab");
+        
+        assertEquals(2, sb.lastIndexOf("a"));
+        //should work like String#lastIndexOf
+        assertEquals("abab".lastIndexOf("a"), sb.lastIndexOf("a"));
+        
+        assertEquals(2, sb.lastIndexOf("ab"));
+        //should work like String#lastIndexOf
+        assertEquals("abab".lastIndexOf("ab"), sb.lastIndexOf("ab"));
+        
+        assertEquals(3, sb.lastIndexOf("b"));
+        assertEquals("abab".lastIndexOf("b"), sb.lastIndexOf("b"));
+        
+        assertEquals(1, sb.lastIndexOf("ba"));
+        assertEquals("abab".lastIndexOf("ba"), sb.lastIndexOf("ba"));
+        
+        assertEquals(-1, sb.lastIndexOf("z"));
+        
+        assertEquals(-1, sb.lastIndexOf((String) null));
+    }
+
+    @Test
+    public void testLastIndexOf_String_int() {
+        StrBuilder sb = new StrBuilder("abab");
+        assertEquals(-1, sb.lastIndexOf("a", -1));
+        assertEquals(0, sb.lastIndexOf("a", 0));
+        assertEquals(0, sb.lastIndexOf("a", 1));
+        assertEquals(2, sb.lastIndexOf("a", 2));
+        assertEquals(2, sb.lastIndexOf("a", 3));
+        assertEquals(2, sb.lastIndexOf("a", 4));
+        assertEquals(2, sb.lastIndexOf("a", 5));
+        
+        assertEquals(-1, sb.lastIndexOf("abcdef", 3));
+        assertEquals("abab".lastIndexOf("", 3), sb.lastIndexOf("", 3));
+        assertEquals("abab".lastIndexOf("", 1), sb.lastIndexOf("", 1));
+        
+        //should work like String#lastIndexOf
+        assertEquals("abab".lastIndexOf("a", 1), sb.lastIndexOf("a", 1));
+        
+        assertEquals(0, sb.lastIndexOf("ab", 1));
+        //should work like String#lastIndexOf
+        assertEquals("abab".lastIndexOf("ab", 1), sb.lastIndexOf("ab", 1));
+        
+        assertEquals(1, sb.lastIndexOf("b", 2));
+        assertEquals("abab".lastIndexOf("b", 2), sb.lastIndexOf("b", 2));
+        
+        assertEquals(1, sb.lastIndexOf("ba", 2));
+        assertEquals("abab".lastIndexOf("ba", 2), sb.lastIndexOf("ba", 2));
+        
+        assertEquals(-1, sb.lastIndexOf("z", 2));
+        
+        sb = new StrBuilder("xyzabc");
+        assertEquals(2, sb.lastIndexOf("za", sb.length()));
+        assertEquals(-1, sb.lastIndexOf("za", 1));
+        
+        assertEquals(-1, sb.lastIndexOf((String) null, 2));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testIndexOf_StrMatcher() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(-1, sb.indexOf((StrMatcher) null));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a')));
+        
+        sb.append("ab bd");
+        assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a')));
+        assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b')));
+        assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher()));
+        assertEquals(4, sb.indexOf(StrMatcher.charMatcher('d')));
+        assertEquals(-1, sb.indexOf(StrMatcher.noneMatcher()));
+        assertEquals(-1, sb.indexOf((StrMatcher) null));
+        
+        sb.append(" A1 junction");
+        assertEquals(6, sb.indexOf(A_NUMBER_MATCHER));
+    }
+
+    @Test
+    public void testIndexOf_StrMatcher_int() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(-1, sb.indexOf((StrMatcher) null, 2));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 2));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 0));
+        
+        sb.append("ab bd");
+        assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a'), -2));
+        assertEquals(0, sb.indexOf(StrMatcher.charMatcher('a'), 0));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 2));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('a'), 20));
+        
+        assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), -1));
+        assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), 0));
+        assertEquals(1, sb.indexOf(StrMatcher.charMatcher('b'), 1));
+        assertEquals(3, sb.indexOf(StrMatcher.charMatcher('b'), 2));
+        assertEquals(3, sb.indexOf(StrMatcher.charMatcher('b'), 3));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 4));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 5));
+        assertEquals(-1, sb.indexOf(StrMatcher.charMatcher('b'), 6));
+        
+        assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), -2));
+        assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), 0));
+        assertEquals(2, sb.indexOf(StrMatcher.spaceMatcher(), 2));
+        assertEquals(-1, sb.indexOf(StrMatcher.spaceMatcher(), 4));
+        assertEquals(-1, sb.indexOf(StrMatcher.spaceMatcher(), 20));
+        
+        assertEquals(-1, sb.indexOf(StrMatcher.noneMatcher(), 0));
+        assertEquals(-1, sb.indexOf((StrMatcher) null, 0));
+        
+        sb.append(" A1 junction with A2");
+        assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 5));
+        assertEquals(6, sb.indexOf(A_NUMBER_MATCHER, 6));
+        assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 7));
+        assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 22));
+        assertEquals(23, sb.indexOf(A_NUMBER_MATCHER, 23));
+        assertEquals(-1, sb.indexOf(A_NUMBER_MATCHER, 24));
+    }
+
+    @Test
+    public void testLastIndexOf_StrMatcher() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(-1, sb.lastIndexOf((StrMatcher) null));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a')));
+        
+        sb.append("ab bd");
+        assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a')));
+        assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b')));
+        assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher()));
+        assertEquals(4, sb.lastIndexOf(StrMatcher.charMatcher('d')));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.noneMatcher()));
+        assertEquals(-1, sb.lastIndexOf((StrMatcher) null));
+        
+        sb.append(" A1 junction");
+        assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER));
+    }
+
+    @Test
+    public void testLastIndexOf_StrMatcher_int() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(-1, sb.lastIndexOf((StrMatcher) null, 2));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), 2));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), 0));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), -1));
+        
+        sb.append("ab bd");
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('a'), -2));
+        assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 0));
+        assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 2));
+        assertEquals(0, sb.lastIndexOf(StrMatcher.charMatcher('a'), 20));
+        
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('b'), -1));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 0));
+        assertEquals(1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 1));
+        assertEquals(1, sb.lastIndexOf(StrMatcher.charMatcher('b'), 2));
+        assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 3));
+        assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 4));
+        assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 5));
+        assertEquals(3, sb.lastIndexOf(StrMatcher.charMatcher('b'), 6));
+        
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.spaceMatcher(), -2));
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.spaceMatcher(), 0));
+        assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 2));
+        assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 4));
+        assertEquals(2, sb.lastIndexOf(StrMatcher.spaceMatcher(), 20));
+        
+        assertEquals(-1, sb.lastIndexOf(StrMatcher.noneMatcher(), 0));
+        assertEquals(-1, sb.lastIndexOf((StrMatcher) null, 0));
+        
+        sb.append(" A1 junction with A2");
+        assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 5));
+        assertEquals(-1, sb.lastIndexOf(A_NUMBER_MATCHER, 6)); // A matches, 1 is outside bounds
+        assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 7));
+        assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 22));
+        assertEquals(6, sb.lastIndexOf(A_NUMBER_MATCHER, 23)); // A matches, 2 is outside bounds
+        assertEquals(23, sb.lastIndexOf(A_NUMBER_MATCHER, 24));
+    }
+
+    static final StrMatcher A_NUMBER_MATCHER = new StrMatcher() {
+        @Override
+        public int isMatch(final char[] buffer, int pos, final int bufferStart, final int bufferEnd) {
+            if (buffer[pos] == 'A') {
+                pos++;
+                if (pos < bufferEnd && buffer[pos] >= '0' && buffer[pos] <= '9') {
+                    return 2;
+                }
+            }
+            return 0;
+        }
+    };
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAsTokenizer() throws Exception {
+        // from Javadoc
+        final StrBuilder b = new StrBuilder();
+        b.append("a b ");
+        final StrTokenizer t = b.asTokenizer();
+        
+        final String[] tokens1 = t.getTokenArray();
+        assertEquals(2, tokens1.length);
+        assertEquals("a", tokens1[0]);
+        assertEquals("b", tokens1[1]);
+        assertEquals(2, t.size());
+        
+        b.append("c d ");
+        final String[] tokens2 = t.getTokenArray();
+        assertEquals(2, tokens2.length);
+        assertEquals("a", tokens2[0]);
+        assertEquals("b", tokens2[1]);
+        assertEquals(2, t.size());
+        assertEquals("a", t.next());
+        assertEquals("b", t.next());
+        
+        t.reset();
+        final String[] tokens3 = t.getTokenArray();
+        assertEquals(4, tokens3.length);
+        assertEquals("a", tokens3[0]);
+        assertEquals("b", tokens3[1]);
+        assertEquals("c", tokens3[2]);
+        assertEquals("d", tokens3[3]);
+        assertEquals(4, t.size());
+        assertEquals("a", t.next());
+        assertEquals("b", t.next());
+        assertEquals("c", t.next());
+        assertEquals("d", t.next());
+        
+        assertEquals("a b c d ", t.getContent());
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testAsReader() throws Exception {
+        final StrBuilder sb = new StrBuilder("some text");
+        Reader reader = sb.asReader();
+        assertTrue(reader.ready());
+        final char[] buf = new char[40];
+        assertEquals(9, reader.read(buf));
+        assertEquals("some text", new String(buf, 0, 9));
+        
+        assertEquals(-1, reader.read());
+        assertFalse(reader.ready());
+        assertEquals(0, reader.skip(2));
+        assertEquals(0, reader.skip(-1));
+        
+        assertTrue(reader.markSupported());
+        reader = sb.asReader();
+        assertEquals('s', reader.read());
+        reader.mark(-1);
+        char[] array = new char[3];
+        assertEquals(3, reader.read(array, 0, 3));
+        assertEquals('o', array[0]);
+        assertEquals('m', array[1]);
+        assertEquals('e', array[2]);
+        reader.reset();
+        assertEquals(1, reader.read(array, 1, 1));
+        assertEquals('o', array[0]);
+        assertEquals('o', array[1]);
+        assertEquals('e', array[2]);
+        assertEquals(2, reader.skip(2));
+        assertEquals(' ', reader.read());
+        
+        assertTrue(reader.ready());
+        reader.close();
+        assertTrue(reader.ready());
+        
+        reader = sb.asReader();
+        array = new char[3];
+        try {
+            reader.read(array, -1, 0);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        try {
+            reader.read(array, 0, -1);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        try {
+            reader.read(array, 100, 1);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        try {
+            reader.read(array, 0, 100);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        try {
+            reader.read(array, Integer.MAX_VALUE, Integer.MAX_VALUE);
+            fail();
+        } catch (final IndexOutOfBoundsException ex) {}
+        
+        assertEquals(0, reader.read(array, 0, 0));
+        assertEquals(0, array[0]);
+        assertEquals(0, array[1]);
+        assertEquals(0, array[2]);
+        
+        reader.skip(9);
+        assertEquals(-1, reader.read(array, 0, 1));
+        
+        reader.reset();
+        array = new char[30];
+        assertEquals(9, reader.read(array, 0, 30));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAsWriter() throws Exception {
+        final StrBuilder sb = new StrBuilder("base");
+        final Writer writer = sb.asWriter();
+        
+        writer.write('l');
+        assertEquals("basel", sb.toString());
+        
+        writer.write(new char[] {'i', 'n'});
+        assertEquals("baselin", sb.toString());
+        
+        writer.write(new char[] {'n', 'e', 'r'}, 1, 2);
+        assertEquals("baseliner", sb.toString());
+        
+        writer.write(" rout");
+        assertEquals("baseliner rout", sb.toString());
+        
+        writer.write("ping that server", 1, 3);
+        assertEquals("baseliner routing", sb.toString());
+        
+        writer.flush();  // no effect
+        assertEquals("baseliner routing", sb.toString());
+        
+        writer.close();  // no effect
+        assertEquals("baseliner routing", sb.toString());
+        
+        writer.write(" hi");  // works after close
+        assertEquals("baseliner routing hi", sb.toString());
+        
+        sb.setLength(4);  // mix and match
+        writer.write('d');
+        assertEquals("based", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testEqualsIgnoreCase() {
+        final StrBuilder sb1 = new StrBuilder();
+        final StrBuilder sb2 = new StrBuilder();
+        assertTrue(sb1.equalsIgnoreCase(sb1));
+        assertTrue(sb1.equalsIgnoreCase(sb2));
+        assertTrue(sb2.equalsIgnoreCase(sb2));
+        
+        sb1.append("abc");
+        assertFalse(sb1.equalsIgnoreCase(sb2));
+        
+        sb2.append("ABC");
+        assertTrue(sb1.equalsIgnoreCase(sb2));
+        
+        sb2.clear().append("abc");
+        assertTrue(sb1.equalsIgnoreCase(sb2));
+        assertTrue(sb1.equalsIgnoreCase(sb1));
+        assertTrue(sb2.equalsIgnoreCase(sb2));
+        
+        sb2.clear().append("aBc");
+        assertTrue(sb1.equalsIgnoreCase(sb2));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testEquals() {
+        final StrBuilder sb1 = new StrBuilder();
+        final StrBuilder sb2 = new StrBuilder();
+        assertTrue(sb1.equals(sb2));
+        assertTrue(sb1.equals(sb1));
+        assertTrue(sb2.equals(sb2));
+        assertTrue(sb1.equals((Object) sb2));
+        
+        sb1.append("abc");
+        assertFalse(sb1.equals(sb2));
+        assertFalse(sb1.equals((Object) sb2));
+        
+        sb2.append("ABC");
+        assertFalse(sb1.equals(sb2));
+        assertFalse(sb1.equals((Object) sb2));
+        
+        sb2.clear().append("abc");
+        assertTrue(sb1.equals(sb2));
+        assertTrue(sb1.equals((Object) sb2));
+        
+        assertFalse(sb1.equals(Integer.valueOf(1)));
+        assertFalse(sb1.equals("abc"));
+    }
+
+    @Test
+    public void test_LANG_1131_EqualsWithNullStrBuilder() throws Exception {
+        final StrBuilder sb = new StrBuilder();
+        final StrBuilder other = null;
+        assertFalse(sb.equals(other));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testHashCode() {
+        final StrBuilder sb = new StrBuilder();
+        final int hc1a = sb.hashCode();
+        final int hc1b = sb.hashCode();
+        assertEquals(0, hc1a);
+        assertEquals(hc1a, hc1b);
+        
+        sb.append("abc");
+        final int hc2a = sb.hashCode();
+        final int hc2b = sb.hashCode();
+        assertTrue(hc2a != 0);
+        assertEquals(hc2a, hc2b);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testToString() {
+        final StrBuilder sb = new StrBuilder("abc");
+        assertEquals("abc", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testToStringBuffer() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(new StringBuffer().toString(), sb.toStringBuffer().toString());
+        
+        sb.append("junit");
+        assertEquals(new StringBuffer("junit").toString(), sb.toStringBuffer().toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testToStringBuilder() {
+        final StrBuilder sb = new StrBuilder();
+        assertEquals(new StringBuilder().toString(), sb.toStringBuilder().toString());
+        
+        sb.append("junit");
+        assertEquals(new StringBuilder("junit").toString(), sb.toStringBuilder().toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testLang294() {
+        final StrBuilder sb = new StrBuilder("\n%BLAH%\nDo more stuff\neven more stuff\n%BLAH%\n");
+        sb.deleteAll("\n%BLAH%");
+        assertEquals("\nDo more stuff\neven more stuff\n", sb.toString()); 
+    }
+
+    @Test
+    public void testIndexOfLang294() {
+        final StrBuilder sb = new StrBuilder("onetwothree");
+        sb.deleteFirst("three");
+        assertEquals(-1, sb.indexOf("three"));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testLang295() {
+        final StrBuilder sb = new StrBuilder("onetwothree");
+        sb.deleteFirst("three");
+        assertFalse( "The contains(char) method is looking beyond the end of the string", sb.contains('h'));
+        assertEquals( "The indexOf(char) method is looking beyond the end of the string", -1, sb.indexOf('h'));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testLang412Right() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadRight(null, 10, '*');
+        assertEquals( "Failed to invoke appendFixedWidthPadRight correctly", "**********", sb.toString());
+    }
+
+    @Test
+    public void testLang412Left() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadLeft(null, 10, '*');
+        assertEquals( "Failed to invoke appendFixedWidthPadLeft correctly", "**********", sb.toString());
+    }
+
+    @Test
+    public void testAsBuilder() {
+        final StrBuilder sb = new StrBuilder().appendAll("Lorem", " ", "ipsum", " ", "dolor");
+        assertEquals(sb.toString(), sb.build());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendCharBuffer() {
+        final StrBuilder sb1 = new StrBuilder();
+        final CharBuffer buf = CharBuffer.allocate(10);
+        buf.append("0123456789");
+        buf.flip();
+        sb1.append(buf);
+        assertEquals("0123456789", sb1.toString());
+
+        final StrBuilder sb2 = new StrBuilder();
+        sb2.append(buf, 1, 8);
+        assertEquals("12345678", sb2.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendToWriter() throws Exception {
+        final StrBuilder sb = new StrBuilder("1234567890");
+        final StringWriter writer = new StringWriter();
+        writer.append("Test ");
+
+        sb.appendTo(writer);
+
+        assertEquals("Test 1234567890", writer.toString());
+    }
+
+    @Test
+    public void testAppendToStringBuilder() throws Exception {
+        final StrBuilder sb = new StrBuilder("1234567890");
+        final StringBuilder builder = new StringBuilder("Test ");
+
+        sb.appendTo(builder);
+
+        assertEquals("Test 1234567890", builder.toString());
+    }
+
+    @Test
+    public void testAppendToStringBuffer() throws Exception {
+        final StrBuilder sb = new StrBuilder("1234567890");
+        final StringBuffer buffer = new StringBuffer("Test ");
+
+        sb.appendTo(buffer);
+
+        assertEquals("Test 1234567890", buffer.toString());
+    }
+
+    @Test
+    public void testAppendToCharBuffer() throws Exception {
+        final StrBuilder sb = new StrBuilder("1234567890");
+        final String text = "Test ";
+        final CharBuffer buffer = CharBuffer.allocate(sb.size() + text.length());
+        buffer.put(text);
+
+        sb.appendTo(buffer);
+
+        buffer.flip();
+        assertEquals("Test 1234567890", buffer.toString());
+    }
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrLookupTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrLookupTest.java b/src/test/java/org/apache/commons/text/StrLookupTest.java
new file mode 100644
index 0000000..dfd8933
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrLookupTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.Test;
+
+/**
+ * Test class for {@link StrLookup}.
+ */
+public class StrLookupTest  {
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testNoneLookup() {
+        assertEquals(null, StrLookup.noneLookup().lookup(null));
+        assertEquals(null, StrLookup.noneLookup().lookup(""));
+        assertEquals(null, StrLookup.noneLookup().lookup("any"));
+    }
+
+    @Test
+    public void testSystemProperiesLookup() {
+        assertEquals(System.getProperty("os.name"), StrLookup.systemPropertiesLookup().lookup("os.name"));
+        assertEquals(null, StrLookup.systemPropertiesLookup().lookup(""));
+        assertEquals(null, StrLookup.systemPropertiesLookup().lookup("other"));
+        try {
+            StrLookup.systemPropertiesLookup().lookup(null);
+            fail();
+        } catch (final NullPointerException ex) {
+            // expected
+        }
+    }
+
+    /**
+     * Tests that a lookup object for system properties can deal with a full
+     * replacement of the system properties object. This test is related to
+     * LANG-1055.
+     */
+    @Test
+    public void testSystemPropertiesLookupReplacedProperties() {
+        final Properties oldProperties = System.getProperties();
+        final String osName = "os.name";
+        final String newOsName = oldProperties.getProperty(osName) + "_changed";
+
+        final StrLookup<String> sysLookup = StrLookup.systemPropertiesLookup();
+        final Properties newProps = new Properties();
+        newProps.setProperty(osName, newOsName);
+        System.setProperties(newProps);
+        try {
+            assertEquals("Changed properties not detected", newOsName, sysLookup.lookup(osName));
+        } finally {
+            System.setProperties(oldProperties);
+        }
+    }
+
+    /**
+     * Tests that a lookup object for system properties sees changes on system
+     * properties. This test is related to LANG-1141.
+     */
+    @Test
+    public void testSystemPropertiesLookupUpdatedProperty() {
+        final String osName = "os.name";
+        final String oldOs = System.getProperty(osName);
+        final String newOsName = oldOs + "_changed";
+
+        final StrLookup<String> sysLookup = StrLookup.systemPropertiesLookup();
+        System.setProperty(osName, newOsName);
+        try {
+            assertEquals("Changed properties not detected", newOsName, sysLookup.lookup(osName));
+        } finally {
+            System.setProperty(osName, oldOs);
+        }
+    }
+
+    @Test
+    public void testMapLookup() {
+        final Map<String, Object> map = new HashMap<>();
+        map.put("key", "value");
+        map.put("number", Integer.valueOf(2));
+        assertEquals("value", StrLookup.mapLookup(map).lookup("key"));
+        assertEquals("2", StrLookup.mapLookup(map).lookup("number"));
+        assertEquals(null, StrLookup.mapLookup(map).lookup(null));
+        assertEquals(null, StrLookup.mapLookup(map).lookup(""));
+        assertEquals(null, StrLookup.mapLookup(map).lookup("other"));
+    }
+
+    @Test
+    public void testMapLookup_nullMap() {
+        final Map<String, ?> map = null;
+        assertEquals(null, StrLookup.mapLookup(map).lookup(null));
+        assertEquals(null, StrLookup.mapLookup(map).lookup(""));
+        assertEquals(null, StrLookup.mapLookup(map).lookup("any"));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrMatcherTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrMatcherTest.java b/src/test/java/org/apache/commons/text/StrMatcherTest.java
new file mode 100644
index 0000000..2b365e0
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrMatcherTest.java
@@ -0,0 +1,214 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link StrMatcher}.
+ */
+public class StrMatcherTest  {
+
+    private static final char[] BUFFER1 = "0,1\t2 3\n\r\f\u0000'\"".toCharArray();
+
+    private static final char[] BUFFER2 = "abcdef".toCharArray();
+
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCommaMatcher() {
+        final StrMatcher matcher = StrMatcher.commaMatcher();
+        assertSame(matcher, StrMatcher.commaMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 0));
+        assertEquals(1, matcher.isMatch(BUFFER1, 1));
+        assertEquals(0, matcher.isMatch(BUFFER1, 2));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTabMatcher() {
+        final StrMatcher matcher = StrMatcher.tabMatcher();
+        assertSame(matcher, StrMatcher.tabMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 2));
+        assertEquals(1, matcher.isMatch(BUFFER1, 3));
+        assertEquals(0, matcher.isMatch(BUFFER1, 4));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSpaceMatcher() {
+        final StrMatcher matcher = StrMatcher.spaceMatcher();
+        assertSame(matcher, StrMatcher.spaceMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 4));
+        assertEquals(1, matcher.isMatch(BUFFER1, 5));
+        assertEquals(0, matcher.isMatch(BUFFER1, 6));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSplitMatcher() {
+        final StrMatcher matcher = StrMatcher.splitMatcher();
+        assertSame(matcher, StrMatcher.splitMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 2));
+        assertEquals(1, matcher.isMatch(BUFFER1, 3));
+        assertEquals(0, matcher.isMatch(BUFFER1, 4));
+        assertEquals(1, matcher.isMatch(BUFFER1, 5));
+        assertEquals(0, matcher.isMatch(BUFFER1, 6));
+        assertEquals(1, matcher.isMatch(BUFFER1, 7));
+        assertEquals(1, matcher.isMatch(BUFFER1, 8));
+        assertEquals(1, matcher.isMatch(BUFFER1, 9));
+        assertEquals(0, matcher.isMatch(BUFFER1, 10));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTrimMatcher() {
+        final StrMatcher matcher = StrMatcher.trimMatcher();
+        assertSame(matcher, StrMatcher.trimMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 2));
+        assertEquals(1, matcher.isMatch(BUFFER1, 3));
+        assertEquals(0, matcher.isMatch(BUFFER1, 4));
+        assertEquals(1, matcher.isMatch(BUFFER1, 5));
+        assertEquals(0, matcher.isMatch(BUFFER1, 6));
+        assertEquals(1, matcher.isMatch(BUFFER1, 7));
+        assertEquals(1, matcher.isMatch(BUFFER1, 8));
+        assertEquals(1, matcher.isMatch(BUFFER1, 9));
+        assertEquals(1, matcher.isMatch(BUFFER1, 10));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testSingleQuoteMatcher() {
+        final StrMatcher matcher = StrMatcher.singleQuoteMatcher();
+        assertSame(matcher, StrMatcher.singleQuoteMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 10));
+        assertEquals(1, matcher.isMatch(BUFFER1, 11));
+        assertEquals(0, matcher.isMatch(BUFFER1, 12));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testDoubleQuoteMatcher() {
+        final StrMatcher matcher = StrMatcher.doubleQuoteMatcher();
+        assertSame(matcher, StrMatcher.doubleQuoteMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 11));
+        assertEquals(1, matcher.isMatch(BUFFER1, 12));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testQuoteMatcher() {
+        final StrMatcher matcher = StrMatcher.quoteMatcher();
+        assertSame(matcher, StrMatcher.quoteMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 10));
+        assertEquals(1, matcher.isMatch(BUFFER1, 11));
+        assertEquals(1, matcher.isMatch(BUFFER1, 12));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testNoneMatcher() {
+        final StrMatcher matcher = StrMatcher.noneMatcher();
+        assertSame(matcher, StrMatcher.noneMatcher());
+        assertEquals(0, matcher.isMatch(BUFFER1, 0));
+        assertEquals(0, matcher.isMatch(BUFFER1, 1));
+        assertEquals(0, matcher.isMatch(BUFFER1, 2));
+        assertEquals(0, matcher.isMatch(BUFFER1, 3));
+        assertEquals(0, matcher.isMatch(BUFFER1, 4));
+        assertEquals(0, matcher.isMatch(BUFFER1, 5));
+        assertEquals(0, matcher.isMatch(BUFFER1, 6));
+        assertEquals(0, matcher.isMatch(BUFFER1, 7));
+        assertEquals(0, matcher.isMatch(BUFFER1, 8));
+        assertEquals(0, matcher.isMatch(BUFFER1, 9));
+        assertEquals(0, matcher.isMatch(BUFFER1, 10));
+        assertEquals(0, matcher.isMatch(BUFFER1, 11));
+        assertEquals(0, matcher.isMatch(BUFFER1, 12));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCharMatcher_char() {
+        final StrMatcher matcher = StrMatcher.charMatcher('c');
+        assertEquals(0, matcher.isMatch(BUFFER2, 0));
+        assertEquals(0, matcher.isMatch(BUFFER2, 1));
+        assertEquals(1, matcher.isMatch(BUFFER2, 2));
+        assertEquals(0, matcher.isMatch(BUFFER2, 3));
+        assertEquals(0, matcher.isMatch(BUFFER2, 4));
+        assertEquals(0, matcher.isMatch(BUFFER2, 5));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCharSetMatcher_String() {
+        final StrMatcher matcher = StrMatcher.charSetMatcher("ace");
+        assertEquals(1, matcher.isMatch(BUFFER2, 0));
+        assertEquals(0, matcher.isMatch(BUFFER2, 1));
+        assertEquals(1, matcher.isMatch(BUFFER2, 2));
+        assertEquals(0, matcher.isMatch(BUFFER2, 3));
+        assertEquals(1, matcher.isMatch(BUFFER2, 4));
+        assertEquals(0, matcher.isMatch(BUFFER2, 5));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.charSetMatcher(""));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.charSetMatcher((String) null));
+        assertTrue(StrMatcher.charSetMatcher("a") instanceof StrMatcher.CharMatcher);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCharSetMatcher_charArray() {
+        final StrMatcher matcher = StrMatcher.charSetMatcher("ace".toCharArray());
+        assertEquals(1, matcher.isMatch(BUFFER2, 0));
+        assertEquals(0, matcher.isMatch(BUFFER2, 1));
+        assertEquals(1, matcher.isMatch(BUFFER2, 2));
+        assertEquals(0, matcher.isMatch(BUFFER2, 3));
+        assertEquals(1, matcher.isMatch(BUFFER2, 4));
+        assertEquals(0, matcher.isMatch(BUFFER2, 5));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.charSetMatcher(new char[0]));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.charSetMatcher((char[]) null));
+        assertTrue(StrMatcher.charSetMatcher("a".toCharArray()) instanceof StrMatcher.CharMatcher);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testStringMatcher_String() {
+        final StrMatcher matcher = StrMatcher.stringMatcher("bc");
+        assertEquals(0, matcher.isMatch(BUFFER2, 0));
+        assertEquals(2, matcher.isMatch(BUFFER2, 1));
+        assertEquals(0, matcher.isMatch(BUFFER2, 2));
+        assertEquals(0, matcher.isMatch(BUFFER2, 3));
+        assertEquals(0, matcher.isMatch(BUFFER2, 4));
+        assertEquals(0, matcher.isMatch(BUFFER2, 5));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.stringMatcher(""));
+        assertSame(StrMatcher.noneMatcher(), StrMatcher.stringMatcher((String) null));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testMatcherIndices() {
+        // remember that the API contract is tight for the isMatch() method
+        // all the onus is on the caller, so invalid inputs are not
+        // the concern of StrMatcher, and are not bugs
+        final StrMatcher matcher = StrMatcher.stringMatcher("bc");
+        assertEquals(2, matcher.isMatch(BUFFER2, 1, 1, BUFFER2.length));
+        assertEquals(2, matcher.isMatch(BUFFER2, 1, 0, 3));
+        assertEquals(0, matcher.isMatch(BUFFER2, 1, 0, 2));
+    }
+
+}


[08/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/StrLookup.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/StrLookup.java b/src/main/java/org/apache/commons/text/StrLookup.java
new file mode 100644
index 0000000..4626713
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/StrLookup.java
@@ -0,0 +1,178 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.util.Map;
+
+/**
+ * Lookup a String key to a String value.
+ * <p>
+ * This class represents the simplest form of a string to string map.
+ * It has a benefit over a map in that it can create the result on
+ * demand based on the key.
+ * <p>
+ * This class comes complete with various factory methods.
+ * If these do not suffice, you can subclass and implement your own matcher.
+ * <p>
+ * For example, it would be possible to implement a lookup that used the
+ * key as a primary key, and looked up the value on demand from the database
+ */
+public abstract class StrLookup<V> {
+
+    /**
+     * Lookup that always returns null.
+     */
+    private static final StrLookup<String> NONE_LOOKUP = new MapStrLookup<>(null);
+
+    /**
+     * Lookup based on system properties.
+     */
+    private static final StrLookup<String> SYSTEM_PROPERTIES_LOOKUP = new SystemPropertiesStrLookup();
+
+    //-----------------------------------------------------------------------
+    /**
+     * Returns a lookup which always returns null.
+     *
+     * @return a lookup that always returns null, not null
+     */
+    public static StrLookup<?> noneLookup() {
+        return NONE_LOOKUP;
+    }
+
+    /**
+     * Returns a new lookup which uses a copy of the current
+     * {@link System#getProperties() System properties}.
+     * <p>
+     * If a security manager blocked access to system properties, then null will
+     * be returned from every lookup.
+     * <p>
+     * If a null key is used, this lookup will throw a NullPointerException.
+     *
+     * @return a lookup using system properties, not null
+     */
+    public static StrLookup<String> systemPropertiesLookup() {
+        return SYSTEM_PROPERTIES_LOOKUP;
+    }
+
+    /**
+     * Returns a lookup which looks up values using a map.
+     * <p>
+     * If the map is null, then null will be returned from every lookup.
+     * The map result object is converted to a string using toString().
+     *
+     * @param <V> the type of the values supported by the lookup
+     * @param map  the map of keys to values, may be null
+     * @return a lookup using the map, not null
+     */
+    public static <V> StrLookup<V> mapLookup(final Map<String, V> map) {
+        return new MapStrLookup<>(map);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Constructor.
+     */
+    protected StrLookup() {
+        super();
+    }
+
+    /**
+     * Looks up a String key to a String value.
+     * <p>
+     * The internal implementation may use any mechanism to return the value.
+     * The simplest implementation is to use a Map. However, virtually any
+     * implementation is possible.
+     * <p>
+     * For example, it would be possible to implement a lookup that used the
+     * key as a primary key, and looked up the value on demand from the database
+     * Or, a numeric based implementation could be created that treats the key
+     * as an integer, increments the value and return the result as a string -
+     * converting 1 to 2, 15 to 16 etc.
+     * <p>
+     * The {@link #lookup(String)} method always returns a String, regardless of
+     * the underlying data, by converting it as necessary. For example:
+     * <pre>
+     * Map&lt;String, Object&gt; map = new HashMap&lt;String, Object&gt;();
+     * map.put("number", Integer.valueOf(2));
+     * assertEquals("2", StrLookup.mapLookup(map).lookup("number"));
+     * </pre>
+     * @param key  the key to be looked up, may be null
+     * @return the matching value, null if no match
+     */
+    public abstract String lookup(String key);
+
+    //-----------------------------------------------------------------------
+    /**
+     * Lookup implementation that uses a Map.
+     */
+    static class MapStrLookup<V> extends StrLookup<V> {
+
+        /** Map keys are variable names and value. */
+        private final Map<String, V> map;
+
+        /**
+         * Creates a new instance backed by a Map.
+         *
+         * @param map  the map of keys to values, may be null
+         */
+        MapStrLookup(final Map<String, V> map) {
+            this.map = map;
+        }
+
+        /**
+         * Looks up a String key to a String value using the map.
+         * <p>
+         * If the map is null, then null is returned.
+         * The map result object is converted to a string using toString().
+         *
+         * @param key  the key to be looked up, may be null
+         * @return the matching value, null if no match
+         */
+        @Override
+        public String lookup(final String key) {
+            if (map == null) {
+                return null;
+            }
+            final Object obj = map.get(key);
+            if (obj == null) {
+                return null;
+            }
+            return obj.toString();
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Lookup implementation based on system properties.
+     */
+    private static class SystemPropertiesStrLookup extends StrLookup<String> {
+        /**
+         * {@inheritDoc} This implementation directly accesses system properties.
+         */
+        @Override
+        public String lookup(final String key) {
+            if (key.length() > 0) {
+                try {
+                    return System.getProperty(key);
+                } catch (final SecurityException scex) {
+                    // Squelched. All lookup(String) will return null.
+                }
+            }
+            return null;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/StrMatcher.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/StrMatcher.java b/src/main/java/org/apache/commons/text/StrMatcher.java
new file mode 100644
index 0000000..a115a9d
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/StrMatcher.java
@@ -0,0 +1,437 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.util.Arrays;
+
+/**
+ * A matcher class that can be queried to determine if a character array
+ * portion matches.
+ * <p>
+ * This class comes complete with various factory methods.
+ * If these do not suffice, you can subclass and implement your own matcher.
+ */
+public abstract class StrMatcher {
+
+    /**
+     * Matches the comma character.
+     */
+    private static final StrMatcher COMMA_MATCHER = new CharMatcher(',');
+    /**
+     * Matches the tab character.
+     */
+    private static final StrMatcher TAB_MATCHER = new CharMatcher('\t');
+    /**
+     * Matches the space character.
+     */
+    private static final StrMatcher SPACE_MATCHER = new CharMatcher(' ');
+    /**
+     * Matches the same characters as StringTokenizer,
+     * namely space, tab, newline, formfeed.
+     */
+    private static final StrMatcher SPLIT_MATCHER = new CharSetMatcher(" \t\n\r\f".toCharArray());
+    /**
+     * Matches the String trim() whitespace characters.
+     */
+    private static final StrMatcher TRIM_MATCHER = new TrimMatcher();
+    /**
+     * Matches the double quote character.
+     */
+    private static final StrMatcher SINGLE_QUOTE_MATCHER = new CharMatcher('\'');
+    /**
+     * Matches the double quote character.
+     */
+    private static final StrMatcher DOUBLE_QUOTE_MATCHER = new CharMatcher('"');
+    /**
+     * Matches the single or double quote character.
+     */
+    private static final StrMatcher QUOTE_MATCHER = new CharSetMatcher("'\"".toCharArray());
+    /**
+     * Matches no characters.
+     */
+    private static final StrMatcher NONE_MATCHER = new NoMatcher();
+
+    // -----------------------------------------------------------------------
+
+    /**
+     * Returns a matcher which matches the comma character.
+     *
+     * @return a matcher for a comma
+     */
+    public static StrMatcher commaMatcher() {
+        return COMMA_MATCHER;
+    }
+
+    /**
+     * Returns a matcher which matches the tab character.
+     *
+     * @return a matcher for a tab
+     */
+    public static StrMatcher tabMatcher() {
+        return TAB_MATCHER;
+    }
+
+    /**
+     * Returns a matcher which matches the space character.
+     *
+     * @return a matcher for a space
+     */
+    public static StrMatcher spaceMatcher() {
+        return SPACE_MATCHER;
+    }
+
+    /**
+     * Matches the same characters as StringTokenizer,
+     * namely space, tab, newline and formfeed.
+     *
+     * @return the split matcher
+     */
+    public static StrMatcher splitMatcher() {
+        return SPLIT_MATCHER;
+    }
+
+    /**
+     * Matches the String trim() whitespace characters.
+     *
+     * @return the trim matcher
+     */
+    public static StrMatcher trimMatcher() {
+        return TRIM_MATCHER;
+    }
+
+    /**
+     * Returns a matcher which matches the single quote character.
+     *
+     * @return a matcher for a single quote
+     */
+    public static StrMatcher singleQuoteMatcher() {
+        return SINGLE_QUOTE_MATCHER;
+    }
+
+    /**
+     * Returns a matcher which matches the double quote character.
+     *
+     * @return a matcher for a double quote
+     */
+    public static StrMatcher doubleQuoteMatcher() {
+        return DOUBLE_QUOTE_MATCHER;
+    }
+
+    /**
+     * Returns a matcher which matches the single or double quote character.
+     *
+     * @return a matcher for a single or double quote
+     */
+    public static StrMatcher quoteMatcher() {
+        return QUOTE_MATCHER;
+    }
+
+    /**
+     * Matches no characters.
+     *
+     * @return a matcher that matches nothing
+     */
+    public static StrMatcher noneMatcher() {
+        return NONE_MATCHER;
+    }
+
+    /**
+     * Constructor that creates a matcher from a character.
+     *
+     * @param ch  the character to match, must not be null
+     * @return a new Matcher for the given char
+     */
+    public static StrMatcher charMatcher(final char ch) {
+        return new CharMatcher(ch);
+    }
+
+    /**
+     * Constructor that creates a matcher from a set of characters.
+     *
+     * @param chars  the characters to match, null or empty matches nothing
+     * @return a new matcher for the given char[]
+     */
+    public static StrMatcher charSetMatcher(final char... chars) {
+        if (chars == null || chars.length == 0) {
+            return NONE_MATCHER;
+        }
+        if (chars.length == 1) {
+            return new CharMatcher(chars[0]);
+        }
+        return new CharSetMatcher(chars);
+    }
+
+    /**
+     * Constructor that creates a matcher from a string representing a set of characters.
+     *
+     * @param chars  the characters to match, null or empty matches nothing
+     * @return a new Matcher for the given characters
+     */
+    public static StrMatcher charSetMatcher(final String chars) {
+        if (chars == null || chars.length() == 0) {
+            return NONE_MATCHER;
+        }
+        if (chars.length() == 1) {
+            return new CharMatcher(chars.charAt(0));
+        }
+        return new CharSetMatcher(chars.toCharArray());
+    }
+
+    /**
+     * Constructor that creates a matcher from a string.
+     *
+     * @param str  the string to match, null or empty matches nothing
+     * @return a new Matcher for the given String
+     */
+    public static StrMatcher stringMatcher(final String str) {
+        if (str == null || str.length() == 0) {
+            return NONE_MATCHER;
+        }
+        return new StringMatcher(str);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Constructor.
+     */
+    protected StrMatcher() {
+        super();
+    }
+
+    /**
+     * Returns the number of matching characters, zero for no match.
+     * <p>
+     * This method is called to check for a match.
+     * The parameter <code>pos</code> represents the current position to be
+     * checked in the string <code>buffer</code> (a character array which must
+     * not be changed).
+     * The API guarantees that <code>pos</code> is a valid index for <code>buffer</code>.
+     * <p>
+     * The character array may be larger than the active area to be matched.
+     * Only values in the buffer between the specified indices may be accessed.
+     * <p>
+     * The matching code may check one character or many.
+     * It may check characters preceding <code>pos</code> as well as those
+     * after, so long as no checks exceed the bounds specified.
+     * <p>
+     * It must return zero for no match, or a positive number if a match was found.
+     * The number indicates the number of characters that matched.
+     *
+     * @param buffer  the text content to match against, do not change
+     * @param pos  the starting position for the match, valid for buffer
+     * @param bufferStart  the first active index in the buffer, valid for buffer
+     * @param bufferEnd  the end index (exclusive) of the active buffer, valid for buffer
+     * @return the number of matching characters, zero for no match
+     */
+    public abstract int isMatch(char[] buffer, int pos, int bufferStart, int bufferEnd);
+
+    /**
+     * Returns the number of matching characters, zero for no match.
+     * <p>
+     * This method is called to check for a match.
+     * The parameter <code>pos</code> represents the current position to be
+     * checked in the string <code>buffer</code> (a character array which must
+     * not be changed).
+     * The API guarantees that <code>pos</code> is a valid index for <code>buffer</code>.
+     * <p>
+     * The matching code may check one character or many.
+     * It may check characters preceding <code>pos</code> as well as those after.
+     * <p>
+     * It must return zero for no match, or a positive number if a match was found.
+     * The number indicates the number of characters that matched.
+     *
+     * @param buffer  the text content to match against, do not change
+     * @param pos  the starting position for the match, valid for buffer
+     * @return the number of matching characters, zero for no match
+     * @since 2.4
+     */
+    public int isMatch(final char[] buffer, final int pos) {
+        return isMatch(buffer, pos, 0, buffer.length);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Class used to define a set of characters for matching purposes.
+     */
+    static final class CharSetMatcher extends StrMatcher {
+        /** The set of characters to match. */
+        private final char[] chars;
+
+        /**
+         * Constructor that creates a matcher from a character array.
+         *
+         * @param chars  the characters to match, must not be null
+         */
+        CharSetMatcher(final char chars[]) {
+            super();
+            this.chars = chars.clone();
+            Arrays.sort(this.chars);
+        }
+
+        /**
+         * Returns whether or not the given character matches.
+         *
+         * @param buffer  the text content to match against, do not change
+         * @param pos  the starting position for the match, valid for buffer
+         * @param bufferStart  the first active index in the buffer, valid for buffer
+         * @param bufferEnd  the end index of the active buffer, valid for buffer
+         * @return the number of matching characters, zero for no match
+         */
+        @Override
+        public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) {
+            return Arrays.binarySearch(chars, buffer[pos]) >= 0 ? 1 : 0;
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Class used to define a character for matching purposes.
+     */
+    static final class CharMatcher extends StrMatcher {
+        /** The character to match. */
+        private final char ch;
+
+        /**
+         * Constructor that creates a matcher that matches a single character.
+         *
+         * @param ch  the character to match
+         */
+        CharMatcher(final char ch) {
+            super();
+            this.ch = ch;
+        }
+
+        /**
+         * Returns whether or not the given character matches.
+         *
+         * @param buffer  the text content to match against, do not change
+         * @param pos  the starting position for the match, valid for buffer
+         * @param bufferStart  the first active index in the buffer, valid for buffer
+         * @param bufferEnd  the end index of the active buffer, valid for buffer
+         * @return the number of matching characters, zero for no match
+         */
+        @Override
+        public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) {
+            return ch == buffer[pos] ? 1 : 0;
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Class used to define a set of characters for matching purposes.
+     */
+    static final class StringMatcher extends StrMatcher {
+        /** The string to match, as a character array. */
+        private final char[] chars;
+
+        /**
+         * Constructor that creates a matcher from a String.
+         *
+         * @param str  the string to match, must not be null
+         */
+        StringMatcher(final String str) {
+            super();
+            chars = str.toCharArray();
+        }
+
+        /**
+         * Returns whether or not the given text matches the stored string.
+         *
+         * @param buffer  the text content to match against, do not change
+         * @param pos  the starting position for the match, valid for buffer
+         * @param bufferStart  the first active index in the buffer, valid for buffer
+         * @param bufferEnd  the end index of the active buffer, valid for buffer
+         * @return the number of matching characters, zero for no match
+         */
+        @Override
+        public int isMatch(final char[] buffer, int pos, final int bufferStart, final int bufferEnd) {
+            final int len = chars.length;
+            if (pos + len > bufferEnd) {
+                return 0;
+            }
+            for (int i = 0; i < chars.length; i++, pos++) {
+                if (chars[i] != buffer[pos]) {
+                    return 0;
+                }
+            }
+            return len;
+        }
+        
+        @Override
+        public String toString() {
+            return super.toString() + ' ' + Arrays.toString(chars);
+        }
+
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Class used to match no characters.
+     */
+    static final class NoMatcher extends StrMatcher {
+
+        /**
+         * Constructs a new instance of <code>NoMatcher</code>.
+         */
+        NoMatcher() {
+            super();
+        }
+
+        /**
+         * Always returns <code>false</code>.
+         *
+         * @param buffer  the text content to match against, do not change
+         * @param pos  the starting position for the match, valid for buffer
+         * @param bufferStart  the first active index in the buffer, valid for buffer
+         * @param bufferEnd  the end index of the active buffer, valid for buffer
+         * @return the number of matching characters, zero for no match
+         */
+        @Override
+        public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) {
+            return 0;
+        }
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Class used to match whitespace as per trim().
+     */
+    static final class TrimMatcher extends StrMatcher {
+
+        /**
+         * Constructs a new instance of <code>TrimMatcher</code>.
+         */
+        TrimMatcher() {
+            super();
+        }
+
+        /**
+         * Returns whether or not the given character matches.
+         *
+         * @param buffer  the text content to match against, do not change
+         * @param pos  the starting position for the match, valid for buffer
+         * @param bufferStart  the first active index in the buffer, valid for buffer
+         * @param bufferEnd  the end index of the active buffer, valid for buffer
+         * @return the number of matching characters, zero for no match
+         */
+        @Override
+        public int isMatch(final char[] buffer, final int pos, final int bufferStart, final int bufferEnd) {
+            return buffer[pos] <= 32 ? 1 : 0;
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/StrSubstitutor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/StrSubstitutor.java b/src/main/java/org/apache/commons/text/StrSubstitutor.java
new file mode 100644
index 0000000..73a3cc1
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/StrSubstitutor.java
@@ -0,0 +1,1227 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+/**
+ * Substitutes variables within a string by values.
+ * <p>
+ * This class takes a piece of text and substitutes all the variables within it.
+ * The default definition of a variable is <code>${variableName}</code>.
+ * The prefix and suffix can be changed via constructors and set methods.
+ * <p>
+ * Variable values are typically resolved from a map, but could also be resolved
+ * from system properties, or by supplying a custom variable resolver.
+ * <p>
+ * The simplest example is to use this class to replace Java System properties. For example:
+ * <pre>
+ * StrSubstitutor.replaceSystemProperties(
+ *      "You are running with java.version = ${java.version} and os.name = ${os.name}.");
+ * </pre>
+ * <p>
+ * Typical usage of this class follows the following pattern: First an instance is created
+ * and initialized with the map that contains the values for the available variables.
+ * If a prefix and/or suffix for variables should be used other than the default ones,
+ * the appropriate settings can be performed. After that the <code>replace()</code>
+ * method can be called passing in the source text for interpolation. In the returned
+ * text all variable references (as long as their values are known) will be resolved.
+ * The following example demonstrates this:
+ * <pre>
+ * Map valuesMap = HashMap();
+ * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
+ * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
+ * String templateString = &quot;The ${animal} jumped over the ${target}.&quot;;
+ * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+ * String resolvedString = sub.replace(templateString);
+ * </pre>
+ * yielding:
+ * <pre>
+ *      The quick brown fox jumped over the lazy dog.
+ * </pre>
+ * <p>
+ * Also, this class allows to set a default value for unresolved variables.
+ * The default value for a variable can be appended to the variable name after the variable
+ * default value delimiter. The default value of the variable default value delimiter is ':-',
+ * as in bash and other *nix shells, as those are arguably where the default ${} delimiter set originated.
+ * The variable default value delimiter can be manually set by calling {@link #setValueDelimiterMatcher(StrMatcher)},
+ * {@link #setValueDelimiter(char)} or {@link #setValueDelimiter(String)}.
+ * The following shows an example with variable default value settings:
+ * <pre>
+ * Map valuesMap = HashMap();
+ * valuesMap.put(&quot;animal&quot;, &quot;quick brown fox&quot;);
+ * valuesMap.put(&quot;target&quot;, &quot;lazy dog&quot;);
+ * String templateString = &quot;The ${animal} jumped over the ${target}. ${undefined.number:-1234567890}.&quot;;
+ * StrSubstitutor sub = new StrSubstitutor(valuesMap);
+ * String resolvedString = sub.replace(templateString);
+ * </pre>
+ * yielding:
+ * <pre>
+ *      The quick brown fox jumped over the lazy dog. 1234567890.
+ * </pre>
+ * <p>
+ * In addition to this usage pattern there are some static convenience methods that
+ * cover the most common use cases. These methods can be used without the need of
+ * manually creating an instance. However if multiple replace operations are to be
+ * performed, creating and reusing an instance of this class will be more efficient.
+ * <p>
+ * Variable replacement works in a recursive way. Thus, if a variable value contains
+ * a variable then that variable will also be replaced. Cyclic replacements are
+ * detected and will cause an exception to be thrown.
+ * <p>
+ * Sometimes the interpolation's result must contain a variable prefix. As an example
+ * take the following source text:
+ * <pre>
+ *   The variable ${${name}} must be used.
+ * </pre>
+ * Here only the variable's name referred to in the text should be replaced resulting
+ * in the text (assuming that the value of the <code>name</code> variable is <code>x</code>):
+ * <pre>
+ *   The variable ${x} must be used.
+ * </pre>
+ * To achieve this effect there are two possibilities: Either set a different prefix
+ * and suffix for variables which do not conflict with the result text you want to
+ * produce. The other possibility is to use the escape character, by default '$'.
+ * If this character is placed before a variable reference, this reference is ignored
+ * and won't be replaced. For example:
+ * <pre>
+ *   The variable $${${name}} must be used.
+ * </pre>
+ * <p>
+ * In some complex scenarios you might even want to perform substitution in the
+ * names of variables, for instance
+ * <pre>
+ * ${jre-${java.specification.version}}
+ * </pre>
+ * <code>StrSubstitutor</code> supports this recursive substitution in variable
+ * names, but it has to be enabled explicitly by setting the
+ * {@link #setEnableSubstitutionInVariables(boolean) enableSubstitutionInVariables}
+ * property to <b>true</b>.
+ * <p>This class is <b>not</b> thread safe.</p>
+ */
+public class StrSubstitutor {
+
+    /**
+     * Constant for the default escape character.
+     */
+    public static final char DEFAULT_ESCAPE = '$';
+    /**
+     * Constant for the default variable prefix.
+     */
+    public static final StrMatcher DEFAULT_PREFIX = StrMatcher.stringMatcher("${");
+    /**
+     * Constant for the default variable suffix.
+     */
+    public static final StrMatcher DEFAULT_SUFFIX = StrMatcher.stringMatcher("}");
+    /**
+     * Constant for the default value delimiter of a variable.
+     * @since 3.2
+     */
+    public static final StrMatcher DEFAULT_VALUE_DELIMITER = StrMatcher.stringMatcher(":-");
+
+    /**
+     * Stores the escape character.
+     */
+    private char escapeChar;
+    /**
+     * Stores the variable prefix.
+     */
+    private StrMatcher prefixMatcher;
+    /**
+     * Stores the variable suffix.
+     */
+    private StrMatcher suffixMatcher;
+    /**
+     * Stores the default variable value delimiter
+     */
+    private StrMatcher valueDelimiterMatcher;
+    /**
+     * Variable resolution is delegated to an implementor of VariableResolver.
+     */
+    private StrLookup<?> variableResolver;
+    /**
+     * The flag whether substitution in variable names is enabled.
+     */
+    private boolean enableSubstitutionInVariables;
+    /**
+     * Whether escapes should be preserved.  Default is false;
+     */
+    private boolean preserveEscapes = false;
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables in the given source object with
+     * their matching values from the map.
+     *
+     * @param <V> the type of the values in the map
+     * @param source  the source text containing the variables to substitute, null returns null
+     * @param valueMap  the map with the values, may be null
+     * @return the result of the replace operation
+     */
+    public static <V> String replace(final Object source, final Map<String, V> valueMap) {
+        return new StrSubstitutor(valueMap).replace(source);
+    }
+
+    /**
+     * Replaces all the occurrences of variables in the given source object with
+     * their matching values from the map. This method allows to specifiy a
+     * custom variable prefix and suffix
+     *
+     * @param <V> the type of the values in the map
+     * @param source  the source text containing the variables to substitute, null returns null
+     * @param valueMap  the map with the values, may be null
+     * @param prefix  the prefix of variables, not null
+     * @param suffix  the suffix of variables, not null
+     * @return the result of the replace operation
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public static <V> String replace(final Object source, final Map<String, V> valueMap, final String prefix, final String suffix) {
+        return new StrSubstitutor(valueMap, prefix, suffix).replace(source);
+    }
+
+    /**
+     * Replaces all the occurrences of variables in the given source object with their matching
+     * values from the properties.
+     *
+     * @param source the source text containing the variables to substitute, null returns null
+     * @param valueProperties the properties with values, may be null
+     * @return the result of the replace operation
+     */
+    public static String replace(final Object source, final Properties valueProperties) {
+        if (valueProperties == null) {
+            return source.toString();
+        }
+        final Map<String,String> valueMap = new HashMap<>();
+        final Enumeration<?> propNames = valueProperties.propertyNames();
+        while (propNames.hasMoreElements()) {
+            final String propName = (String)propNames.nextElement();
+            final String propValue = valueProperties.getProperty(propName);
+            valueMap.put(propName, propValue);
+        }
+        return StrSubstitutor.replace(source, valueMap);
+    }
+
+    /**
+     * Replaces all the occurrences of variables in the given source object with
+     * their matching values from the system properties.
+     *
+     * @param source  the source text containing the variables to substitute, null returns null
+     * @return the result of the replace operation
+     */
+    public static String replaceSystemProperties(final Object source) {
+        return new StrSubstitutor(StrLookup.systemPropertiesLookup()).replace(source);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Creates a new instance with defaults for variable prefix and suffix
+     * and the escaping character.
+     */
+    public StrSubstitutor() {
+        this((StrLookup<?>) null, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
+    }
+
+    /**
+     * Creates a new instance and initializes it. Uses defaults for variable
+     * prefix and suffix and the escaping character.
+     *
+     * @param <V> the type of the values in the map
+     * @param valueMap  the map with the variables' values, may be null
+     */
+    public <V> StrSubstitutor(final Map<String, V> valueMap) {
+        this(StrLookup.mapLookup(valueMap), DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
+    }
+
+    /**
+     * Creates a new instance and initializes it. Uses a default escaping character.
+     *
+     * @param <V> the type of the values in the map
+     * @param valueMap  the map with the variables' values, may be null
+     * @param prefix  the prefix for variables, not null
+     * @param suffix  the suffix for variables, not null
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix) {
+        this(StrLookup.mapLookup(valueMap), prefix, suffix, DEFAULT_ESCAPE);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param <V> the type of the values in the map
+     * @param valueMap  the map with the variables' values, may be null
+     * @param prefix  the prefix for variables, not null
+     * @param suffix  the suffix for variables, not null
+     * @param escape  the escape character
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
+                              final char escape) {
+        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param <V> the type of the values in the map
+     * @param valueMap  the map with the variables' values, may be null
+     * @param prefix  the prefix for variables, not null
+     * @param suffix  the suffix for variables, not null
+     * @param escape  the escape character
+     * @param valueDelimiter  the variable default value delimiter, may be null
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     * @since 3.2
+     */
+    public <V> StrSubstitutor(final Map<String, V> valueMap, final String prefix, final String suffix,
+                              final char escape, final String valueDelimiter) {
+        this(StrLookup.mapLookup(valueMap), prefix, suffix, escape, valueDelimiter);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     */
+    public StrSubstitutor(final StrLookup<?> variableResolver) {
+        this(variableResolver, DEFAULT_PREFIX, DEFAULT_SUFFIX, DEFAULT_ESCAPE);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     * @param prefix  the prefix for variables, not null
+     * @param suffix  the suffix for variables, not null
+     * @param escape  the escape character
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
+                          final char escape) {
+        this.setVariableResolver(variableResolver);
+        this.setVariablePrefix(prefix);
+        this.setVariableSuffix(suffix);
+        this.setEscapeChar(escape);
+        this.setValueDelimiterMatcher(DEFAULT_VALUE_DELIMITER);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     * @param prefix  the prefix for variables, not null
+     * @param suffix  the suffix for variables, not null
+     * @param escape  the escape character
+     * @param valueDelimiter  the variable default value delimiter string, may be null
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     * @since 3.2
+     */
+    public StrSubstitutor(final StrLookup<?> variableResolver, final String prefix, final String suffix,
+                          final char escape, final String valueDelimiter) {
+        this.setVariableResolver(variableResolver);
+        this.setVariablePrefix(prefix);
+        this.setVariableSuffix(suffix);
+        this.setEscapeChar(escape);
+        this.setValueDelimiter(valueDelimiter);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     * @param prefixMatcher  the prefix for variables, not null
+     * @param suffixMatcher  the suffix for variables, not null
+     * @param escape  the escape character
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     */
+    public StrSubstitutor(
+            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
+            final char escape) {
+        this(variableResolver, prefixMatcher, suffixMatcher, escape, DEFAULT_VALUE_DELIMITER);
+    }
+
+    /**
+     * Creates a new instance and initializes it.
+     *
+     * @param variableResolver  the variable resolver, may be null
+     * @param prefixMatcher  the prefix for variables, not null
+     * @param suffixMatcher  the suffix for variables, not null
+     * @param escape  the escape character
+     * @param valueDelimiterMatcher  the variable default value delimiter matcher, may be null
+     * @throws IllegalArgumentException if the prefix or suffix is null
+     * @since 3.2
+     */
+    public StrSubstitutor(
+            final StrLookup<?> variableResolver, final StrMatcher prefixMatcher, final StrMatcher suffixMatcher,
+            final char escape, final StrMatcher valueDelimiterMatcher) {
+        this.setVariableResolver(variableResolver);
+        this.setVariablePrefixMatcher(prefixMatcher);
+        this.setVariableSuffixMatcher(suffixMatcher);
+        this.setEscapeChar(escape);
+        this.setValueDelimiterMatcher(valueDelimiterMatcher);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source string as a template.
+     *
+     * @param source  the string to replace in, null returns null
+     * @return the result of the replace operation
+     */
+    public String replace(final String source) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(source);
+        if (substitute(buf, 0, source.length()) == false) {
+            return source;
+        }
+        return buf.toString();
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source string as a template.
+     * <p>
+     * Only the specified portion of the string will be processed.
+     * The rest of the string is not processed, and is not returned.
+     *
+     * @param source  the string to replace in, null returns null
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the array to be processed, must be valid
+     * @return the result of the replace operation
+     */
+    public String replace(final String source, final int offset, final int length) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        if (substitute(buf, 0, length) == false) {
+            return source.substring(offset, offset + length);
+        }
+        return buf.toString();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source array as a template.
+     * The array is not altered by this method.
+     *
+     * @param source  the character array to replace in, not altered, null returns null
+     * @return the result of the replace operation
+     */
+    public String replace(final char[] source) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(source.length).append(source);
+        substitute(buf, 0, source.length);
+        return buf.toString();
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source array as a template.
+     * The array is not altered by this method.
+     * <p>
+     * Only the specified portion of the array will be processed.
+     * The rest of the array is not processed, and is not returned.
+     *
+     * @param source  the character array to replace in, not altered, null returns null
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the array to be processed, must be valid
+     * @return the result of the replace operation
+     */
+    public String replace(final char[] source, final int offset, final int length) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        substitute(buf, 0, length);
+        return buf.toString();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source buffer as a template.
+     * The buffer is not altered by this method.
+     *
+     * @param source  the buffer to use as a template, not changed, null returns null
+     * @return the result of the replace operation
+     */
+    public String replace(final StringBuffer source) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(source.length()).append(source);
+        substitute(buf, 0, buf.length());
+        return buf.toString();
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source buffer as a template.
+     * The buffer is not altered by this method.
+     * <p>
+     * Only the specified portion of the buffer will be processed.
+     * The rest of the buffer is not processed, and is not returned.
+     *
+     * @param source  the buffer to use as a template, not changed, null returns null
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the array to be processed, must be valid
+     * @return the result of the replace operation
+     */
+    public String replace(final StringBuffer source, final int offset, final int length) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        substitute(buf, 0, length);
+        return buf.toString();
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source as a template.
+     * The source is not altered by this method.
+     *
+     * @param source  the buffer to use as a template, not changed, null returns null
+     * @return the result of the replace operation
+     * @since 3.2
+     */
+    public String replace(final CharSequence source) {
+        if (source == null) {
+            return null;
+        }
+        return replace(source, 0, source.length());
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source as a template.
+     * The source is not altered by this method.
+     * <p>
+     * Only the specified portion of the buffer will be processed.
+     * The rest of the buffer is not processed, and is not returned.
+     *
+     * @param source  the buffer to use as a template, not changed, null returns null
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the array to be processed, must be valid
+     * @return the result of the replace operation
+     * @since 3.2
+     */
+    public String replace(final CharSequence source, final int offset, final int length) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        substitute(buf, 0, length);
+        return buf.toString();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source builder as a template.
+     * The builder is not altered by this method.
+     *
+     * @param source  the builder to use as a template, not changed, null returns null
+     * @return the result of the replace operation
+     */
+    public String replace(final StrBuilder source) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(source.length()).append(source);
+        substitute(buf, 0, buf.length());
+        return buf.toString();
+    }
+
+    /**
+     * Replaces all the occurrences of variables with their matching values
+     * from the resolver using the given source builder as a template.
+     * The builder is not altered by this method.
+     * <p>
+     * Only the specified portion of the builder will be processed.
+     * The rest of the builder is not processed, and is not returned.
+     *
+     * @param source  the builder to use as a template, not changed, null returns null
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the array to be processed, must be valid
+     * @return the result of the replace operation
+     */
+    public String replace(final StrBuilder source, final int offset, final int length) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        substitute(buf, 0, length);
+        return buf.toString();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables in the given source object with
+     * their matching values from the resolver. The input source object is
+     * converted to a string using <code>toString</code> and is not altered.
+     *
+     * @param source  the source to replace in, null returns null
+     * @return the result of the replace operation
+     */
+    public String replace(final Object source) {
+        if (source == null) {
+            return null;
+        }
+        final StrBuilder buf = new StrBuilder().append(source);
+        substitute(buf, 0, buf.length());
+        return buf.toString();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables within the given source buffer
+     * with their matching values from the resolver.
+     * The buffer is updated with the result.
+     *
+     * @param source  the buffer to replace in, updated, null returns zero
+     * @return true if altered
+     */
+    public boolean replaceIn(final StringBuffer source) {
+        if (source == null) {
+            return false;
+        }
+        return replaceIn(source, 0, source.length());
+    }
+
+    /**
+     * Replaces all the occurrences of variables within the given source buffer
+     * with their matching values from the resolver.
+     * The buffer is updated with the result.
+     * <p>
+     * Only the specified portion of the buffer will be processed.
+     * The rest of the buffer is not processed, but it is not deleted.
+     *
+     * @param source  the buffer to replace in, updated, null returns zero
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the buffer to be processed, must be valid
+     * @return true if altered
+     */
+    public boolean replaceIn(final StringBuffer source, final int offset, final int length) {
+        if (source == null) {
+            return false;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        if (substitute(buf, 0, length) == false) {
+            return false;
+        }
+        source.replace(offset, offset + length, buf.toString());
+        return true;
+    }
+
+  //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables within the given source buffer
+     * with their matching values from the resolver.
+     * The buffer is updated with the result.
+     *
+     * @param source  the buffer to replace in, updated, null returns zero
+     * @return true if altered
+     * @since 3.2
+     */
+    public boolean replaceIn(final StringBuilder source) {
+        if (source == null) {
+            return false;
+        }
+        return replaceIn(source, 0, source.length());
+    }
+
+    /**
+     * Replaces all the occurrences of variables within the given source builder
+     * with their matching values from the resolver.
+     * The builder is updated with the result.
+     * <p>
+     * Only the specified portion of the buffer will be processed.
+     * The rest of the buffer is not processed, but it is not deleted.
+     *
+     * @param source  the buffer to replace in, updated, null returns zero
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the buffer to be processed, must be valid
+     * @return true if altered
+     * @since 3.2
+     */
+    public boolean replaceIn(final StringBuilder source, final int offset, final int length) {
+        if (source == null) {
+            return false;
+        }
+        final StrBuilder buf = new StrBuilder(length).append(source, offset, length);
+        if (substitute(buf, 0, length) == false) {
+            return false;
+        }
+        source.replace(offset, offset + length, buf.toString());
+        return true;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Replaces all the occurrences of variables within the given source
+     * builder with their matching values from the resolver.
+     *
+     * @param source  the builder to replace in, updated, null returns zero
+     * @return true if altered
+     */
+    public boolean replaceIn(final StrBuilder source) {
+        if (source == null) {
+            return false;
+        }
+        return substitute(source, 0, source.length());
+    }
+
+    /**
+     * Replaces all the occurrences of variables within the given source
+     * builder with their matching values from the resolver.
+     * <p>
+     * Only the specified portion of the builder will be processed.
+     * The rest of the builder is not processed, but it is not deleted.
+     *
+     * @param source  the builder to replace in, null returns zero
+     * @param offset  the start offset within the array, must be valid
+     * @param length  the length within the builder to be processed, must be valid
+     * @return true if altered
+     */
+    public boolean replaceIn(final StrBuilder source, final int offset, final int length) {
+        if (source == null) {
+            return false;
+        }
+        return substitute(source, offset, length);
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Internal method that substitutes the variables.
+     * <p>
+     * Most users of this class do not need to call this method. This method will
+     * be called automatically by another (public) method.
+     * <p>
+     * Writers of subclasses can override this method if they need access to
+     * the substitution process at the start or end.
+     *
+     * @param buf  the string builder to substitute into, not null
+     * @param offset  the start offset within the builder, must be valid
+     * @param length  the length within the builder to be processed, must be valid
+     * @return true if altered
+     */
+    protected boolean substitute(final StrBuilder buf, final int offset, final int length) {
+        return substitute(buf, offset, length, null) > 0;
+    }
+
+    /**
+     * Recursive handler for multiple levels of interpolation. This is the main
+     * interpolation method, which resolves the values of all variable references
+     * contained in the passed in text.
+     *
+     * @param buf  the string builder to substitute into, not null
+     * @param offset  the start offset within the builder, must be valid
+     * @param length  the length within the builder to be processed, must be valid
+     * @param priorVariables  the stack keeping track of the replaced variables, may be null
+     * @return the length change that occurs, unless priorVariables is null when the int
+     *  represents a boolean flag as to whether any change occurred.
+     */
+    private int substitute(final StrBuilder buf, final int offset, final int length, List<String> priorVariables) {
+        final StrMatcher pfxMatcher = getVariablePrefixMatcher();
+        final StrMatcher suffMatcher = getVariableSuffixMatcher();
+        final char escape = getEscapeChar();
+        final StrMatcher valueDelimMatcher = getValueDelimiterMatcher();
+        final boolean substitutionInVariablesEnabled = isEnableSubstitutionInVariables();
+
+        final boolean top = priorVariables == null;
+        boolean altered = false;
+        int lengthChange = 0;
+        char[] chars = buf.buffer;
+        int bufEnd = offset + length;
+        int pos = offset;
+        while (pos < bufEnd) {
+            final int startMatchLen = pfxMatcher.isMatch(chars, pos, offset,
+                    bufEnd);
+            if (startMatchLen == 0) {
+                pos++;
+            } else {
+                // found variable start marker
+                if (pos > offset && chars[pos - 1] == escape) {
+                    // escaped
+                    if (preserveEscapes) {
+                        pos++;
+                        continue;
+                    }
+                    buf.deleteCharAt(pos - 1);
+                    chars = buf.buffer; // in case buffer was altered
+                    lengthChange--;
+                    altered = true;
+                    bufEnd--;
+                } else {
+                    // find suffix
+                    final int startPos = pos;
+                    pos += startMatchLen;
+                    int endMatchLen = 0;
+                    int nestedVarCount = 0;
+                    while (pos < bufEnd) {
+                        if (substitutionInVariablesEnabled
+                                && (endMatchLen = pfxMatcher.isMatch(chars,
+                                        pos, offset, bufEnd)) != 0) {
+                            // found a nested variable start
+                            nestedVarCount++;
+                            pos += endMatchLen;
+                            continue;
+                        }
+
+                        endMatchLen = suffMatcher.isMatch(chars, pos, offset,
+                                bufEnd);
+                        if (endMatchLen == 0) {
+                            pos++;
+                        } else {
+                            // found variable end marker
+                            if (nestedVarCount == 0) {
+                                String varNameExpr = new String(chars, startPos
+                                        + startMatchLen, pos - startPos
+                                        - startMatchLen);
+                                if (substitutionInVariablesEnabled) {
+                                    final StrBuilder bufName = new StrBuilder(varNameExpr);
+                                    substitute(bufName, 0, bufName.length());
+                                    varNameExpr = bufName.toString();
+                                }
+                                pos += endMatchLen;
+                                final int endPos = pos;
+
+                                String varName = varNameExpr;
+                                String varDefaultValue = null;
+
+                                if (valueDelimMatcher != null) {
+                                    final char [] varNameExprChars = varNameExpr.toCharArray();
+                                    int valueDelimiterMatchLen = 0;
+                                    for (int i = 0; i < varNameExprChars.length; i++) {
+                                        // if there's any nested variable when nested variable substitution disabled, then stop resolving name and default value.
+                                        if (!substitutionInVariablesEnabled
+                                                && pfxMatcher.isMatch(varNameExprChars, i, i, varNameExprChars.length) != 0) {
+                                            break;
+                                        }
+                                        if ((valueDelimiterMatchLen = valueDelimMatcher.isMatch(varNameExprChars, i)) != 0) {
+                                            varName = varNameExpr.substring(0, i);
+                                            varDefaultValue = varNameExpr.substring(i + valueDelimiterMatchLen);
+                                            break;
+                                        }
+                                    }
+                                }
+
+                                // on the first call initialize priorVariables
+                                if (priorVariables == null) {
+                                    priorVariables = new ArrayList<>();
+                                    priorVariables.add(new String(chars,
+                                            offset, length));
+                                }
+
+                                // handle cyclic substitution
+                                checkCyclicSubstitution(varName, priorVariables);
+                                priorVariables.add(varName);
+
+                                // resolve the variable
+                                String varValue = resolveVariable(varName, buf,
+                                        startPos, endPos);
+                                if (varValue == null) {
+                                    varValue = varDefaultValue;
+                                }
+                                if (varValue != null) {
+                                    // recursive replace
+                                    final int varLen = varValue.length();
+                                    buf.replace(startPos, endPos, varValue);
+                                    altered = true;
+                                    int change = substitute(buf, startPos,
+                                            varLen, priorVariables);
+                                    change = change
+                                            + varLen - (endPos - startPos);
+                                    pos += change;
+                                    bufEnd += change;
+                                    lengthChange += change;
+                                    chars = buf.buffer; // in case buffer was
+                                                        // altered
+                                }
+
+                                // remove variable from the cyclic stack
+                                priorVariables
+                                        .remove(priorVariables.size() - 1);
+                                break;
+                            }
+                            nestedVarCount--;
+                            pos += endMatchLen;
+                        }
+                    }
+                }
+            }
+        }
+        if (top) {
+            return altered ? 1 : 0;
+        }
+        return lengthChange;
+    }
+
+    /**
+     * Checks if the specified variable is already in the stack (list) of variables.
+     *
+     * @param varName  the variable name to check
+     * @param priorVariables  the list of prior variables
+     */
+    private void checkCyclicSubstitution(final String varName, final List<String> priorVariables) {
+        if (priorVariables.contains(varName) == false) {
+            return;
+        }
+        final StrBuilder buf = new StrBuilder(256);
+        buf.append("Infinite loop in property interpolation of ");
+        buf.append(priorVariables.remove(0));
+        buf.append(": ");
+        buf.appendWithSeparators(priorVariables, "->");
+        throw new IllegalStateException(buf.toString());
+    }
+
+    /**
+     * Internal method that resolves the value of a variable.
+     * <p>
+     * Most users of this class do not need to call this method. This method is
+     * called automatically by the substitution process.
+     * <p>
+     * Writers of subclasses can override this method if they need to alter
+     * how each substitution occurs. The method is passed the variable's name
+     * and must return the corresponding value. This implementation uses the
+     * {@link #getVariableResolver()} with the variable's name as the key.
+     *
+     * @param variableName  the name of the variable, not null
+     * @param buf  the buffer where the substitution is occurring, not null
+     * @param startPos  the start position of the variable including the prefix, valid
+     * @param endPos  the end position of the variable including the suffix, valid
+     * @return the variable's value or <b>null</b> if the variable is unknown
+     */
+    protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
+        final StrLookup<?> resolver = getVariableResolver();
+        if (resolver == null) {
+            return null;
+        }
+        return resolver.lookup(variableName);
+    }
+
+    // Escape
+    //-----------------------------------------------------------------------
+    /**
+     * Returns the escape character.
+     *
+     * @return the character used for escaping variable references
+     */
+    public char getEscapeChar() {
+        return this.escapeChar;
+    }
+
+    /**
+     * Sets the escape character.
+     * If this character is placed before a variable reference in the source
+     * text, this variable will be ignored.
+     *
+     * @param escapeCharacter  the escape character (0 for disabling escaping)
+     */
+    public void setEscapeChar(final char escapeCharacter) {
+        this.escapeChar = escapeCharacter;
+    }
+
+    // Prefix
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the variable prefix matcher currently in use.
+     * <p>
+     * The variable prefix is the characer or characters that identify the
+     * start of a variable. This prefix is expressed in terms of a matcher
+     * allowing advanced prefix matches.
+     *
+     * @return the prefix matcher in use
+     */
+    public StrMatcher getVariablePrefixMatcher() {
+        return prefixMatcher;
+    }
+
+    /**
+     * Sets the variable prefix matcher currently in use.
+     * <p>
+     * The variable prefix is the characer or characters that identify the
+     * start of a variable. This prefix is expressed in terms of a matcher
+     * allowing advanced prefix matches.
+     *
+     * @param prefixMatcher  the prefix matcher to use, null ignored
+     * @return this, to enable chaining
+     * @throws IllegalArgumentException if the prefix matcher is null
+     */
+    public StrSubstitutor setVariablePrefixMatcher(final StrMatcher prefixMatcher) {
+        if (prefixMatcher == null) {
+            throw new IllegalArgumentException("Variable prefix matcher must not be null!");
+        }
+        this.prefixMatcher = prefixMatcher;
+        return this;
+    }
+
+    /**
+     * Sets the variable prefix to use.
+     * <p>
+     * The variable prefix is the character or characters that identify the
+     * start of a variable. This method allows a single character prefix to
+     * be easily set.
+     *
+     * @param prefix  the prefix character to use
+     * @return this, to enable chaining
+     */
+    public StrSubstitutor setVariablePrefix(final char prefix) {
+        return setVariablePrefixMatcher(StrMatcher.charMatcher(prefix));
+    }
+
+    /**
+     * Sets the variable prefix to use.
+     * <p>
+     * The variable prefix is the characer or characters that identify the
+     * start of a variable. This method allows a string prefix to be easily set.
+     *
+     * @param prefix  the prefix for variables, not null
+     * @return this, to enable chaining
+     * @throws IllegalArgumentException if the prefix is null
+     */
+    public StrSubstitutor setVariablePrefix(final String prefix) {
+       if (prefix == null) {
+            throw new IllegalArgumentException("Variable prefix must not be null!");
+        }
+        return setVariablePrefixMatcher(StrMatcher.stringMatcher(prefix));
+    }
+
+    // Suffix
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the variable suffix matcher currently in use.
+     * <p>
+     * The variable suffix is the characer or characters that identify the
+     * end of a variable. This suffix is expressed in terms of a matcher
+     * allowing advanced suffix matches.
+     *
+     * @return the suffix matcher in use
+     */
+    public StrMatcher getVariableSuffixMatcher() {
+        return suffixMatcher;
+    }
+
+    /**
+     * Sets the variable suffix matcher currently in use.
+     * <p>
+     * The variable suffix is the characer or characters that identify the
+     * end of a variable. This suffix is expressed in terms of a matcher
+     * allowing advanced suffix matches.
+     *
+     * @param suffixMatcher  the suffix matcher to use, null ignored
+     * @return this, to enable chaining
+     * @throws IllegalArgumentException if the suffix matcher is null
+     */
+    public StrSubstitutor setVariableSuffixMatcher(final StrMatcher suffixMatcher) {
+        if (suffixMatcher == null) {
+            throw new IllegalArgumentException("Variable suffix matcher must not be null!");
+        }
+        this.suffixMatcher = suffixMatcher;
+        return this;
+    }
+
+    /**
+     * Sets the variable suffix to use.
+     * <p>
+     * The variable suffix is the characer or characters that identify the
+     * end of a variable. This method allows a single character suffix to
+     * be easily set.
+     *
+     * @param suffix  the suffix character to use
+     * @return this, to enable chaining
+     */
+    public StrSubstitutor setVariableSuffix(final char suffix) {
+        return setVariableSuffixMatcher(StrMatcher.charMatcher(suffix));
+    }
+
+    /**
+     * Sets the variable suffix to use.
+     * <p>
+     * The variable suffix is the character or characters that identify the
+     * end of a variable. This method allows a string suffix to be easily set.
+     *
+     * @param suffix  the suffix for variables, not null
+     * @return this, to enable chaining
+     * @throws IllegalArgumentException if the suffix is null
+     */
+    public StrSubstitutor setVariableSuffix(final String suffix) {
+       if (suffix == null) {
+            throw new IllegalArgumentException("Variable suffix must not be null!");
+        }
+        return setVariableSuffixMatcher(StrMatcher.stringMatcher(suffix));
+    }
+
+    // Variable Default Value Delimiter
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the variable default value delimiter matcher currently in use.
+     * <p>
+     * The variable default value delimiter is the characer or characters that delimite the
+     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
+     * allowing advanced variable default value delimiter matches.
+     * <p>
+     * If it returns null, then the variable default value resolution is disabled.
+     *
+     * @return the variable default value delimiter matcher in use, may be null
+     * @since 3.2
+     */
+    public StrMatcher getValueDelimiterMatcher() {
+        return valueDelimiterMatcher;
+    }
+
+    /**
+     * Sets the variable default value delimiter matcher to use.
+     * <p>
+     * The variable default value delimiter is the characer or characters that delimite the
+     * variable name and the variable default value. This delimiter is expressed in terms of a matcher
+     * allowing advanced variable default value delimiter matches.
+     * <p>
+     * If the <code>valueDelimiterMatcher</code> is null, then the variable default value resolution
+     * becomes disabled.
+     *
+     * @param valueDelimiterMatcher  variable default value delimiter matcher to use, may be null
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrSubstitutor setValueDelimiterMatcher(final StrMatcher valueDelimiterMatcher) {
+        this.valueDelimiterMatcher = valueDelimiterMatcher;
+        return this;
+    }
+
+    /**
+     * Sets the variable default value delimiter to use.
+     * <p>
+     * The variable default value delimiter is the characer or characters that delimite the
+     * variable name and the variable default value. This method allows a single character
+     * variable default value delimiter to be easily set.
+     *
+     * @param valueDelimiter  the variable default value delimiter character to use
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrSubstitutor setValueDelimiter(final char valueDelimiter) {
+        return setValueDelimiterMatcher(StrMatcher.charMatcher(valueDelimiter));
+    }
+
+    /**
+     * Sets the variable default value delimiter to use.
+     * <p>
+     * The variable default value delimiter is the characer or characters that delimite the
+     * variable name and the variable default value. This method allows a string
+     * variable default value delimiter to be easily set.
+     * <p>
+     * If the <code>valueDelimiter</code> is null or empty string, then the variable default
+     * value resolution becomes disabled.
+     *
+     * @param valueDelimiter  the variable default value delimiter string to use, may be null or empty
+     * @return this, to enable chaining
+     * @since 3.2
+     */
+    public StrSubstitutor setValueDelimiter(final String valueDelimiter) {
+        if (valueDelimiter == null || valueDelimiter.length() == 0) {
+            setValueDelimiterMatcher(null);
+            return this;
+        }
+        return setValueDelimiterMatcher(StrMatcher.stringMatcher(valueDelimiter));
+    }
+
+    // Resolver
+    //-----------------------------------------------------------------------
+    /**
+     * Gets the VariableResolver that is used to lookup variables.
+     *
+     * @return the VariableResolver
+     */
+    public StrLookup<?> getVariableResolver() {
+        return this.variableResolver;
+    }
+
+    /**
+     * Sets the VariableResolver that is used to lookup variables.
+     *
+     * @param variableResolver  the VariableResolver
+     */
+    public void setVariableResolver(final StrLookup<?> variableResolver) {
+        this.variableResolver = variableResolver;
+    }
+
+    // Substitution support in variable names
+    //-----------------------------------------------------------------------
+    /**
+     * Returns a flag whether substitution is done in variable names.
+     *
+     * @return the substitution in variable names flag
+     * @since 3.0
+     */
+    public boolean isEnableSubstitutionInVariables() {
+        return enableSubstitutionInVariables;
+    }
+
+    /**
+     * Sets a flag whether substitution is done in variable names. If set to
+     * <b>true</b>, the names of variables can contain other variables which are
+     * processed first before the original variable is evaluated, e.g.
+     * <code>${jre-${java.version}}</code>. The default value is <b>false</b>.
+     *
+     * @param enableSubstitutionInVariables the new value of the flag
+     * @since 3.0
+     */
+    public void setEnableSubstitutionInVariables(
+            final boolean enableSubstitutionInVariables) {
+        this.enableSubstitutionInVariables = enableSubstitutionInVariables;
+    }
+
+    /**
+     * Returns the flag controlling whether escapes are preserved during
+     * substitution.
+     * 
+     * @return the preserve escape flag
+     * @since 3.5
+     */
+    public boolean isPreserveEscapes() {
+        return preserveEscapes;
+    }
+
+    /**
+     * Sets a flag controlling whether escapes are preserved during
+     * substitution.  If set to <b>true</b>, the escape character is retained
+     * during substitution (e.g. <code>$${this-is-escaped}</code> remains
+     * <code>$${this-is-escaped}</code>).  If set to <b>false</b>, the escape
+     * character is removed during substitution (e.g.
+     * <code>$${this-is-escaped}</code> becomes
+     * <code>${this-is-escaped}</code>).  The default value is <b>false</b>
+     * 
+     * @param preserveEscapes true if escapes are to be preserved
+     * @since 3.5
+     */
+    public void setPreserveEscapes(final boolean preserveEscapes) {
+        this.preserveEscapes = preserveEscapes;
+    }
+}


[13/14] [text] TEXT-23: Resolving merge conflict in changes.xml

Posted by ch...@apache.org.
TEXT-23: Resolving merge conflict in changes.xml


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/56c0a072
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/56c0a072
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/56c0a072

Branch: refs/heads/master
Commit: 56c0a0728f80241d550a070e43fc60ec80d0b806
Parents: 3aa96d4 c8a803c
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 20:46:18 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 20:46:18 2016 -0500

----------------------------------------------------------------------
 src/changes/changes.xml                         |   1 +
 .../similarity/LevenshteinDetailedDistance.java | 513 +++++++++++++++++++
 .../text/similarity/LevenshteinResults.java     | 125 +++++
 .../LevenshteinDetailedDistanceTest.java        | 402 +++++++++++++++
 4 files changed, 1041 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/56c0a072/src/changes/changes.xml
----------------------------------------------------------------------
diff --cc src/changes/changes.xml
index c3d78da,45825e7..e60b364
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@@ -22,7 -22,7 +22,8 @@@
    <body>
  
    <release version="1.0" date="tba" description="tba">
 +    <action issue="TEXT-23" type="add" dev="chtompki">Moving from commons-lang, the package org.apache.commons.lang3.text</action>
+     <action issue="TEXT-10" type="add" dev="kinow" due-to="Don Jeba">A more complex Levenshtein distance</action>
      <action issue="TEXT-24" type="add" dev="chtompki">Add coveralls and Travis.ci integration</action>
      <action issue="TEXT-19" type="add" dev="kinow" due-to="Eyal Allweil">Add alphabet converter</action>
      <action issue="TEXT-13" type="add" dev="kinow">Create Commons Text logo</action>


[02/14] [text] TEXT-23: Adding JavaUnicodeEscaperTest

Posted by ch...@apache.org.
TEXT-23: Adding JavaUnicodeEscaperTest


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/32196b18
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/32196b18
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/32196b18

Branch: refs/heads/master
Commit: 32196b189ac7e4909f8b4dab7ed5a34ef65e532c
Parents: 2f55bd1
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 10:28:59 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 10:28:59 2016 -0500

----------------------------------------------------------------------
 .../text/translate/JavaUnicodeEscaperTest.java  | 65 ++++++++++++++++++++
 1 file changed, 65 insertions(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/32196b18/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java b/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java
new file mode 100644
index 0000000..aab7599
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/translate/JavaUnicodeEscaperTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text.translate;
+
+import org.junit.Test;
+
+import java.io.UnsupportedEncodingException;
+
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Unit tests for {@link org.apache.commons.text.translate.JavaUnicodeEscaper}.
+ */
+public class JavaUnicodeEscaperTest {
+
+    @Test
+    public void testBelow() {
+        final org.apache.commons.text.translate.JavaUnicodeEscaper jue = org.apache.commons.text.translate.JavaUnicodeEscaper.below('F');
+
+        final String input = "ADFGZ";
+        final String result = jue.translate(input);
+        assertEquals("Failed to escape Unicode characters via the below method", "\\u0041\\u0044FGZ", result);
+    }
+
+    @Test
+    public void testBetween() {
+        final org.apache.commons.text.translate.JavaUnicodeEscaper jue = org.apache.commons.text.translate.JavaUnicodeEscaper.between('F', 'L');
+
+        final String input = "ADFGZ";
+        final String result = jue.translate(input);
+        assertEquals("Failed to escape Unicode characters via the between method", "AD\\u0046\\u0047Z", result);
+    }
+
+    @Test
+    public void testAbove() {
+        final org.apache.commons.text.translate.JavaUnicodeEscaper jue = JavaUnicodeEscaper.above('F');
+
+        final String input = "ADFGZ";
+        final String result = jue.translate(input);
+        assertEquals("Failed to escape Unicode characters via the above method", "ADF\\u0047\\u005A", result);
+    }
+
+    @Test
+    public void testToUtf16Escape() throws UnsupportedEncodingException {
+        final org.apache.commons.text.translate.JavaUnicodeEscaper jue = org.apache.commons.text.translate.JavaUnicodeEscaper.below('F');
+        // According to https://en.wikipedia.org/wiki/UTF-16#Code_points_U.2B10000..U.2B10FFFF,
+        // Character ?,	U+24B62,	Binary Code Point 0010 0100 1011 0110 0010,	Binary UTF-167 1101 1000 0101 0010 1101 1111 0110 0010, UTF-16 Hex Code Units D852 DF62
+        String encoding = jue.toUtf16Escape(Integer.parseInt("024B62", 16));
+        assertEquals("\\uD852\\uDF62",encoding);
+    }
+}


[12/14] [text] TEXT-23: adding documentation to changes.xml

Posted by ch...@apache.org.
TEXT-23: adding documentation to changes.xml


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/3aa96d44
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/3aa96d44
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/3aa96d44

Branch: refs/heads/master
Commit: 3aa96d4485cee6726592b2a50bb94de4aeb35f22
Parents: 7adf1d5
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 20:07:26 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 20:07:26 2016 -0500

----------------------------------------------------------------------
 src/changes/changes.xml | 1 +
 1 file changed, 1 insertion(+)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/3aa96d44/src/changes/changes.xml
----------------------------------------------------------------------
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 05f0bb6..c3d78da 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -22,6 +22,7 @@
   <body>
 
   <release version="1.0" date="tba" description="tba">
+    <action issue="TEXT-23" type="add" dev="chtompki">Moving from commons-lang, the package org.apache.commons.lang3.text</action>
     <action issue="TEXT-24" type="add" dev="chtompki">Add coveralls and Travis.ci integration</action>
     <action issue="TEXT-19" type="add" dev="kinow" due-to="Eyal Allweil">Add alphabet converter</action>
     <action issue="TEXT-13" type="add" dev="kinow">Create Commons Text logo</action>


[06/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java b/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java
new file mode 100644
index 0000000..6b91643
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/ExtendedMessageFormatTest.java
@@ -0,0 +1,484 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.junit.Test;
+import org.junit.Before;
+import static org.junit.Assert.*;
+import static org.apache.commons.lang3.JavaVersion.JAVA_1_4;
+
+import java.text.ChoiceFormat;
+import java.text.DateFormat;
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.MessageFormat;
+import java.text.NumberFormat;
+import java.text.ParsePosition;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Map;
+
+import org.apache.commons.lang3.SystemUtils;
+
+/**
+ * Test case for {@link ExtendedMessageFormat}.
+ *
+ * @since 2.4
+ */
+public class ExtendedMessageFormatTest {
+
+    private final Map<String, FormatFactory> registry = new HashMap<>();
+
+    @Before
+    public void setUp() throws Exception {
+        registry.put("lower", new LowerCaseFormatFactory());
+        registry.put("upper", new UpperCaseFormatFactory());
+    }
+
+    /**
+     * Test extended formats.
+     */
+    @Test
+    public void testExtendedFormats() {
+        final String pattern = "Lower: {0,lower} Upper: {1,upper}";
+        final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
+        assertPatternsEqual("TOPATTERN", pattern, emf.toPattern());
+        assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"foo", "bar"}));
+        assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"Foo", "Bar"}));
+        assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"FOO", "BAR"}));
+        assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"FOO", "bar"}));
+        assertEquals("Lower: foo Upper: BAR", emf.format(new Object[] {"foo", "BAR"}));
+    }
+
+    /**
+     * Test Bug LANG-477 - out of memory error with escaped quote
+     */
+    @Test
+    public void testEscapedQuote_LANG_477() {
+        final String pattern = "it''s a {0,lower} 'test'!";
+        final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
+        assertEquals("it's a dummy test!", emf.format(new Object[] {"DUMMY"}));
+    }
+
+    /**
+     * Test Bug LANG-917 - IndexOutOfBoundsException and/or infinite loop when using a choice pattern
+     */
+    @Test
+    public void testEmbeddedPatternInChoice() {
+        final String pattern = "Hi {0,lower}, got {1,choice,0#none|1#one|1<{1,number}}, {2,upper}!";
+        final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
+        assertEquals(emf.format(new Object[] {"there", 3, "great"}), "Hi there, got 3, GREAT!");
+    }
+
+    /**
+     * Test Bug LANG-948 - Exception while using ExtendedMessageFormat and escaping braces
+     */
+    @Test
+    public void testEscapedBraces_LANG_948() {
+        // message without placeholder because braces are escaped by quotes 
+        final String pattern = "Message without placeholders '{}'";
+        final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
+        assertEquals("Message without placeholders {}", emf.format(new Object[] {"DUMMY"}));
+
+        // message with placeholder because quotes are escaped by quotes 
+        final String pattern2 = "Message with placeholder ''{0}''";
+        final ExtendedMessageFormat emf2 = new ExtendedMessageFormat(pattern2, registry);
+        assertEquals("Message with placeholder 'DUMMY'", emf2.format(new Object[] {"DUMMY"}));
+    }
+
+    /**
+     * Test extended and built in formats.
+     */
+    @Test
+    public void testExtendedAndBuiltInFormats() {
+        final Calendar cal = Calendar.getInstance();
+        cal.set(2007, Calendar.JANUARY, 23, 18, 33, 05);
+        final Object[] args = new Object[] {"John Doe", cal.getTime(), Double.valueOf("12345.67")};
+        final String builtinsPattern = "DOB: {1,date,short} Salary: {2,number,currency}";
+        final String extendedPattern = "Name: {0,upper} ";
+        final String pattern = extendedPattern + builtinsPattern;
+
+        final HashSet<Locale> testLocales = new HashSet<>();
+        testLocales.addAll(Arrays.asList(DateFormat.getAvailableLocales()));
+        testLocales.retainAll(Arrays.asList(NumberFormat.getAvailableLocales()));
+        testLocales.add(null);
+
+        for (final Locale locale : testLocales) {
+            final MessageFormat builtins = createMessageFormat(builtinsPattern, locale);
+            final String expectedPattern = extendedPattern + builtins.toPattern();
+            DateFormat df = null;
+            NumberFormat nf = null;
+            ExtendedMessageFormat emf = null;
+            if (locale == null) {
+                df = DateFormat.getDateInstance(DateFormat.SHORT);
+                nf = NumberFormat.getCurrencyInstance();
+                emf = new ExtendedMessageFormat(pattern, registry);
+            } else {
+                df = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+                nf = NumberFormat.getCurrencyInstance(locale);
+                emf = new ExtendedMessageFormat(pattern, locale, registry);
+            }
+            final StringBuilder expected = new StringBuilder();
+            expected.append("Name: ");
+            expected.append(args[0].toString().toUpperCase());
+            expected.append(" DOB: ");
+            expected.append(df.format(args[1]));
+            expected.append(" Salary: ");
+            expected.append(nf.format(args[2]));
+            assertPatternsEqual("pattern comparison for locale " + locale, expectedPattern, emf.toPattern());
+            assertEquals(String.valueOf(locale), expected.toString(), emf.format(args));
+        }
+    }
+
+//    /**
+//     * Test extended formats with choice format.
+//     *
+//     * NOTE: FAILING - currently sub-formats not supported
+//     */
+//    public void testExtendedWithChoiceFormat() {
+//        String pattern = "Choice: {0,choice,1.0#{1,lower}|2.0#{1,upper}}";
+//        ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, registry);
+//        assertPatterns(null, pattern, emf.toPattern());
+//        try {
+//            assertEquals("one", emf.format(new Object[] {Integer.valueOf(1), "ONE"}));
+//            assertEquals("TWO", emf.format(new Object[] {Integer.valueOf(2), "two"}));
+//        } catch (IllegalArgumentException e) {
+//            // currently sub-formats not supported
+//        }
+//    }
+
+//    /**
+//     * Test mixed extended and built-in formats with choice format.
+//     *
+//     * NOTE: FAILING - currently sub-formats not supported
+//     */
+//    public void testExtendedAndBuiltInWithChoiceFormat() {
+//        String pattern = "Choice: {0,choice,1.0#{0} {1,lower} {2,number}|2.0#{0} {1,upper} {2,number,currency}}";
+//        Object[] lowArgs  = new Object[] {Integer.valueOf(1), "Low",  Double.valueOf("1234.56")};
+//        Object[] highArgs = new Object[] {Integer.valueOf(2), "High", Double.valueOf("9876.54")};
+//        Locale[] availableLocales = ChoiceFormat.getAvailableLocales();
+//        Locale[] testLocales = new Locale[availableLocales.length + 1];
+//        testLocales[0] = null;
+//        System.arraycopy(availableLocales, 0, testLocales, 1, availableLocales.length);
+//        for (int i = 0; i < testLocales.length; i++) {
+//            NumberFormat nf = null;
+//            NumberFormat cf = null;
+//            ExtendedMessageFormat emf = null;
+//            if (testLocales[i] == null) {
+//                nf = NumberFormat.getNumberInstance();
+//                cf = NumberFormat.getCurrencyInstance();
+//                emf = new ExtendedMessageFormat(pattern, registry);
+//            } else {
+//                nf = NumberFormat.getNumberInstance(testLocales[i]);
+//                cf = NumberFormat.getCurrencyInstance(testLocales[i]);
+//                emf = new ExtendedMessageFormat(pattern, testLocales[i], registry);
+//            }
+//            assertPatterns(null, pattern, emf.toPattern());
+//            try {
+//                String lowExpected = lowArgs[0] + " low "    + nf.format(lowArgs[2]);
+//                String highExpected = highArgs[0] + " HIGH "  + cf.format(highArgs[2]);
+//                assertEquals(lowExpected,  emf.format(lowArgs));
+//                assertEquals(highExpected, emf.format(highArgs));
+//            } catch (IllegalArgumentException e) {
+//                // currently sub-formats not supported
+//            }
+//        }
+//    }
+
+    /**
+     * Test the built in choice format.
+     */
+    @Test
+    public void testBuiltInChoiceFormat() {
+        final Object[] values = new Number[] {Integer.valueOf(1), Double.valueOf("2.2"), Double.valueOf("1234.5")};
+        String choicePattern = null;
+        final Locale[] availableLocales = ChoiceFormat.getAvailableLocales();
+
+        choicePattern = "{0,choice,1#One|2#Two|3#Many {0,number}}";
+        for (final Object value : values) {
+            checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales);
+        }
+
+        choicePattern = "{0,choice,1#''One''|2#\"Two\"|3#''{Many}'' {0,number}}";
+        for (final Object value : values) {
+            checkBuiltInFormat(value + ": " + choicePattern, new Object[] {value}, availableLocales);
+        }
+    }
+
+    /**
+     * Test the built in date/time formats
+     */
+    @Test
+    public void testBuiltInDateTimeFormat() {
+        final Calendar cal = Calendar.getInstance();
+        cal.set(2007, Calendar.JANUARY, 23, 18, 33, 05);
+        final Object[] args = new Object[] {cal.getTime()};
+        final Locale[] availableLocales = DateFormat.getAvailableLocales();
+
+        checkBuiltInFormat("1: {0,date,short}",    args, availableLocales);
+        checkBuiltInFormat("2: {0,date,medium}",   args, availableLocales);
+        checkBuiltInFormat("3: {0,date,long}",     args, availableLocales);
+        checkBuiltInFormat("4: {0,date,full}",     args, availableLocales);
+        checkBuiltInFormat("5: {0,date,d MMM yy}", args, availableLocales);
+        checkBuiltInFormat("6: {0,time,short}",    args, availableLocales);
+        checkBuiltInFormat("7: {0,time,medium}",   args, availableLocales);
+        checkBuiltInFormat("8: {0,time,long}",     args, availableLocales);
+        checkBuiltInFormat("9: {0,time,full}",     args, availableLocales);
+        checkBuiltInFormat("10: {0,time,HH:mm}",   args, availableLocales);
+        checkBuiltInFormat("11: {0,date}",         args, availableLocales);
+        checkBuiltInFormat("12: {0,time}",         args, availableLocales);
+    }
+
+    @Test
+    public void testOverriddenBuiltinFormat() {
+        final Calendar cal = Calendar.getInstance();
+        cal.set(2007, Calendar.JANUARY, 23);
+        final Object[] args = new Object[] {cal.getTime()};
+        final Locale[] availableLocales = DateFormat.getAvailableLocales();
+        final Map<String, ? extends FormatFactory> dateRegistry = Collections.singletonMap("date", new OverrideShortDateFormatFactory());
+
+        //check the non-overridden builtins:
+        checkBuiltInFormat("1: {0,date}", dateRegistry,          args, availableLocales);
+        checkBuiltInFormat("2: {0,date,medium}", dateRegistry,   args, availableLocales);
+        checkBuiltInFormat("3: {0,date,long}", dateRegistry,     args, availableLocales);
+        checkBuiltInFormat("4: {0,date,full}", dateRegistry,     args, availableLocales);
+        checkBuiltInFormat("5: {0,date,d MMM yy}", dateRegistry, args, availableLocales);
+
+        //check the overridden format:
+        for (int i = -1; i < availableLocales.length; i++) {
+            final Locale locale = i < 0 ? null : availableLocales[i];
+            final MessageFormat dateDefault = createMessageFormat("{0,date}", locale);
+            final String pattern = "{0,date,short}";
+            final ExtendedMessageFormat dateShort = new ExtendedMessageFormat(pattern, locale, dateRegistry);
+            assertEquals("overridden date,short format", dateDefault.format(args), dateShort.format(args));
+            assertEquals("overridden date,short pattern", pattern, dateShort.toPattern());
+        }
+    }
+
+    /**
+     * Test the built in number formats.
+     */
+    @Test
+    public void testBuiltInNumberFormat() {
+        final Object[] args = new Object[] {Double.valueOf("6543.21")};
+        final Locale[] availableLocales = NumberFormat.getAvailableLocales();
+        checkBuiltInFormat("1: {0,number}",            args, availableLocales);
+        checkBuiltInFormat("2: {0,number,integer}",    args, availableLocales);
+        checkBuiltInFormat("3: {0,number,currency}",   args, availableLocales);
+        checkBuiltInFormat("4: {0,number,percent}",    args, availableLocales);
+        checkBuiltInFormat("5: {0,number,00000.000}",  args, availableLocales);
+    }
+
+    /**
+     * Test equals() and hashcode.
+     */
+    @Test
+    public void testEqualsHashcode() {
+        final Map<String, ? extends FormatFactory> fmtRegistry = Collections.singletonMap("testfmt", new LowerCaseFormatFactory());
+        final Map<String, ? extends FormatFactory> otherRegitry = Collections.singletonMap("testfmt", new UpperCaseFormatFactory());
+
+        final String pattern = "Pattern: {0,testfmt}";
+        final ExtendedMessageFormat emf = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
+
+        ExtendedMessageFormat other = null;
+
+        // Same object
+        assertTrue("same, equals()",   emf.equals(emf));
+        assertTrue("same, hashcode()", emf.hashCode() == emf.hashCode());
+
+        // Equal Object
+        other = new ExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
+        assertTrue("equal, equals()",   emf.equals(other));
+        assertTrue("equal, hashcode()", emf.hashCode() == other.hashCode());
+
+        // Different Class
+        other = new OtherExtendedMessageFormat(pattern, Locale.US, fmtRegistry);
+        assertFalse("class, equals()",  emf.equals(other));
+        assertTrue("class, hashcode()", emf.hashCode() == other.hashCode()); // same hashcode
+        
+        // Different pattern
+        other = new ExtendedMessageFormat("X" + pattern, Locale.US, fmtRegistry);
+        assertFalse("pattern, equals()",   emf.equals(other));
+        assertFalse("pattern, hashcode()", emf.hashCode() == other.hashCode());
+
+        // Different registry
+        other = new ExtendedMessageFormat(pattern, Locale.US, otherRegitry);
+        assertFalse("registry, equals()",   emf.equals(other));
+        assertFalse("registry, hashcode()", emf.hashCode() == other.hashCode());
+
+        // Different Locale
+        other = new ExtendedMessageFormat(pattern, Locale.FRANCE, fmtRegistry);
+        assertFalse("locale, equals()",  emf.equals(other));
+        assertTrue("locale, hashcode()", emf.hashCode() == other.hashCode()); // same hashcode
+    }
+
+    /**
+     * Test a built in format for the specified Locales, plus <code>null</code> Locale.
+     * @param pattern MessageFormat pattern
+     * @param args MessageFormat arguments
+     * @param locales to test
+     */
+    private void checkBuiltInFormat(final String pattern, final Object[] args, final Locale[] locales) {
+        checkBuiltInFormat(pattern, null, args, locales);
+    }
+
+    /**
+     * Test a built in format for the specified Locales, plus <code>null</code> Locale.
+     * @param pattern MessageFormat pattern
+     * @param fmtRegistry FormatFactory registry to use
+     * @param args MessageFormat arguments
+     * @param locales to test
+     */
+    private void checkBuiltInFormat(final String pattern, final Map<String, ?> fmtRegistry, final Object[] args, final Locale[] locales) {
+        checkBuiltInFormat(pattern, fmtRegistry, args, (Locale) null);
+        for (final Locale locale : locales) {
+            checkBuiltInFormat(pattern, fmtRegistry, args, locale);
+        }
+    }
+
+    /**
+     * Create an ExtendedMessageFormat for the specified pattern and locale and check the
+     * formated output matches the expected result for the parameters.
+     * @param pattern string
+     * @param registryUnused map (currently unused)
+     * @param args Object[]
+     * @param locale Locale
+     */
+    private void checkBuiltInFormat(final String pattern, final Map<String, ?> registryUnused, final Object[] args, final Locale locale) {
+        final StringBuilder buffer = new StringBuilder();
+        buffer.append("Pattern=[");
+        buffer.append(pattern);
+        buffer.append("], locale=[");
+        buffer.append(locale);
+        buffer.append("]");
+        final MessageFormat mf = createMessageFormat(pattern, locale);
+        // System.out.println(buffer + ", result=[" + mf.format(args) +"]");
+        ExtendedMessageFormat emf = null;
+        if (locale == null) {
+            emf = new ExtendedMessageFormat(pattern);
+        } else {
+            emf = new ExtendedMessageFormat(pattern, locale);
+        }
+        assertEquals("format "    + buffer.toString(), mf.format(args), emf.format(args));
+        assertPatternsEqual("toPattern " + buffer.toString(), mf.toPattern(),  emf.toPattern());
+    }
+
+    //can't trust what MessageFormat does with toPattern() pre 1.4:
+    private void assertPatternsEqual(final String message, final String expected, final String actual) {
+        if (SystemUtils.isJavaVersionAtLeast(JAVA_1_4)) {
+            assertEquals(message, expected, actual);
+        }
+    }
+
+    /**
+     * Replace MessageFormat(String, Locale) constructor (not available until JDK 1.4).
+     * @param pattern string
+     * @param locale Locale
+     * @return MessageFormat
+     */
+    private MessageFormat createMessageFormat(final String pattern, final Locale locale) {
+        final MessageFormat result = new MessageFormat(pattern);
+        if (locale != null) {
+            result.setLocale(locale);
+            result.applyPattern(pattern);
+        }
+        return result;
+    }
+
+    // ------------------------ Test Formats ------------------------
+
+    /**
+     * {@link Format} implementation which converts to lower case.
+     */
+    private static class LowerCaseFormat extends Format {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
+            return toAppendTo.append(((String)obj).toLowerCase());
+        }
+        @Override
+        public Object parseObject(final String source, final ParsePosition pos) {throw new UnsupportedOperationException();}
+    }
+
+    /**
+     * {@link Format} implementation which converts to upper case.
+     */
+    private static class UpperCaseFormat extends Format {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public StringBuffer format(final Object obj, final StringBuffer toAppendTo, final FieldPosition pos) {
+            return toAppendTo.append(((String)obj).toUpperCase());
+        }
+        @Override
+        public Object parseObject(final String source, final ParsePosition pos) {throw new UnsupportedOperationException();}
+    }
+
+
+    // ------------------------ Test Format Factories ---------------
+    /**
+     * {@link FormatFactory} implementation for lower case format.
+     */
+    private static class LowerCaseFormatFactory implements FormatFactory {
+        private static final Format LOWER_INSTANCE = new LowerCaseFormat();
+        @Override
+        public Format getFormat(final String name, final String arguments, final Locale locale) {
+            return LOWER_INSTANCE;
+        }
+    }
+    /**
+     * {@link FormatFactory} implementation for upper case format.
+     */
+    private static class UpperCaseFormatFactory implements FormatFactory {
+        private static final Format UPPER_INSTANCE = new UpperCaseFormat();
+        @Override
+        public Format getFormat(final String name, final String arguments, final Locale locale) {
+            return UPPER_INSTANCE;
+        }
+    }
+    /**
+     * {@link FormatFactory} implementation to override date format "short" to "default".
+     */
+    private static class OverrideShortDateFormatFactory implements FormatFactory {
+        @Override
+        public Format getFormat(final String name, final String arguments, final Locale locale) {
+            return !"short".equals(arguments) ? null
+                    : locale == null ? DateFormat
+                            .getDateInstance(DateFormat.DEFAULT) : DateFormat
+                            .getDateInstance(DateFormat.DEFAULT, locale);
+        }
+    }
+
+    /**
+     * Alternative ExtendedMessageFormat impl.
+     */
+    private static class OtherExtendedMessageFormat extends ExtendedMessageFormat {
+        private static final long serialVersionUID = 1L;
+
+        public OtherExtendedMessageFormat(final String pattern, final Locale locale,
+                final Map<String, ? extends FormatFactory> registry) {
+            super(pattern, locale, registry);
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/FormattableUtilsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/FormattableUtilsTest.java b/src/test/java/org/apache/commons/text/FormattableUtilsTest.java
new file mode 100644
index 0000000..d23dba6
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/FormattableUtilsTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import static java.util.FormattableFlags.LEFT_JUSTIFY;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Formatter;
+
+import org.junit.Test;
+
+/**
+ * Unit tests {@link FormattableUtils}.
+ */
+public class FormattableUtilsTest {
+
+    @Test
+    public void testDefaultAppend() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1).toString());
+        assertEquals("fo", FormattableUtils.append("foo", new Formatter(), 0, -1, 2).toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1).toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1).toString());
+        assertEquals(" fo", FormattableUtils.append("foo", new Formatter(), 0, 3, 2).toString());
+        assertEquals("   fo", FormattableUtils.append("foo", new Formatter(), 0, 5, 2).toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1).toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1).toString());
+        assertEquals("fo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2).toString());
+        assertEquals("fo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2).toString());
+    }
+
+    @Test
+    public void testAlternatePadCharacter() {
+        final char pad='_';
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, pad).toString());
+        assertEquals("fo", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, pad).toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, pad).toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, pad).toString());
+        assertEquals("_fo", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, pad).toString());
+        assertEquals("___fo", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, pad).toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, pad).toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, pad).toString());
+        assertEquals("fo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, pad).toString());
+        assertEquals("fo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, pad).toString());
+    }
+
+    @Test
+    public void testEllipsis() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, "*").toString());
+        assertEquals("f*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, "*").toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, "*").toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, "*").toString());
+        assertEquals(" f*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, "*").toString());
+        assertEquals("   f*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, "*").toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, "*").toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, "*").toString());
+        assertEquals("f* ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, "*").toString());
+        assertEquals("f*   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, "*").toString());
+
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, "+*").toString());
+        assertEquals("+*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, "+*").toString());
+        assertEquals(" foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, "+*").toString());
+        assertEquals("   foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, "+*").toString());
+        assertEquals(" +*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, "+*").toString());
+        assertEquals("   +*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, "+*").toString());
+        assertEquals("foo ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, "+*").toString());
+        assertEquals("foo   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, "+*").toString());
+        assertEquals("+* ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, "+*").toString());
+        assertEquals("+*   ", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, "+*").toString());
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void testIllegalEllipsis() {
+        FormattableUtils.append("foo", new Formatter(), 0, -1, 1, "xx");
+    }
+
+    @Test
+    public void testAlternatePadCharAndEllipsis() {
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, '_', "*").toString());
+        assertEquals("f*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, '_', "*").toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, '_', "*").toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, '_', "*").toString());
+        assertEquals("_f*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, '_', "*").toString());
+        assertEquals("___f*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, '_', "*").toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, '_', "*").toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, '_', "*").toString());
+        assertEquals("f*_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, '_', "*").toString());
+        assertEquals("f*___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, '_', "*").toString());
+
+        assertEquals("foo", FormattableUtils.append("foo", new Formatter(), 0, -1, -1, '_', "+*").toString());
+        assertEquals("+*", FormattableUtils.append("foo", new Formatter(), 0, -1, 2, '_', "+*").toString());
+        assertEquals("_foo", FormattableUtils.append("foo", new Formatter(), 0, 4, -1, '_', "+*").toString());
+        assertEquals("___foo", FormattableUtils.append("foo", new Formatter(), 0, 6, -1, '_', "+*").toString());
+        assertEquals("_+*", FormattableUtils.append("foo", new Formatter(), 0, 3, 2, '_', "+*").toString());
+        assertEquals("___+*", FormattableUtils.append("foo", new Formatter(), 0, 5, 2, '_', "+*").toString());
+        assertEquals("foo_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 4, -1, '_', "+*").toString());
+        assertEquals("foo___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 6, -1, '_', "+*").toString());
+        assertEquals("+*_", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 3, 2, '_', "+*").toString());
+        assertEquals("+*___", FormattableUtils.append("foo", new Formatter(), LEFT_JUSTIFY, 5, 2, '_', "+*").toString());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java b/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java
new file mode 100644
index 0000000..228c8df
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrBuilderAppendInsertTest.java
@@ -0,0 +1,1607 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+import java.text.DecimalFormatSymbols;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.apache.commons.lang3.SystemUtils;
+
+/**
+ * Unit tests for {@link StrBuilder}.
+ */
+public class StrBuilderAppendInsertTest {
+
+    /** The system line separator. */
+    private static final String SEP = SystemUtils.LINE_SEPARATOR;
+
+    /** Test subclass of Object, with a toString method. */
+    private static final Object FOO = new Object() {
+        @Override
+        public String toString() {
+            return "foo";
+        }
+    };
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendNewLine() {
+        StrBuilder sb = new StrBuilder("---");
+        sb.appendNewLine().append("+++");
+        assertEquals("---" + SEP + "+++", sb.toString());
+        
+        sb = new StrBuilder("---");
+        sb.setNewLineText("#").appendNewLine().setNewLineText(null).appendNewLine();
+        assertEquals("---#" + SEP, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendWithNullText() {
+        final StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL");
+        assertEquals("", sb.toString());
+
+        sb.appendNull();
+        assertEquals("NULL", sb.toString());
+
+        sb.append((Object) null);
+        assertEquals("NULLNULL", sb.toString());
+
+        sb.append(FOO);
+        assertEquals("NULLNULLfoo", sb.toString());
+
+        sb.append((String) null);
+        assertEquals("NULLNULLfooNULL", sb.toString());
+
+        sb.append("");
+        assertEquals("NULLNULLfooNULL", sb.toString());
+
+        sb.append("bar");
+        assertEquals("NULLNULLfooNULLbar", sb.toString());
+
+        sb.append((StringBuffer) null);
+        assertEquals("NULLNULLfooNULLbarNULL", sb.toString());
+
+        sb.append(new StringBuffer("baz"));
+        assertEquals("NULLNULLfooNULLbarNULLbaz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_Object() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendNull();
+        assertEquals("", sb.toString());
+
+        sb.append((Object) null);
+        assertEquals("", sb.toString());
+
+        sb.append(FOO);
+        assertEquals("foo", sb.toString());
+
+        sb.append((StringBuffer) null);
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuffer("baz"));
+        assertEquals("foobaz", sb.toString());
+
+        sb.append(new StrBuilder("yes"));
+        assertEquals("foobazyes", sb.toString());
+
+        sb.append((CharSequence) "Seq");
+        assertEquals("foobazyesSeq", sb.toString());
+
+        sb.append(new StringBuilder("bld")); // Check it supports StringBuilder
+        assertEquals("foobazyesSeqbld", sb.toString());
+    }
+    
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StringBuilder() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((String) null);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StringBuilder("foo"));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuilder(""));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuilder("bar"));
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_String() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((String) null);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+
+        sb.append("");
+        assertEquals("foo", sb.toString());
+
+        sb.append("bar");
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_String_int_int() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((String) null, 0, 1);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append("foo", 0, 3);
+        assertEquals("foo", sb.toString());
+
+        try {
+            sb.append("bar", -1, 1);
+            fail("append(char[], -1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append("bar", 3, 1);
+            fail("append(char[], 3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append("bar", 1, -1);
+            fail("append(char[],, -1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append("bar", 1, 3);
+            fail("append(char[], 1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append("bar", -1, 3);
+            fail("append(char[], -1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append("bar", 4, 0);
+            fail("append(char[], 4, 0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.append("bar", 3, 0);
+        assertEquals("foo", sb.toString());
+
+        sb.append("abcbardef", 3, 3);
+        assertEquals("foobar", sb.toString());
+
+        sb.append( (CharSequence)"abcbardef", 4, 3);
+        assertEquals("foobarard", sb.toString());
+    }
+    
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StringBuilder_int_int() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((String) null, 0, 1);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StringBuilder("foo"), 0, 3);
+        assertEquals("foo", sb.toString());
+
+        try {
+            sb.append(new StringBuilder("bar"), -1, 1);
+            fail("append(StringBuilder, -1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuilder("bar"), 3, 1);
+            fail("append(StringBuilder, 3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuilder("bar"), 1, -1);
+            fail("append(StringBuilder,, -1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuilder("bar"), 1, 3);
+            fail("append(StringBuilder, 1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuilder("bar"), -1, 3);
+            fail("append(StringBuilder, -1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuilder("bar"), 4, 0);
+            fail("append(StringBuilder, 4, 0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.append(new StringBuilder("bar"), 3, 0);
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuilder("abcbardef"), 3, 3);
+        assertEquals("foobar", sb.toString());
+
+        sb.append( new StringBuilder("abcbardef"), 4, 3);
+        assertEquals("foobarard", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StringBuffer() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((StringBuffer) null);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StringBuffer("foo"));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuffer(""));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuffer("bar"));
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StringBuffer_int_int() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((StringBuffer) null, 0, 1);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StringBuffer("foo"), 0, 3);
+        assertEquals("foo", sb.toString());
+
+        try {
+            sb.append(new StringBuffer("bar"), -1, 1);
+            fail("append(char[], -1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuffer("bar"), 3, 1);
+            fail("append(char[], 3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuffer("bar"), 1, -1);
+            fail("append(char[],, -1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuffer("bar"), 1, 3);
+            fail("append(char[], 1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuffer("bar"), -1, 3);
+            fail("append(char[], -1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StringBuffer("bar"), 4, 0);
+            fail("append(char[], 4, 0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.append(new StringBuffer("bar"), 3, 0);
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StringBuffer("abcbardef"), 3, 3);
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StrBuilder() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((StrBuilder) null);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StrBuilder("foo"));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StrBuilder(""));
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StrBuilder("bar"));
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_StrBuilder_int_int() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((StrBuilder) null, 0, 1);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new StrBuilder("foo"), 0, 3);
+        assertEquals("foo", sb.toString());
+
+        try {
+            sb.append(new StrBuilder("bar"), -1, 1);
+            fail("append(char[], -1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StrBuilder("bar"), 3, 1);
+            fail("append(char[], 3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StrBuilder("bar"), 1, -1);
+            fail("append(char[],, -1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StrBuilder("bar"), 1, 3);
+            fail("append(char[], 1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StrBuilder("bar"), -1, 3);
+            fail("append(char[], -1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new StrBuilder("bar"), 4, 0);
+            fail("append(char[], 4, 0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.append(new StrBuilder("bar"), 3, 0);
+        assertEquals("foo", sb.toString());
+
+        sb.append(new StrBuilder("abcbardef"), 3, 3);
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_CharArray() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((char[]) null);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new char[0]);
+        assertEquals("", sb.toString());
+
+        sb.append(new char[]{'f', 'o', 'o'});
+        assertEquals("foo", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_CharArray_int_int() {
+        StrBuilder sb = new StrBuilder();
+        sb.setNullText("NULL").append((char[]) null, 0, 1);
+        assertEquals("NULL", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append(new char[]{'f', 'o', 'o'}, 0, 3);
+        assertEquals("foo", sb.toString());
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, -1, 1);
+            fail("append(char[], -1,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, 3, 1);
+            fail("append(char[], 3,) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, 1, -1);
+            fail("append(char[],, -1) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, 1, 3);
+            fail("append(char[], 1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, -1, 3);
+            fail("append(char[], -1, 3) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.append(new char[]{'b', 'a', 'r'}, 4, 0);
+            fail("append(char[], 4, 0) expected IndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.append(new char[]{'b', 'a', 'r'}, 3, 0);
+        assertEquals("foo", sb.toString());
+
+        sb.append(new char[]{'a', 'b', 'c', 'b', 'a', 'r', 'd', 'e', 'f'}, 3, 3);
+        assertEquals("foobar", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_Boolean() {
+        final StrBuilder sb = new StrBuilder();
+        sb.append(true);
+        assertEquals("true", sb.toString());
+
+        sb.append(false);
+        assertEquals("truefalse", sb.toString());
+
+        sb.append('!');
+        assertEquals("truefalse!", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_PrimitiveNumber() {
+        final StrBuilder sb = new StrBuilder();
+        sb.append(0);
+        assertEquals("0", sb.toString());
+
+        sb.append(1L);
+        assertEquals("01", sb.toString());
+
+        sb.append(2.3f);
+        assertEquals("012.3", sb.toString());
+
+        sb.append(4.5d);
+        assertEquals("012.34.5", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_FormattedString() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final String str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln("Hello %s", "Alice");
+        assertEquals("Hello Alice" + SEP, sb.toString());
+        assertEquals(2, count[0]);  // appendNewLine() calls append(String)
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_Object() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendln((Object) null);
+        assertEquals("" + SEP, sb.toString());
+
+        sb.appendln(FOO);
+        assertEquals(SEP + "foo" + SEP, sb.toString());
+
+        sb.appendln(Integer.valueOf(6));
+        assertEquals(SEP + "foo" + SEP + "6" + SEP, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_String() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final String str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln("foo");
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(2, count[0]);  // appendNewLine() calls append(String)
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_String_int_int() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final String str, final int startIndex, final int length) {
+                count[0]++;
+                return super.append(str, startIndex, length);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln("foo", 0, 3);
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StringBuffer() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StringBuffer str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StringBuffer("foo"));
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StringBuilder() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StringBuilder str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StringBuilder("foo"));
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StringBuffer_int_int() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StringBuffer str, final int startIndex, final int length) {
+                count[0]++;
+                return super.append(str, startIndex, length);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StringBuffer("foo"), 0, 3);
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StringBuilder_int_int() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StringBuilder str, final int startIndex, final int length) {
+                count[0]++;
+                return super.append(str, startIndex, length);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StringBuilder("foo"), 0, 3);
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StrBuilder() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StrBuilder str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StrBuilder("foo"));
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_StrBuilder_int_int() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final StrBuilder str, final int startIndex, final int length) {
+                count[0]++;
+                return super.append(str, startIndex, length);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln(new StrBuilder("foo"), 0, 3);
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_CharArray() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final char[] str) {
+                count[0]++;
+                return super.append(str);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln("foo".toCharArray());
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_CharArray_int_int() {
+        final int[] count = new int[2];
+        final StrBuilder sb = new StrBuilder() {
+            private static final long serialVersionUID = 1L;
+
+            @Override
+            public StrBuilder append(final char[] str, final int startIndex, final int length) {
+                count[0]++;
+                return super.append(str, startIndex, length);
+            }
+            @Override
+            public StrBuilder appendNewLine() {
+                count[1]++;
+                return super.appendNewLine();
+            }
+        };
+        sb.appendln("foo".toCharArray(), 0, 3);
+        assertEquals("foo" + SEP, sb.toString());
+        assertEquals(1, count[0]);
+        assertEquals(1, count[1]);
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_Boolean() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendln(true);
+        assertEquals("true" + SEP, sb.toString());
+        
+        sb.clear();
+        sb.appendln(false);
+        assertEquals("false" + SEP, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendln_PrimitiveNumber() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendln(0);
+        assertEquals("0" + SEP, sb.toString());
+        
+        sb.clear();
+        sb.appendln(1L);
+        assertEquals("1" + SEP, sb.toString());
+        
+        sb.clear();
+        sb.appendln(2.3f);
+        assertEquals("2.3" + SEP, sb.toString());
+        
+        sb.clear();
+        sb.appendln(4.5d);
+        assertEquals("4.5" + SEP, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendPadding() {
+        final StrBuilder sb = new StrBuilder();
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+
+        sb.appendPadding(-1, '-');
+        assertEquals("foo", sb.toString());
+
+        sb.appendPadding(0, '-');
+        assertEquals("foo", sb.toString());
+
+        sb.appendPadding(1, '-');
+        assertEquals("foo-", sb.toString());
+
+        sb.appendPadding(16, '-');
+        assertEquals(20, sb.length());
+        //            12345678901234567890
+        assertEquals("foo-----------------", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendFixedWidthPadLeft() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadLeft("foo", -1, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 0, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 1, '-');
+        assertEquals("o", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 2, '-');
+        assertEquals("oo", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 3, '-');
+        assertEquals("foo", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 4, '-');
+        assertEquals("-foo", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft("foo", 10, '-');
+        assertEquals(10, sb.length());
+        //            1234567890
+        assertEquals("-------foo", sb.toString());
+
+        sb.clear();
+        sb.setNullText("null");
+        sb.appendFixedWidthPadLeft(null, 5, '-');
+        assertEquals("-null", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendFixedWidthPadLeft_int() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadLeft(123, -1, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 0, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 1, '-');
+        assertEquals("3", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 2, '-');
+        assertEquals("23", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 3, '-');
+        assertEquals("123", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 4, '-');
+        assertEquals("-123", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadLeft(123, 10, '-');
+        assertEquals(10, sb.length());
+        //            1234567890
+        assertEquals("-------123", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendFixedWidthPadRight() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadRight("foo", -1, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 0, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 1, '-');
+        assertEquals("f", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 2, '-');
+        assertEquals("fo", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 3, '-');
+        assertEquals("foo", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 4, '-');
+        assertEquals("foo-", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight("foo", 10, '-');
+        assertEquals(10, sb.length());
+        //            1234567890
+        assertEquals("foo-------", sb.toString());
+
+        sb.clear();
+        sb.setNullText("null");
+        sb.appendFixedWidthPadRight(null, 5, '-');
+        assertEquals("null-", sb.toString());
+    }
+
+    // See: http://issues.apache.org/jira/browse/LANG-299
+    @Test
+    public void testLang299() {
+        final StrBuilder sb = new StrBuilder(1);
+        sb.appendFixedWidthPadRight("foo", 1, '-');
+        assertEquals("f", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendFixedWidthPadRight_int() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendFixedWidthPadRight(123, -1, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 0, '-');
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 1, '-');
+        assertEquals("1", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 2, '-');
+        assertEquals("12", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 3, '-');
+        assertEquals("123", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 4, '-');
+        assertEquals("123-", sb.toString());
+
+        sb.clear();
+        sb.appendFixedWidthPadRight(123, 10, '-');
+        assertEquals(10, sb.length());
+        //            1234567890
+        assertEquals("123-------", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppend_FormattedString() {
+        StrBuilder sb;
+
+        sb = new StrBuilder();
+        sb.append("Hi", (Object[]) null);
+        assertEquals("Hi", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append("Hi", "Alice");
+        assertEquals("Hi", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append("Hi %s", "Alice");
+        assertEquals("Hi Alice", sb.toString());
+
+        sb = new StrBuilder();
+        sb.append("Hi %s %,d", "Alice", 5000);
+        // group separator depends on system locale
+        final char groupingSeparator = DecimalFormatSymbols.getInstance().getGroupingSeparator();
+        final String expected = "Hi Alice 5" + groupingSeparator + "000";
+        assertEquals(expected, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendAll_Array() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendAll((Object[]) null);
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(new Object[0]);
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(new Object[]{"foo", "bar", "baz"});
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.appendAll("foo", "bar", "baz");
+        assertEquals("foobarbaz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendAll_Collection() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendAll((Collection<?>) null);
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(Collections.EMPTY_LIST);
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(Arrays.asList(new Object[]{"foo", "bar", "baz"}));
+        assertEquals("foobarbaz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendAll_Iterator() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendAll((Iterator<?>) null);
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(Collections.EMPTY_LIST.iterator());
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendAll(Arrays.asList(new Object[]{"foo", "bar", "baz"}).iterator());
+        assertEquals("foobarbaz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendWithSeparators_Array() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendWithSeparators((Object[]) null, ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(new Object[0], ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(new Object[]{"foo", "bar", "baz"}, ",");
+        assertEquals("foo,bar,baz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(new Object[]{"foo", "bar", "baz"}, null);
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(new Object[]{"foo", null, "baz"}, ",");
+        assertEquals("foo,,baz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendWithSeparators_Collection() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendWithSeparators((Collection<?>) null, ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Collections.EMPTY_LIST, ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", "bar", "baz"}), ",");
+        assertEquals("foo,bar,baz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", "bar", "baz"}), null);
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", null, "baz"}), ",");
+        assertEquals("foo,,baz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendWithSeparators_Iterator() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendWithSeparators((Iterator<?>) null, ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Collections.EMPTY_LIST.iterator(), ",");
+        assertEquals("", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", "bar", "baz"}).iterator(), ",");
+        assertEquals("foo,bar,baz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", "bar", "baz"}).iterator(), null);
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", null, "baz"}).iterator(), ",");
+        assertEquals("foo,,baz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendWithSeparatorsWithNullText() {
+        final StrBuilder sb = new StrBuilder();
+        sb.setNullText("null");
+        sb.appendWithSeparators(new Object[]{"foo", null, "baz"}, ",");
+        assertEquals("foo,null,baz", sb.toString());
+
+        sb.clear();
+        sb.appendWithSeparators(Arrays.asList(new Object[]{"foo", null, "baz"}), ",");
+        assertEquals("foo,null,baz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendSeparator_String() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendSeparator(",");  // no effect
+        assertEquals("", sb.toString());
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+        sb.appendSeparator(",");
+        assertEquals("foo,", sb.toString());
+    }
+    
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendSeparator_String_String() {
+        final StrBuilder sb = new StrBuilder();
+        final String startSeparator = "order by ";
+        final String standardSeparator = ",";
+        final String foo = "foo";
+        sb.appendSeparator(null, null);
+        assertEquals("", sb.toString());
+        sb.appendSeparator(standardSeparator, null);
+        assertEquals("", sb.toString());
+        sb.appendSeparator(standardSeparator, startSeparator); 
+        assertEquals(startSeparator, sb.toString());
+        sb.appendSeparator(null, null); 
+        assertEquals(startSeparator, sb.toString());
+        sb.appendSeparator(null, startSeparator); 
+        assertEquals(startSeparator, sb.toString());
+        sb.append(foo);
+        assertEquals(startSeparator + foo, sb.toString());
+        sb.appendSeparator(standardSeparator, startSeparator);
+        assertEquals(startSeparator + foo + standardSeparator, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendSeparator_char() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendSeparator(',');  // no effect
+        assertEquals("", sb.toString());
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+        sb.appendSeparator(',');
+        assertEquals("foo,", sb.toString());
+    }
+    @Test
+    public void testAppendSeparator_char_char() {
+        final StrBuilder sb = new StrBuilder();
+        final char startSeparator = ':';
+        final char standardSeparator = ',';
+        final String foo = "foo";
+        sb.appendSeparator(standardSeparator, startSeparator);  // no effect
+        assertEquals(String.valueOf(startSeparator), sb.toString());
+        sb.append(foo);
+        assertEquals(String.valueOf(startSeparator) + foo, sb.toString());
+        sb.appendSeparator(standardSeparator, startSeparator);
+        assertEquals(String.valueOf(startSeparator) + foo + standardSeparator, sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendSeparator_String_int() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendSeparator(",", 0);  // no effect
+        assertEquals("", sb.toString());
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+        sb.appendSeparator(",", 1);
+        assertEquals("foo,", sb.toString());
+        
+        sb.appendSeparator(",", -1);  // no effect
+        assertEquals("foo,", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testAppendSeparator_char_int() {
+        final StrBuilder sb = new StrBuilder();
+        sb.appendSeparator(',', 0);  // no effect
+        assertEquals("", sb.toString());
+        sb.append("foo");
+        assertEquals("foo", sb.toString());
+        sb.appendSeparator(',', 1);
+        assertEquals("foo,", sb.toString());
+        
+        sb.appendSeparator(',', -1);  // no effect
+        assertEquals("foo,", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testInsert() {
+
+        final StrBuilder sb = new StrBuilder();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, FOO);
+            fail("insert(-1, Object) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, FOO);
+            fail("insert(7, Object) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (Object) null);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, FOO);
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, "foo");
+            fail("insert(-1, String) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, "foo");
+            fail("insert(7, String) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (String) null);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, "foo");
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, new char[]{'f', 'o', 'o'});
+            fail("insert(-1, char[]) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, new char[]{'f', 'o', 'o'});
+            fail("insert(7, char[]) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (char[]) null);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, new char[0]);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, new char[]{'f', 'o', 'o'});
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3);
+            fail("insert(-1, char[], 3, 3) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3);
+            fail("insert(7, char[], 3, 3) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (char[]) null, 0, 0);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, new char[0], 0, 0);
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, -1, 3);
+            fail("insert(0, char[], -1, 3) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 10, 3);
+            fail("insert(0, char[], 10, 3) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, -1);
+            fail("insert(0, char[], 0, -1) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 10);
+            fail("insert(0, char[], 0, 10) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 0, 0);
+        assertEquals("barbaz", sb.toString());
+
+        sb.insert(0, new char[]{'a', 'b', 'c', 'f', 'o', 'o', 'd', 'e', 'f'}, 3, 3);
+        assertEquals("foobarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, true);
+            fail("insert(-1, boolean) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, true);
+            fail("insert(7, boolean) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, true);
+        assertEquals("truebarbaz", sb.toString());
+
+        sb.insert(0, false);
+        assertEquals("falsetruebarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, '!');
+            fail("insert(-1, char) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, '!');
+            fail("insert(7, char) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, '!');
+        assertEquals("!barbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, 0);
+            fail("insert(-1, int) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, 0);
+            fail("insert(7, int) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, '0');
+        assertEquals("0barbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, 1L);
+            fail("insert(-1, long) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, 1L);
+            fail("insert(7, long) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, 1L);
+        assertEquals("1barbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, 2.3F);
+            fail("insert(-1, float) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, 2.3F);
+            fail("insert(7, float) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, 2.3F);
+        assertEquals("2.3barbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, 4.5D);
+            fail("insert(-1, double) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, 4.5D);
+            fail("insert(7, double) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, 4.5D);
+        assertEquals("4.5barbaz", sb.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testInsertWithNullText() {
+        final StrBuilder sb = new StrBuilder();
+        sb.setNullText("null");
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, FOO);
+            fail("insert(-1, Object) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, FOO);
+            fail("insert(7, Object) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (Object) null);
+        assertEquals("nullbarbaz", sb.toString());
+
+        sb.insert(0, FOO);
+        assertEquals("foonullbarbaz", sb.toString());
+
+        sb.clear();
+        sb.append("barbaz");
+        assertEquals("barbaz", sb.toString());
+
+        try {
+            sb.insert(-1, "foo");
+            fail("insert(-1, String) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        try {
+            sb.insert(7, "foo");
+            fail("insert(7, String) expected StringIndexOutOfBoundsException");
+        } catch (final IndexOutOfBoundsException e) {
+            // expected
+        }
+
+        sb.insert(0, (String) null);
+        assertEquals("nullbarbaz", sb.toString());
+
+        sb.insert(0, "foo");
+        assertEquals("foonullbarbaz", sb.toString());
+
+        sb.insert(0, (char[]) null);
+        assertEquals("nullfoonullbarbaz", sb.toString());
+
+        sb.insert(0, (char[]) null, 0, 0);
+        assertEquals("nullnullfoonullbarbaz", sb.toString());
+    }
+}


[03/14] [text] TEXT-23: adding tests to EntitiyArraysTest to validate escaping vs unescaping

Posted by ch...@apache.org.
TEXT-23: adding tests to EntitiyArraysTest to validate escaping vs unescaping


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/e3c006e1
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/e3c006e1
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/e3c006e1

Branch: refs/heads/master
Commit: e3c006e13b202cf92f274d7b26ee5ffb2316dd2c
Parents: 32196b1
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 12:34:05 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 12:34:05 2016 -0500

----------------------------------------------------------------------
 .../text/translate/EntityArraysTest.java        | 37 +++++++++++++++++++-
 1 file changed, 36 insertions(+), 1 deletion(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/e3c006e1/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java b/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java
index 9b3dd4d..f1451b5 100644
--- a/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java
+++ b/src/test/java/org/apache/commons/text/translate/EntityArraysTest.java
@@ -22,6 +22,7 @@ import org.junit.Test;
 import java.util.HashSet;
 import java.util.Set;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 /**
@@ -67,6 +68,40 @@ public class EntityArraysTest  {
         }
         assertTrue("One or more errors detected",success);
     }
-    
+
+    @Test
+    public void testISO8859Arrays() {
+        testEscapeVsUnescapeArrays(EntityArrays.ISO8859_1_ESCAPE(), EntityArrays.ISO8859_1_UNESCAPE());
+    }
+
+    @Test
+    public void testHtml40ExtendedArrays() {
+        testEscapeVsUnescapeArrays(EntityArrays.HTML40_EXTENDED_ESCAPE(), EntityArrays.HTML40_EXTENDED_UNESCAPE());
+    }
+
+    @Test
+    public void testAposArrays() {
+        testEscapeVsUnescapeArrays(EntityArrays.APOS_ESCAPE(), EntityArrays.APOS_UNESCAPE());
+    }
+
+    @Test
+    public void testBasicArrays() {
+        testEscapeVsUnescapeArrays(EntityArrays.BASIC_ESCAPE(), EntityArrays.BASIC_UNESCAPE());
+    }
+
+    @Test
+    public void testJavaCntrlCharsArrays() {
+        testEscapeVsUnescapeArrays(EntityArrays.JAVA_CTRL_CHARS_ESCAPE(), EntityArrays.JAVA_CTRL_CHARS_ESCAPE());
+    }
+
+    private void testEscapeVsUnescapeArrays(String[][] escapeArray, String[][] unescapeArray) {
+        for (String[] escapeElement : escapeArray) {
+            for (String[] unescapeElement : unescapeArray) {
+                if (escapeElement[0] == unescapeElement[1]) {
+                    assertEquals(escapeElement[1], unescapeElement[0]);
+                }
+            }
+        }
+    }
     
 }


[10/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
TEXT-23: Adding remainder of the o.a.c.l.text package


Project: http://git-wip-us.apache.org/repos/asf/commons-text/repo
Commit: http://git-wip-us.apache.org/repos/asf/commons-text/commit/6f24aa45
Tree: http://git-wip-us.apache.org/repos/asf/commons-text/tree/6f24aa45
Diff: http://git-wip-us.apache.org/repos/asf/commons-text/diff/6f24aa45

Branch: refs/heads/master
Commit: 6f24aa4568fb62777d515625cd0146d4c36d5a6c
Parents: e3c006e
Author: Rob Tompkins <ch...@capitalone.com>
Authored: Wed Nov 9 15:35:38 2016 -0500
Committer: Rob Tompkins <ch...@capitalone.com>
Committed: Wed Nov 9 15:35:38 2016 -0500

----------------------------------------------------------------------
 .../java/org/apache/commons/text/Builder.java   |   87 +
 .../apache/commons/text/CompositeFormat.java    |  114 +
 .../commons/text/ExtendedMessageFormat.java     |  529 +++
 .../org/apache/commons/text/FormatFactory.java  |   39 +
 .../apache/commons/text/FormattableUtils.java   |  151 +
 .../org/apache/commons/text/StrBuilder.java     | 3117 ++++++++++++++++++
 .../java/org/apache/commons/text/StrLookup.java |  178 +
 .../org/apache/commons/text/StrMatcher.java     |  437 +++
 .../org/apache/commons/text/StrSubstitutor.java | 1227 +++++++
 .../org/apache/commons/text/StrTokenizer.java   | 1116 +++++++
 .../java/org/apache/commons/text/WordUtils.java |  733 ++++
 .../text/translate/AggregateTranslator.java     |    2 -
 .../text/translate/CharSequenceTranslator.java  |    6 +-
 .../text/translate/CodePointTranslator.java     |    2 -
 .../commons/text/translate/EntityArrays.java    |    2 -
 .../text/translate/JavaUnicodeEscaper.java      |   14 +-
 .../text/translate/LookupTranslator.java        |    2 -
 .../text/translate/NumericEntityEscaper.java    |   14 +-
 .../text/translate/NumericEntityUnescaper.java  |    2 -
 .../commons/text/translate/OctalUnescaper.java  |    2 -
 .../commons/text/translate/UnicodeEscaper.java  |   14 +-
 .../text/translate/UnicodeUnescaper.java        |    2 -
 .../commons/text/translate/package-info.java    |    2 -
 .../commons/text/CompositeFormatTest.java       |   85 +
 .../commons/text/ExtendedMessageFormatTest.java |  484 +++
 .../commons/text/FormattableUtilsTest.java      |  115 +
 .../text/StrBuilderAppendInsertTest.java        | 1607 +++++++++
 .../org/apache/commons/text/StrBuilderTest.java | 2009 +++++++++++
 .../org/apache/commons/text/StrLookupTest.java  |  115 +
 .../org/apache/commons/text/StrMatcherTest.java |  214 ++
 .../apache/commons/text/StrSubstitutorTest.java |  740 +++++
 .../apache/commons/text/StrTokenizerTest.java   |  879 +++++
 .../org/apache/commons/text/WordUtilsTest.java  |  421 +++
 33 files changed, 14417 insertions(+), 44 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/Builder.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/Builder.java b/src/main/java/org/apache/commons/text/Builder.java
new file mode 100644
index 0000000..8d9a34d
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/Builder.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+/**
+ * <p>
+ * The Builder interface is designed to designate a class as a <em>builder</em>
+ * object in the Builder design pattern. Builders are capable of creating and
+ * configuring objects or results that normally take multiple steps to construct
+ * or are very complex to derive.
+ * </p>
+ *
+ * <p>
+ * The builder interface defines a single method, {@link #build()}, that
+ * classes must implement. The result of this method should be the final
+ * configured object or result after all building operations are performed.
+ * </p>
+ *
+ * <p>
+ * It is a recommended practice that the methods supplied to configure the
+ * object or result being built return a reference to {@code this} so that
+ * method calls can be chained together.
+ * </p>
+ *
+ * <p>
+ * Example Builder:
+ * <pre><code>
+ * class FontBuilder implements Builder&lt;Font&gt; {
+ *     private Font font;
+ *
+ *     public FontBuilder(String fontName) {
+ *         this.font = new Font(fontName, Font.PLAIN, 12);
+ *     }
+ *
+ *     public FontBuilder bold() {
+ *         this.font = this.font.deriveFont(Font.BOLD);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ *
+ *     public FontBuilder size(float pointSize) {
+ *         this.font = this.font.deriveFont(pointSize);
+ *         return this; // Reference returned so calls can be chained
+ *     }
+ *
+ *     // Other Font construction methods
+ *
+ *     public Font build() {
+ *         return this.font;
+ *     }
+ * }
+ * </code></pre>
+ *
+ * Example Builder Usage:
+ * <pre><code>
+ * Font bold14ptSansSerifFont = new FontBuilder(Font.SANS_SERIF).bold()
+ *                                                              .size(14.0f)
+ *                                                              .build();
+ * </code></pre>
+ *
+ *
+ * @param <T> the type of object that the builder will construct or compute.
+ *
+ */
+public interface Builder<T> {
+
+    /**
+     * Returns a reference to the object being constructed or result being
+     * calculated by the builder.
+     *
+     * @return the object constructed or result calculated by the builder.
+     */
+    T build();
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/CompositeFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/CompositeFormat.java b/src/main/java/org/apache/commons/text/CompositeFormat.java
new file mode 100644
index 0000000..6239561
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/CompositeFormat.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.text.FieldPosition;
+import java.text.Format;
+import java.text.ParseException;
+import java.text.ParsePosition;
+
+/**
+ * Formats using one formatter and parses using a different formatter. An
+ * example of use for this would be a webapp where data is taken in one way and
+ * stored in a database another way.
+ */
+public class CompositeFormat extends Format {
+
+    /**
+     * Required for serialization support.
+     * 
+     * @see java.io.Serializable
+     */
+    private static final long serialVersionUID = -4329119827877627683L;
+
+    /** The parser to use. */
+    private final Format parser;
+    /** The formatter to use. */
+    private final Format formatter;
+
+    /**
+     * Create a format that points its parseObject method to one implementation
+     * and its format method to another.
+     * 
+     * @param parser implementation
+     * @param formatter implementation
+     */
+    public CompositeFormat(final Format parser, final Format formatter) {
+        this.parser = parser;
+        this.formatter = formatter;
+    }
+
+    /**
+     * Uses the formatter Format instance.
+     * 
+     * @param obj the object to format
+     * @param toAppendTo the {@link StringBuffer} to append to
+     * @param pos the FieldPosition to use (or ignore).
+     * @return <code>toAppendTo</code>
+     * @see Format#format(Object, StringBuffer, FieldPosition)
+     */
+    @Override // Therefore has to use StringBuffer
+    public StringBuffer format(final Object obj, final StringBuffer toAppendTo,
+            final FieldPosition pos) {
+        return formatter.format(obj, toAppendTo, pos);
+    }
+
+    /**
+     * Uses the parser Format instance.
+     * 
+     * @param source the String source
+     * @param pos the ParsePosition containing the position to parse from, will
+     *            be updated according to parsing success (index) or failure
+     *            (error index)
+     * @return the parsed Object
+     * @see Format#parseObject(String, ParsePosition)
+     */
+    @Override
+    public Object parseObject(final String source, final ParsePosition pos) {
+        return parser.parseObject(source, pos);
+    }
+
+    /**
+     * Provides access to the parser Format implementation.
+     * 
+     * @return parser Format implementation
+     */
+    public Format getParser() {
+        return this.parser;
+    }
+
+    /**
+     * Provides access to the parser Format implementation.
+     * 
+     * @return formatter Format implementation
+     */
+    public Format getFormatter() {
+        return this.formatter;
+    }
+
+    /**
+     * Utility method to parse and then reformat a String.
+     * 
+     * @param input String to reformat
+     * @return A reformatted String
+     * @throws ParseException thrown by parseObject(String) call
+     */
+    public String reformat(final String input) throws ParseException {
+        return format(parseObject(input));
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java b/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java
new file mode 100644
index 0000000..c88752a
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/ExtendedMessageFormat.java
@@ -0,0 +1,529 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.apache.commons.lang3.ObjectUtils;
+
+import java.text.Format;
+import java.text.MessageFormat;
+import java.text.ParsePosition;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Extends <code>java.text.MessageFormat</code> to allow pluggable/additional formatting
+ * options for embedded format elements.  Client code should specify a registry
+ * of <code>FormatFactory</code> instances associated with <code>String</code>
+ * format names.  This registry will be consulted when the format elements are
+ * parsed from the message pattern.  In this way custom patterns can be specified,
+ * and the formats supported by <code>java.text.MessageFormat</code> can be overridden
+ * at the format and/or format style level (see MessageFormat).  A "format element"
+ * embedded in the message pattern is specified (<b>()?</b> signifies optionality):<br>
+ * <code>{</code><i>argument-number</i><b>(</b><code>,</code><i>format-name</i><b>
+ * (</b><code>,</code><i>format-style</i><b>)?)?</b><code>}</code>
+ *
+ * <p>
+ * <i>format-name</i> and <i>format-style</i> values are trimmed of surrounding whitespace
+ * in the manner of <code>java.text.MessageFormat</code>.  If <i>format-name</i> denotes
+ * <code>FormatFactory formatFactoryInstance</code> in <code>registry</code>, a <code>Format</code>
+ * matching <i>format-name</i> and <i>format-style</i> is requested from
+ * <code>formatFactoryInstance</code>.  If this is successful, the <code>Format</code>
+ * found is used for this format element.
+ * </p>
+ *
+ * <p><b>NOTICE:</b> The various subformat mutator methods are considered unnecessary; they exist on the parent
+ * class to allow the type of customization which it is the job of this class to provide in
+ * a configurable fashion.  These methods have thus been disabled and will throw
+ * <code>UnsupportedOperationException</code> if called.
+ * </p>
+ *
+ * <p>Limitations inherited from <code>java.text.MessageFormat</code>:</p>
+ * <ul>
+ * <li>When using "choice" subformats, support for nested formatting instructions is limited
+ *     to that provided by the base class.</li>
+ * <li>Thread-safety of <code>Format</code>s, including <code>MessageFormat</code> and thus
+ *     <code>ExtendedMessageFormat</code>, is not guaranteed.</li>
+ * </ul>
+ */
+public class ExtendedMessageFormat extends MessageFormat {
+    private static final long serialVersionUID = -2362048321261811743L;
+    private static final int HASH_SEED = 31;
+
+    private static final String DUMMY_PATTERN = "";
+    private static final char START_FMT = ',';
+    private static final char END_FE = '}';
+    private static final char START_FE = '{';
+    private static final char QUOTE = '\'';
+
+    private String toPattern;
+    private final Map<String, ? extends FormatFactory> registry;
+
+    /**
+     * Create a new ExtendedMessageFormat for the default locale.
+     *
+     * @param pattern  the pattern to use, not null
+     * @throws IllegalArgumentException in case of a bad pattern.
+     */
+    public ExtendedMessageFormat(final String pattern) {
+        this(pattern, Locale.getDefault());
+    }
+
+    /**
+     * Create a new ExtendedMessageFormat.
+     *
+     * @param pattern  the pattern to use, not null
+     * @param locale  the locale to use, not null
+     * @throws IllegalArgumentException in case of a bad pattern.
+     */
+    public ExtendedMessageFormat(final String pattern, final Locale locale) {
+        this(pattern, locale, null);
+    }
+
+    /**
+     * Create a new ExtendedMessageFormat for the default locale.
+     *
+     * @param pattern  the pattern to use, not null
+     * @param registry  the registry of format factories, may be null
+     * @throws IllegalArgumentException in case of a bad pattern.
+     */
+    public ExtendedMessageFormat(final String pattern, final Map<String, ? extends FormatFactory> registry) {
+        this(pattern, Locale.getDefault(), registry);
+    }
+
+    /**
+     * Create a new ExtendedMessageFormat.
+     *
+     * @param pattern  the pattern to use, not null
+     * @param locale  the locale to use, not null
+     * @param registry  the registry of format factories, may be null
+     * @throws IllegalArgumentException in case of a bad pattern.
+     */
+    public ExtendedMessageFormat(final String pattern, final Locale locale, final Map<String, ? extends FormatFactory> registry) {
+        super(DUMMY_PATTERN);
+        setLocale(locale);
+        this.registry = registry;
+        applyPattern(pattern);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public String toPattern() {
+        return toPattern;
+    }
+
+    /**
+     * Apply the specified pattern.
+     *
+     * @param pattern String
+     */
+    @Override
+    public final void applyPattern(final String pattern) {
+        if (registry == null) {
+            super.applyPattern(pattern);
+            toPattern = super.toPattern();
+            return;
+        }
+        final ArrayList<Format> foundFormats = new ArrayList<>();
+        final ArrayList<String> foundDescriptions = new ArrayList<>();
+        final StringBuilder stripCustom = new StringBuilder(pattern.length());
+
+        final ParsePosition pos = new ParsePosition(0);
+        final char[] c = pattern.toCharArray();
+        int fmtCount = 0;
+        while (pos.getIndex() < pattern.length()) {
+            switch (c[pos.getIndex()]) {
+            case QUOTE:
+                appendQuotedString(pattern, pos, stripCustom);
+                break;
+            case START_FE:
+                fmtCount++;
+                seekNonWs(pattern, pos);
+                final int start = pos.getIndex();
+                final int index = readArgumentIndex(pattern, next(pos));
+                stripCustom.append(START_FE).append(index);
+                seekNonWs(pattern, pos);
+                Format format = null;
+                String formatDescription = null;
+                if (c[pos.getIndex()] == START_FMT) {
+                    formatDescription = parseFormatDescription(pattern,
+                            next(pos));
+                    format = getFormat(formatDescription);
+                    if (format == null) {
+                        stripCustom.append(START_FMT).append(formatDescription);
+                    }
+                }
+                foundFormats.add(format);
+                foundDescriptions.add(format == null ? null : formatDescription);
+                if(foundFormats.size() != fmtCount) {
+                    throw new IllegalArgumentException("The validated expression is false");
+                }
+                if (foundDescriptions.size() != fmtCount) {
+                    throw new IllegalArgumentException("The validated expression is false");
+                }
+                if (c[pos.getIndex()] != END_FE) {
+                    throw new IllegalArgumentException(
+                            "Unreadable format element at position " + start);
+                }
+                //$FALL-THROUGH$
+            default:
+                stripCustom.append(c[pos.getIndex()]);
+                next(pos);
+            }
+        }
+        super.applyPattern(stripCustom.toString());
+        toPattern = insertFormats(super.toPattern(), foundDescriptions);
+        if (containsElements(foundFormats)) {
+            final Format[] origFormats = getFormats();
+            // only loop over what we know we have, as MessageFormat on Java 1.3
+            // seems to provide an extra format element:
+            int i = 0;
+            for (final Iterator<Format> it = foundFormats.iterator(); it.hasNext(); i++) {
+                final Format f = it.next();
+                if (f != null) {
+                    origFormats[i] = f;
+                }
+            }
+            super.setFormats(origFormats);
+        }
+    }
+
+    /**
+     * Throws UnsupportedOperationException - see class Javadoc for details.
+     *
+     * @param formatElementIndex format element index
+     * @param newFormat the new format
+     * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
+     */
+    @Override
+    public void setFormat(final int formatElementIndex, final Format newFormat) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Throws UnsupportedOperationException - see class Javadoc for details.
+     *
+     * @param argumentIndex argument index
+     * @param newFormat the new format
+     * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
+     */
+    @Override
+    public void setFormatByArgumentIndex(final int argumentIndex, final Format newFormat) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Throws UnsupportedOperationException - see class Javadoc for details.
+     *
+     * @param newFormats new formats
+     * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
+     */
+    @Override
+    public void setFormats(final Format[] newFormats) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Throws UnsupportedOperationException - see class Javadoc for details.
+     *
+     * @param newFormats new formats
+     * @throws UnsupportedOperationException always thrown since this isn't supported by ExtendMessageFormat
+     */
+    @Override
+    public void setFormatsByArgumentIndex(final Format[] newFormats) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Check if this extended message format is equal to another object.
+     *
+     * @param obj the object to compare to
+     * @return true if this object equals the other, otherwise false
+     */
+    @Override
+    public boolean equals(final Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (!super.equals(obj)) {
+            return false;
+        }
+        if (ObjectUtils.notEqual(getClass(), obj.getClass())) {
+          return false;
+        }
+        final ExtendedMessageFormat rhs = (ExtendedMessageFormat)obj;
+        if (ObjectUtils.notEqual(toPattern, rhs.toPattern)) {
+            return false;
+        }
+        if (ObjectUtils.notEqual(registry, rhs.registry)) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = HASH_SEED * result + Objects.hashCode(registry);
+        result = HASH_SEED * result + Objects.hashCode(toPattern);
+        return result;
+    }
+
+    /**
+     * Get a custom format from a format description.
+     *
+     * @param desc String
+     * @return Format
+     */
+    private Format getFormat(final String desc) {
+        if (registry != null) {
+            String name = desc;
+            String args = null;
+            final int i = desc.indexOf(START_FMT);
+            if (i > 0) {
+                name = desc.substring(0, i).trim();
+                args = desc.substring(i + 1).trim();
+            }
+            final FormatFactory factory = registry.get(name);
+            if (factory != null) {
+                return factory.getFormat(name, args, getLocale());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Read the argument index from the current format element
+     *
+     * @param pattern pattern to parse
+     * @param pos current parse position
+     * @return argument index
+     */
+    private int readArgumentIndex(final String pattern, final ParsePosition pos) {
+        final int start = pos.getIndex();
+        seekNonWs(pattern, pos);
+        final StringBuilder result = new StringBuilder();
+        boolean error = false;
+        for (; !error && pos.getIndex() < pattern.length(); next(pos)) {
+            char c = pattern.charAt(pos.getIndex());
+            if (Character.isWhitespace(c)) {
+                seekNonWs(pattern, pos);
+                c = pattern.charAt(pos.getIndex());
+                if (c != START_FMT && c != END_FE) {
+                    error = true;
+                    continue;
+                }
+            }
+            if ((c == START_FMT || c == END_FE) && result.length() > 0) {
+                try {
+                    return Integer.parseInt(result.toString());
+                } catch (final NumberFormatException e) { // NOPMD
+                    // we've already ensured only digits, so unless something
+                    // outlandishly large was specified we should be okay.
+                }
+            }
+            error = !Character.isDigit(c);
+            result.append(c);
+        }
+        if (error) {
+            throw new IllegalArgumentException(
+                    "Invalid format argument index at position " + start + ": "
+                            + pattern.substring(start, pos.getIndex()));
+        }
+        throw new IllegalArgumentException(
+                "Unterminated format element at position " + start);
+    }
+
+    /**
+     * Parse the format component of a format element.
+     *
+     * @param pattern string to parse
+     * @param pos current parse position
+     * @return Format description String
+     */
+    private String parseFormatDescription(final String pattern, final ParsePosition pos) {
+        final int start = pos.getIndex();
+        seekNonWs(pattern, pos);
+        final int text = pos.getIndex();
+        int depth = 1;
+        for (; pos.getIndex() < pattern.length(); next(pos)) {
+            switch (pattern.charAt(pos.getIndex())) {
+            case START_FE:
+                depth++;
+                break;
+            case END_FE:
+                depth--;
+                if (depth == 0) {
+                    return pattern.substring(text, pos.getIndex());
+                }
+                break;
+            case QUOTE:
+                getQuotedString(pattern, pos);
+                break;
+            default:
+                break;
+            }
+        }
+        throw new IllegalArgumentException(
+                "Unterminated format element at position " + start);
+    }
+
+    /**
+     * Insert formats back into the pattern for toPattern() support.
+     *
+     * @param pattern source
+     * @param customPatterns The custom patterns to re-insert, if any
+     * @return full pattern
+     */
+    private String insertFormats(final String pattern, final ArrayList<String> customPatterns) {
+        if (!containsElements(customPatterns)) {
+            return pattern;
+        }
+        final StringBuilder sb = new StringBuilder(pattern.length() * 2);
+        final ParsePosition pos = new ParsePosition(0);
+        int fe = -1;
+        int depth = 0;
+        while (pos.getIndex() < pattern.length()) {
+            final char c = pattern.charAt(pos.getIndex());
+            switch (c) {
+            case QUOTE:
+                appendQuotedString(pattern, pos, sb);
+                break;
+            case START_FE:
+                depth++;
+                sb.append(START_FE).append(readArgumentIndex(pattern, next(pos)));
+                // do not look for custom patterns when they are embedded, e.g. in a choice
+                if (depth == 1) {
+                    fe++;
+                    final String customPattern = customPatterns.get(fe);
+                    if (customPattern != null) {
+                        sb.append(START_FMT).append(customPattern);
+                    }
+                }
+                break;
+            case END_FE:
+                depth--;
+                //$FALL-THROUGH$
+            default:
+                sb.append(c);
+                next(pos);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Consume whitespace from the current parse position.
+     *
+     * @param pattern String to read
+     * @param pos current position
+     */
+    private void seekNonWs(final String pattern, final ParsePosition pos) {
+        int len = 0;
+        final char[] buffer = pattern.toCharArray();
+        do {
+            len = StrMatcher.splitMatcher().isMatch(buffer, pos.getIndex());
+            pos.setIndex(pos.getIndex() + len);
+        } while (len > 0 && pos.getIndex() < pattern.length());
+    }
+
+    /**
+     * Convenience method to advance parse position by 1
+     *
+     * @param pos ParsePosition
+     * @return <code>pos</code>
+     */
+    private ParsePosition next(final ParsePosition pos) {
+        pos.setIndex(pos.getIndex() + 1);
+        return pos;
+    }
+
+    /**
+     * Consume a quoted string, adding it to <code>appendTo</code> if
+     * specified.
+     *
+     * @param pattern pattern to parse
+     * @param pos current parse position
+     * @param appendTo optional StringBuilder to append
+     * @return <code>appendTo</code>
+     */
+    private StringBuilder appendQuotedString(final String pattern, final ParsePosition pos,
+            final StringBuilder appendTo) {
+        assert pattern.toCharArray()[pos.getIndex()] == QUOTE : 
+            "Quoted string must start with quote character";
+
+        // handle quote character at the beginning of the string
+        if(appendTo != null) {
+            appendTo.append(QUOTE);
+        }
+        next(pos);
+
+        final int start = pos.getIndex();
+        final char[] c = pattern.toCharArray();
+        final int lastHold = start;
+        for (int i = pos.getIndex(); i < pattern.length(); i++) {
+            switch (c[pos.getIndex()]) {
+            case QUOTE:
+                next(pos);
+                return appendTo == null ? null : appendTo.append(c, lastHold,
+                        pos.getIndex() - lastHold);
+            default:
+                next(pos);
+            }
+        }
+        throw new IllegalArgumentException(
+                "Unterminated quoted string at position " + start);
+    }
+
+    /**
+     * Consume quoted string only
+     *
+     * @param pattern pattern to parse
+     * @param pos current parse position
+     */
+    private void getQuotedString(final String pattern, final ParsePosition pos) {
+        appendQuotedString(pattern, pos, null);
+    }
+
+    /**
+     * Learn whether the specified Collection contains non-null elements.
+     * @param coll to check
+     * @return <code>true</code> if some Object was found, <code>false</code> otherwise.
+     */
+    private boolean containsElements(final Collection<?> coll) {
+        if (coll == null || coll.isEmpty()) {
+            return false;
+        }
+        for (final Object name : coll) {
+            if (name != null) {
+                return true;
+            }
+        }
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/FormatFactory.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/FormatFactory.java b/src/main/java/org/apache/commons/text/FormatFactory.java
new file mode 100644
index 0000000..1f85b0f
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/FormatFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.text.Format;
+import java.util.Locale;
+
+/**
+ * Format factory.
+ */
+public interface FormatFactory {
+
+    /**
+     * Create or retrieve a format instance.
+     *
+     * @param name The format type name
+     * @param arguments Arguments used to create the format instance. This allows the
+     *                  <code>FormatFactory</code> to implement the "format style"
+     *                  concept from <code>java.text.MessageFormat</code>.
+     * @param locale The locale, may be null
+     * @return The format instance
+     */
+    Format getFormat(String name, String arguments, Locale locale);
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/main/java/org/apache/commons/text/FormattableUtils.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/commons/text/FormattableUtils.java b/src/main/java/org/apache/commons/text/FormattableUtils.java
new file mode 100644
index 0000000..887f593
--- /dev/null
+++ b/src/main/java/org/apache/commons/text/FormattableUtils.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import java.util.Formattable;
+import java.util.Formatter;
+
+import static java.util.FormattableFlags.LEFT_JUSTIFY;
+
+/**
+ * <p>Provides utilities for working with the {@code Formattable} interface.</p>
+ * 
+ * <p>The {@link Formattable} interface provides basic control over formatting
+ * when using a {@code Formatter}. It is primarily concerned with numeric precision
+ * and padding, and is not designed to allow generalised alternate formats.</p>
+ *
+ */
+public class FormattableUtils {
+
+    /**
+     * A format that simply outputs the value as a string.
+     */
+    private static final String SIMPLEST_FORMAT = "%s";
+
+    /**
+     * <p>{@code FormattableUtils} instances should NOT be constructed in
+     * standard programming. Instead, the methods of the class should be invoked
+     * statically.</p>
+     * 
+     * <p>This constructor is public to permit tools that require a JavaBean
+     * instance to operate.</p>
+     */
+    public FormattableUtils() {
+        super();
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Get the default formatted representation of the specified
+     * {@code Formattable}.
+     * 
+     * @param formattable  the instance to convert to a string, not null
+     * @return the resulting string, not null
+     */
+    public static String toString(final Formattable formattable) {
+        return String.format(SIMPLEST_FORMAT, formattable);
+    }
+
+    /**
+     * Handles the common {@code Formattable} operations of truncate-pad-append,
+     * with no ellipsis on precision overflow, and padding width underflow with
+     * spaces.
+     * 
+     * @param seq  the string to handle, not null
+     * @param formatter  the destination formatter, not null
+     * @param flags  the flags for formatting, see {@code Formattable}
+     * @param width  the width of the output, see {@code Formattable}
+     * @param precision  the precision of the output, see {@code Formattable}
+     * @return the {@code formatter} instance, not null
+     */
+    public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width,
+            final int precision) {
+        return append(seq, formatter, flags, width, precision, ' ', null);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append,
+     * with no ellipsis on precision overflow.
+     * 
+     * @param seq  the string to handle, not null
+     * @param formatter  the destination formatter, not null
+     * @param flags  the flags for formatting, see {@code Formattable}
+     * @param width  the width of the output, see {@code Formattable}
+     * @param precision  the precision of the output, see {@code Formattable}
+     * @param padChar  the pad character to use
+     * @return the {@code formatter} instance, not null
+     */
+    public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width,
+            final int precision, final char padChar) {
+        return append(seq, formatter, flags, width, precision, padChar, null);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append,
+     * padding width underflow with spaces.
+     * 
+     * @param seq  the string to handle, not null
+     * @param formatter  the destination formatter, not null
+     * @param flags  the flags for formatting, see {@code Formattable}
+     * @param width  the width of the output, see {@code Formattable}
+     * @param precision  the precision of the output, see {@code Formattable}
+     * @param ellipsis  the ellipsis to use when precision dictates truncation, null or
+     *  empty causes a hard truncation
+     * @return the {@code formatter} instance, not null
+     */
+    public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width,
+            final int precision, final CharSequence ellipsis) {
+        return append(seq, formatter, flags, width, precision, ' ', ellipsis);
+    }
+
+    /**
+     * Handles the common {@link Formattable} operations of truncate-pad-append.
+     * 
+     * @param seq  the string to handle, not null
+     * @param formatter  the destination formatter, not null
+     * @param flags  the flags for formatting, see {@code Formattable}
+     * @param width  the width of the output, see {@code Formattable}
+     * @param precision  the precision of the output, see {@code Formattable}
+     * @param padChar  the pad character to use
+     * @param ellipsis  the ellipsis to use when precision dictates truncation, null or
+     *  empty causes a hard truncation
+     * @return the {@code formatter} instance, not null
+     */
+    public static Formatter append(final CharSequence seq, final Formatter formatter, final int flags, final int width,
+            final int precision, final char padChar, final CharSequence ellipsis) {
+        if ( ! (ellipsis == null || precision < 0 || ellipsis.length() <= precision) ) {
+            throw new IllegalArgumentException(String.format("Specified ellipsis '%1$s' exceeds precision of %2$s", ellipsis, Integer.valueOf(precision)));
+        }
+        final StringBuilder buf = new StringBuilder(seq);
+        if (precision >= 0 && precision < seq.length()) {
+            final CharSequence _ellipsis;
+            if (ellipsis == null) {
+                _ellipsis = "";
+            } else {
+                _ellipsis = ellipsis;
+            }
+            buf.replace(precision - _ellipsis.length(), seq.length(), _ellipsis.toString());
+        }
+        final boolean leftJustify = (flags & LEFT_JUSTIFY) == LEFT_JUSTIFY;
+        for (int i = buf.length(); i < width; i++) {
+            buf.insert(leftJustify ? i : 0, padChar);
+        }
+        formatter.format(buf.toString());
+        return formatter;
+    }
+
+}


[04/14] [text] TEXT-23: Adding remainder of the o.a.c.l.text package

Posted by ch...@apache.org.
http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrSubstitutorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrSubstitutorTest.java b/src/test/java/org/apache/commons/text/StrSubstitutorTest.java
new file mode 100644
index 0000000..e382691
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrSubstitutorTest.java
@@ -0,0 +1,740 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.commons.lang3.mutable.MutableObject;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * Test class for {@link StrSubstitutor}.
+ */
+public class StrSubstitutorTest {
+
+    private Map<String, String> values;
+
+    @Before
+    public void setUp() throws Exception {
+        values = new HashMap<>();
+        values.put("animal", "quick brown fox");
+        values.put("target", "lazy dog");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        values = null;
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Tests simple key replace.
+     */
+    @Test
+    public void testReplaceSimple() {
+        doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests simple key replace.
+     */
+    @Test
+    public void testReplaceSolo() {
+        doTestReplace("quick brown fox", "${animal}", false);
+    }
+
+    /**
+     * Tests replace with no variables.
+     */
+    @Test
+    public void testReplaceNoVariables() {
+        doTestNoReplace("The balloon arrived.");
+    }
+
+    /**
+     * Tests replace with null.
+     */
+    @Test
+    public void testReplaceNull() {
+        doTestNoReplace(null);
+    }
+
+    /**
+     * Tests replace with null.
+     */
+    @Test
+    public void testReplaceEmpty() {
+        doTestNoReplace("");
+    }
+
+    /**
+     * Tests key replace changing map after initialization (not recommended).
+     */
+    @Test
+    public void testReplaceChangedMap() {
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        values.put("target", "moon");
+        assertEquals("The quick brown fox jumps over the moon.", sub.replace("The ${animal} jumps over the ${target}."));
+    }
+
+    /**
+     * Tests unknown key replace.
+     */
+    @Test
+    public void testReplaceUnknownKey() {
+        doTestReplace("The ${person} jumps over the lazy dog.", "The ${person} jumps over the ${target}.", true);
+        doTestReplace("The ${person} jumps over the lazy dog. 1234567890.", "The ${person} jumps over the ${target}. ${undefined.number:-1234567890}.", true);
+    }
+
+    /**
+     * Tests adjacent keys.
+     */
+    @Test
+    public void testReplaceAdjacentAtStart() {
+        values.put("code", "GBP");
+        values.put("amount", "12.50");
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        assertEquals("GBP12.50 charged", sub.replace("${code}${amount} charged"));
+    }
+
+    /**
+     * Tests adjacent keys.
+     */
+    @Test
+    public void testReplaceAdjacentAtEnd() {
+        values.put("code", "GBP");
+        values.put("amount", "12.50");
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        assertEquals("Amount is GBP12.50", sub.replace("Amount is ${code}${amount}"));
+    }
+
+    /**
+     * Tests simple recursive replace.
+     */
+    @Test
+    public void testReplaceRecursive() {
+        values.put("animal", "${critter}");
+        values.put("target", "${pet}");
+        values.put("pet", "${petCharacteristic} dog");
+        values.put("petCharacteristic", "lazy");
+        values.put("critter", "${critterSpeed} ${critterColor} ${critterType}");
+        values.put("critterSpeed", "quick");
+        values.put("critterColor", "brown");
+        values.put("critterType", "fox");
+        doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
+
+        values.put("pet", "${petCharacteristicUnknown:-lazy} dog");
+        doTestReplace("The quick brown fox jumps over the lazy dog.", "The ${animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests escaping.
+     */
+    @Test
+    public void testReplaceEscaping() {
+        doTestReplace("The ${animal} jumps over the lazy dog.", "The $${animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests escaping.
+     */
+    @Test
+    public void testReplaceSoloEscaping() {
+        doTestReplace("${animal}", "$${animal}", false);
+    }
+
+    /**
+     * Tests complex escaping.
+     */
+    @Test
+    public void testReplaceComplexEscaping() {
+        doTestReplace("The ${quick brown fox} jumps over the lazy dog.", "The $${${animal}} jumps over the ${target}.", true);
+        doTestReplace("The ${quick brown fox} jumps over the lazy dog. ${1234567890}.", "The $${${animal}} jumps over the ${target}. $${${undefined.number:-1234567890}}.", true);
+    }
+
+    /**
+     * Tests when no prefix or suffix.
+     */
+    @Test
+    public void testReplaceNoPrefixNoSuffix() {
+        doTestReplace("The animal jumps over the lazy dog.", "The animal jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests when no incomplete prefix.
+     */
+    @Test
+    public void testReplaceIncompletePrefix() {
+        doTestReplace("The {animal} jumps over the lazy dog.", "The {animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests when prefix but no suffix.
+     */
+    @Test
+    public void testReplacePrefixNoSuffix() {
+        doTestReplace("The ${animal jumps over the ${target} lazy dog.", "The ${animal jumps over the ${target} ${target}.", true);
+    }
+
+    /**
+     * Tests when suffix but no prefix.
+     */
+    @Test
+    public void testReplaceNoPrefixSuffix() {
+        doTestReplace("The animal} jumps over the lazy dog.", "The animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests when no variable name.
+     */
+    @Test
+    public void testReplaceEmptyKeys() {
+        doTestReplace("The ${} jumps over the lazy dog.", "The ${} jumps over the ${target}.", true);
+        doTestReplace("The animal jumps over the lazy dog.", "The ${:-animal} jumps over the ${target}.", true);
+    }
+
+    /**
+     * Tests replace creates output same as input.
+     */
+    @Test
+    public void testReplaceToIdentical() {
+        values.put("animal", "$${${thing}}");
+        values.put("thing", "animal");
+        doTestReplace("The ${animal} jumps.", "The ${animal} jumps.", true);
+    }
+
+    /**
+     * Tests a cyclic replace operation.
+     * The cycle should be detected and cause an exception to be thrown.
+     */
+    @Test
+    public void testCyclicReplacement() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("animal", "${critter}");
+        map.put("target", "${pet}");
+        map.put("pet", "${petCharacteristic} dog");
+        map.put("petCharacteristic", "lazy");
+        map.put("critter", "${critterSpeed} ${critterColor} ${critterType}");
+        map.put("critterSpeed", "quick");
+        map.put("critterColor", "brown");
+        map.put("critterType", "${animal}");
+        StrSubstitutor sub = new StrSubstitutor(map);
+        try {
+            sub.replace("The ${animal} jumps over the ${target}.");
+            fail("Cyclic replacement was not detected!");
+        } catch (final IllegalStateException ex) {
+            // expected
+        }
+
+        // also check even when default value is set.
+        map.put("critterType", "${animal:-fox}");
+        sub = new StrSubstitutor(map);
+        try {
+            sub.replace("The ${animal} jumps over the ${target}.");
+            fail("Cyclic replacement was not detected!");
+        } catch (final IllegalStateException ex) {
+            // expected
+        }
+    }
+
+    /**
+     * Tests interpolation with weird boundary patterns.
+     */
+    @Test
+    public void testReplaceWeirdPattens() {
+        doTestNoReplace("");
+        doTestNoReplace("${}");
+        doTestNoReplace("${ }");
+        doTestNoReplace("${\t}");
+        doTestNoReplace("${\n}");
+        doTestNoReplace("${\b}");
+        doTestNoReplace("${");
+        doTestNoReplace("$}");
+        doTestNoReplace("}");
+        doTestNoReplace("${}$");
+        doTestNoReplace("${${");
+        doTestNoReplace("${${}}");
+        doTestNoReplace("${$${}}");
+        doTestNoReplace("${$$${}}");
+        doTestNoReplace("${$$${$}}");
+        doTestNoReplace("${${}}");
+        doTestNoReplace("${${ }}");
+    }
+
+    /**
+     * Tests simple key replace.
+     */
+    @Test
+    public void testReplacePartialString_noReplace() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertEquals("${animal} jumps", sub.replace("The ${animal} jumps over the ${target}.", 4, 15));
+    }
+
+    /**
+     * Tests whether a variable can be replaced in a variable name.
+     */
+    @Test
+    public void testReplaceInVariable() {
+        values.put("animal.1", "fox");
+        values.put("animal.2", "mouse");
+        values.put("species", "2");
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        sub.setEnableSubstitutionInVariables(true);
+        assertEquals(
+                "Wrong result (1)",
+                "The mouse jumps over the lazy dog.",
+                sub.replace("The ${animal.${species}} jumps over the ${target}."));
+        values.put("species", "1");
+        assertEquals(
+                "Wrong result (2)",
+                "The fox jumps over the lazy dog.",
+                sub.replace("The ${animal.${species}} jumps over the ${target}."));
+        assertEquals(
+                "Wrong result (3)",
+                "The fox jumps over the lazy dog.",
+                sub.replace("The ${unknown.animal.${unknown.species:-1}:-fox} jumps over the ${unknow.target:-lazy dog}."));
+    }
+
+    /**
+     * Tests whether substitution in variable names is disabled per default.
+     */
+    @Test
+    public void testReplaceInVariableDisabled() {
+        values.put("animal.1", "fox");
+        values.put("animal.2", "mouse");
+        values.put("species", "2");
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        assertEquals(
+                "Wrong result (1)",
+                "The ${animal.${species}} jumps over the lazy dog.",
+                sub.replace("The ${animal.${species}} jumps over the ${target}."));
+        assertEquals(
+                "Wrong result (2)",
+                "The ${animal.${species:-1}} jumps over the lazy dog.",
+                sub.replace("The ${animal.${species:-1}} jumps over the ${target}."));
+    }
+
+    /**
+     * Tests complex and recursive substitution in variable names.
+     */
+    @Test
+    public void testReplaceInVariableRecursive() {
+        values.put("animal.2", "brown fox");
+        values.put("animal.1", "white mouse");
+        values.put("color", "white");
+        values.put("species.white", "1");
+        values.put("species.brown", "2");
+        final StrSubstitutor sub = new StrSubstitutor(values);
+        sub.setEnableSubstitutionInVariables(true);
+        assertEquals(
+                "Wrong result (1)",
+                "The white mouse jumps over the lazy dog.",
+                sub.replace("The ${animal.${species.${color}}} jumps over the ${target}."));
+        assertEquals(
+                "Wrong result (2)",
+                "The brown fox jumps over the lazy dog.",
+                sub.replace("The ${animal.${species.${unknownColor:-brown}}} jumps over the ${target}."));
+    }
+
+    @Test
+    public void testDefaultValueDelimiters() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("animal", "fox");
+        map.put("target", "dog");
+
+        StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$');
+        assertEquals("The fox jumps over the lazy dog. 1234567890.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number:-1234567890}."));
+
+        sub = new StrSubstitutor(map, "${", "}", '$', "?:");
+        assertEquals("The fox jumps over the lazy dog. 1234567890.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number?:1234567890}."));
+
+        sub = new StrSubstitutor(map, "${", "}", '$', "||");
+        assertEquals("The fox jumps over the lazy dog. 1234567890.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number||1234567890}."));
+
+        sub = new StrSubstitutor(map, "${", "}", '$', "!");
+        assertEquals("The fox jumps over the lazy dog. 1234567890.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
+
+        sub = new StrSubstitutor(map, "${", "}", '$', "");
+        sub.setValueDelimiterMatcher(null);
+        assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
+
+        sub = new StrSubstitutor(map, "${", "}", '$');
+        sub.setValueDelimiterMatcher(null);
+        assertEquals("The fox jumps over the lazy dog. ${undefined.number!1234567890}.",
+                sub.replace("The ${animal} jumps over the lazy ${target}. ${undefined.number!1234567890}."));
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Tests protected.
+     */
+    @Test
+    public void testResolveVariable() {
+        final StrBuilder builder = new StrBuilder("Hi ${name}!");
+        final Map<String, String> map = new HashMap<>();
+        map.put("name", "commons");
+        final StrSubstitutor sub = new StrSubstitutor(map) {
+            @Override
+            protected String resolveVariable(final String variableName, final StrBuilder buf, final int startPos, final int endPos) {
+                assertEquals("name", variableName);
+                assertSame(builder, buf);
+                assertEquals(3, startPos);
+                assertEquals(10, endPos);
+                return "jakarta";
+            }
+        };
+        sub.replaceIn(builder);
+        assertEquals("Hi jakarta!", builder.toString());
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Tests constructor.
+     */
+    @Test
+    public void testConstructorNoArgs() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertEquals("Hi ${name}", sub.replace("Hi ${name}"));
+    }
+
+    /**
+     * Tests constructor.
+     */
+    @Test
+    public void testConstructorMapPrefixSuffix() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("name", "commons");
+        final StrSubstitutor sub = new StrSubstitutor(map, "<", ">");
+        assertEquals("Hi < commons", sub.replace("Hi $< <name>"));
+    }
+
+    /**
+     * Tests constructor.
+     */
+    @Test
+    public void testConstructorMapFull() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("name", "commons");
+        StrSubstitutor sub = new StrSubstitutor(map, "<", ">", '!');
+        assertEquals("Hi < commons", sub.replace("Hi !< <name>"));
+        sub = new StrSubstitutor(map, "<", ">", '!', "||");
+        assertEquals("Hi < commons", sub.replace("Hi !< <name2||commons>"));
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Tests get set.
+     */
+    @Test
+    public void testGetSetEscape() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertEquals('$', sub.getEscapeChar());
+        sub.setEscapeChar('<');
+        assertEquals('<', sub.getEscapeChar());
+    }
+
+    /**
+     * Tests get set.
+     */
+    @Test
+    public void testGetSetPrefix() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher);
+        sub.setVariablePrefix('<');
+        assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.CharMatcher);
+
+        sub.setVariablePrefix("<<");
+        assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher);
+        try {
+            sub.setVariablePrefix((String) null);
+            fail();
+        } catch (final IllegalArgumentException ex) {
+            // expected
+        }
+        assertTrue(sub.getVariablePrefixMatcher() instanceof StrMatcher.StringMatcher);
+
+        final StrMatcher matcher = StrMatcher.commaMatcher();
+        sub.setVariablePrefixMatcher(matcher);
+        assertSame(matcher, sub.getVariablePrefixMatcher());
+        try {
+            sub.setVariablePrefixMatcher((StrMatcher) null);
+            fail();
+        } catch (final IllegalArgumentException ex) {
+            // expected
+        }
+        assertSame(matcher, sub.getVariablePrefixMatcher());
+    }
+
+    /**
+     * Tests get set.
+     */
+    @Test
+    public void testGetSetSuffix() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher);
+        sub.setVariableSuffix('<');
+        assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.CharMatcher);
+
+        sub.setVariableSuffix("<<");
+        assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher);
+        try {
+            sub.setVariableSuffix((String) null);
+            fail();
+        } catch (final IllegalArgumentException ex) {
+            // expected
+        }
+        assertTrue(sub.getVariableSuffixMatcher() instanceof StrMatcher.StringMatcher);
+
+        final StrMatcher matcher = StrMatcher.commaMatcher();
+        sub.setVariableSuffixMatcher(matcher);
+        assertSame(matcher, sub.getVariableSuffixMatcher());
+        try {
+            sub.setVariableSuffixMatcher((StrMatcher) null);
+            fail();
+        } catch (final IllegalArgumentException ex) {
+            // expected
+        }
+        assertSame(matcher, sub.getVariableSuffixMatcher());
+    }
+
+    /**
+     * Tests get set.
+     */
+    @Test
+    public void testGetSetValueDelimiter() {
+        final StrSubstitutor sub = new StrSubstitutor();
+        assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher);
+        sub.setValueDelimiter(':');
+        assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.CharMatcher);
+
+        sub.setValueDelimiter("||");
+        assertTrue(sub.getValueDelimiterMatcher() instanceof StrMatcher.StringMatcher);
+        sub.setValueDelimiter((String) null);
+        assertNull(sub.getValueDelimiterMatcher());
+
+        final StrMatcher matcher = StrMatcher.commaMatcher();
+        sub.setValueDelimiterMatcher(matcher);
+        assertSame(matcher, sub.getValueDelimiterMatcher());
+        sub.setValueDelimiterMatcher((StrMatcher) null);
+        assertNull(sub.getValueDelimiterMatcher());
+    }
+
+    //-----------------------------------------------------------------------
+    /**
+     * Tests static.
+     */
+    @Test
+    public void testStaticReplace() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("name", "commons");
+        assertEquals("Hi commons!", StrSubstitutor.replace("Hi ${name}!", map));
+    }
+
+    /**
+     * Tests static.
+     */
+    @Test
+    public void testStaticReplacePrefixSuffix() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("name", "commons");
+        assertEquals("Hi commons!", StrSubstitutor.replace("Hi <name>!", map, "<", ">"));
+    }
+
+    /**
+     * Tests interpolation with system properties.
+     */
+    @Test
+    public void testStaticReplaceSystemProperties() {
+        final StrBuilder buf = new StrBuilder();
+        buf.append("Hi ").append(System.getProperty("user.name"));
+        buf.append(", you are working with ");
+        buf.append(System.getProperty("os.name"));
+        buf.append(", your home directory is ");
+        buf.append(System.getProperty("user.home")).append('.');
+        assertEquals(buf.toString(), StrSubstitutor.replaceSystemProperties("Hi ${user.name}, you are "
+            + "working with ${os.name}, your home "
+            + "directory is ${user.home}."));
+    }
+
+    /**
+     * Test for LANG-1055: StrSubstitutor.replaceSystemProperties does not work consistently
+     */
+    @Test
+    public void testLANG1055() {
+        System.setProperty("test_key",  "test_value");
+
+        final String expected = StrSubstitutor.replace("test_key=${test_key}", System.getProperties());
+        final String actual = StrSubstitutor.replaceSystemProperties("test_key=${test_key}");
+        assertEquals(expected, actual);
+    }
+
+    /**
+     * Test the replace of a properties object
+     */
+    @Test
+    public void testSubstituteDefaultProperties(){
+        final String org = "${doesnotwork}";
+        System.setProperty("doesnotwork", "It works!");
+
+        // create a new Properties object with the System.getProperties as default
+        final Properties props = new Properties(System.getProperties());
+
+        assertEquals("It works!", StrSubstitutor.replace(org, props));
+    }
+
+    @Test
+    public void testSamePrefixAndSuffix() {
+        final Map<String, String> map = new HashMap<>();
+        map.put("greeting", "Hello");
+        map.put(" there ", "XXX");
+        map.put("name", "commons");
+        assertEquals("Hi commons!", StrSubstitutor.replace("Hi @name@!", map, "@", "@"));
+        assertEquals("Hello there commons!", StrSubstitutor.replace("@greeting@ there @name@!", map, "@", "@"));
+    }
+
+    @Test
+    public void testSubstitutePreserveEscape() {
+        final String org = "${not-escaped} $${escaped}";
+        final Map<String, String> map = new HashMap<>();
+        map.put("not-escaped", "value");
+
+        final StrSubstitutor sub = new StrSubstitutor(map, "${", "}", '$');
+        assertFalse(sub.isPreserveEscapes());
+        assertEquals("value ${escaped}", sub.replace(org));
+
+        sub.setPreserveEscapes(true);
+        assertTrue(sub.isPreserveEscapes());
+        assertEquals("value $${escaped}", sub.replace(org));
+    }
+
+    //-----------------------------------------------------------------------
+    private void doTestReplace(final String expectedResult, final String replaceTemplate, final boolean substring) {
+        final String expectedShortResult = expectedResult.substring(1, expectedResult.length() - 1);
+        final StrSubstitutor sub = new StrSubstitutor(values);
+
+        // replace using String
+        assertEquals(expectedResult, sub.replace(replaceTemplate));
+        if (substring) {
+            assertEquals(expectedShortResult, sub.replace(replaceTemplate, 1, replaceTemplate.length() - 2));
+        }
+
+        // replace using char[]
+        final char[] chars = replaceTemplate.toCharArray();
+        assertEquals(expectedResult, sub.replace(chars));
+        if (substring) {
+            assertEquals(expectedShortResult, sub.replace(chars, 1, chars.length - 2));
+        }
+
+        // replace using StringBuffer
+        StringBuffer buf = new StringBuffer(replaceTemplate);
+        assertEquals(expectedResult, sub.replace(buf));
+        if (substring) {
+            assertEquals(expectedShortResult, sub.replace(buf, 1, buf.length() - 2));
+        }
+
+        // replace using StringBuilder
+        StringBuilder builder = new StringBuilder(replaceTemplate);
+        assertEquals(expectedResult, sub.replace(builder));
+        if (substring) {
+            assertEquals(expectedShortResult, sub.replace(builder, 1, builder.length() - 2));
+        }
+
+        // replace using StrBuilder
+        StrBuilder bld = new StrBuilder(replaceTemplate);
+        assertEquals(expectedResult, sub.replace(bld));
+        if (substring) {
+            assertEquals(expectedShortResult, sub.replace(bld, 1, bld.length() - 2));
+        }
+
+        // replace using object
+        final MutableObject<String> obj = new MutableObject<>(replaceTemplate);  // toString returns template
+        assertEquals(expectedResult, sub.replace(obj));
+
+        // replace in StringBuffer
+        buf = new StringBuffer(replaceTemplate);
+        assertTrue(sub.replaceIn(buf));
+        assertEquals(expectedResult, buf.toString());
+        if (substring) {
+            buf = new StringBuffer(replaceTemplate);
+            assertTrue(sub.replaceIn(buf, 1, buf.length() - 2));
+            assertEquals(expectedResult, buf.toString());  // expect full result as remainder is untouched
+        }
+
+        // replace in StringBuilder
+        builder = new StringBuilder(replaceTemplate);
+        assertTrue(sub.replaceIn(builder));
+        assertEquals(expectedResult, builder.toString());
+        if (substring) {
+            builder = new StringBuilder(replaceTemplate);
+            assertTrue(sub.replaceIn(builder, 1, builder.length() - 2));
+            assertEquals(expectedResult, builder.toString());  // expect full result as remainder is untouched
+        }
+
+        // replace in StrBuilder
+        bld = new StrBuilder(replaceTemplate);
+        assertTrue(sub.replaceIn(bld));
+        assertEquals(expectedResult, bld.toString());
+        if (substring) {
+            bld = new StrBuilder(replaceTemplate);
+            assertTrue(sub.replaceIn(bld, 1, bld.length() - 2));
+            assertEquals(expectedResult, bld.toString());  // expect full result as remainder is untouched
+        }
+    }
+
+    private void doTestNoReplace(final String replaceTemplate) {
+        final StrSubstitutor sub = new StrSubstitutor(values);
+
+        if (replaceTemplate == null) {
+            assertEquals(null, sub.replace((String) null));
+            assertEquals(null, sub.replace((String) null, 0, 100));
+            assertEquals(null, sub.replace((char[]) null));
+            assertEquals(null, sub.replace((char[]) null, 0, 100));
+            assertEquals(null, sub.replace((StringBuffer) null));
+            assertEquals(null, sub.replace((StringBuffer) null, 0, 100));
+            assertEquals(null, sub.replace((StrBuilder) null));
+            assertEquals(null, sub.replace((StrBuilder) null, 0, 100));
+            assertEquals(null, sub.replace((Object) null));
+            assertFalse(sub.replaceIn((StringBuffer) null));
+            assertFalse(sub.replaceIn((StringBuffer) null, 0, 100));
+            assertFalse(sub.replaceIn((StrBuilder) null));
+            assertFalse(sub.replaceIn((StrBuilder) null, 0, 100));
+        } else {
+            assertEquals(replaceTemplate, sub.replace(replaceTemplate));
+            final StrBuilder bld = new StrBuilder(replaceTemplate);
+            assertFalse(sub.replaceIn(bld));
+            assertEquals(replaceTemplate, bld.toString());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/StrTokenizerTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/StrTokenizerTest.java b/src/test/java/org/apache/commons/text/StrTokenizerTest.java
new file mode 100644
index 0000000..4b39f55
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/StrTokenizerTest.java
@@ -0,0 +1,879 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+/**
+ * Unit test for {@link StrTokenizer}.
+ */
+public class StrTokenizerTest {
+
+    private static final String CSV_SIMPLE_FIXTURE = "A,b,c";
+
+    private static final String TSV_SIMPLE_FIXTURE = "A\tb\tc";
+
+    private void checkClone(final StrTokenizer tokenizer) {
+        assertFalse(StrTokenizer.getCSVInstance() == tokenizer);
+        assertFalse(StrTokenizer.getTSVInstance() == tokenizer);
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void test1() {
+
+        final String input = "a;b;c;\"d;\"\"e\";f; ; ;  ";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", "c", "d;\"e", "f", "", "", "",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test2() {
+
+        final String input = "a;b;c ;\"d;\"\"e\";f; ; ;";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.noneMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", "c ", "d;\"e", "f", " ", " ", "",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test3() {
+
+        final String input = "a;b; c;\"d;\"\"e\";f; ; ;";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.noneMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", " c", "d;\"e", "f", " ", " ", "",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test4() {
+
+        final String input = "a;b; c;\"d;\"\"e\";f; ; ;";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(true);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", "c", "d;\"e", "f",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test5() {
+
+        final String input = "a;b; c;\"d;\"\"e\";f; ; ;";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", "c", "d;\"e", "f", null, null, null,};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test6() {
+
+        final String input = "a;b; c;\"d;\"\"e\";f; ; ;";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterChar(';');
+        tok.setQuoteChar('"');
+        tok.setIgnoredMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        // tok.setTreatingEmptyAsNull(true);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", " c", "d;\"e", "f", null, null, null,};
+
+        int nextCount = 0;
+        while (tok.hasNext()) {
+            tok.next();
+            nextCount++;
+        }
+
+        int prevCount = 0;
+        while (tok.hasPrevious()) {
+            tok.previous();
+            prevCount++;
+        }
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+
+        assertTrue("could not cycle through entire token list" + " using the 'hasNext' and 'next' methods",
+                nextCount == expected.length);
+
+        assertTrue("could not cycle through entire token list" + " using the 'hasPrevious' and 'previous' methods",
+                prevCount == expected.length);
+
+    }
+
+    @Test
+    public void test7() {
+
+        final String input = "a   b c \"d e\" f ";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterMatcher(StrMatcher.spaceMatcher());
+        tok.setQuoteMatcher(StrMatcher.doubleQuoteMatcher());
+        tok.setIgnoredMatcher(StrMatcher.noneMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "", "", "b", "c", "d e", "f", "",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void test8() {
+
+        final String input = "a   b c \"d e\" f ";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setDelimiterMatcher(StrMatcher.spaceMatcher());
+        tok.setQuoteMatcher(StrMatcher.doubleQuoteMatcher());
+        tok.setIgnoredMatcher(StrMatcher.noneMatcher());
+        tok.setIgnoreEmptyTokens(true);
+        final String tokens[] = tok.getTokenArray();
+
+        final String expected[] = new String[]{"a", "b", "c", "d e", "f",};
+
+        assertEquals(ArrayUtils.toString(tokens), expected.length, tokens.length);
+        for (int i = 0; i < expected.length; i++) {
+            assertEquals("token[" + i + "] was '" + tokens[i] + "' but was expected to be '" + expected[i] + "'",
+                    expected[i], tokens[i]);
+        }
+
+    }
+
+    @Test
+    public void testBasic1() {
+        final String input = "a  b c";
+        final StrTokenizer tok = new StrTokenizer(input);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasic2() {
+        final String input = "a \nb\fc";
+        final StrTokenizer tok = new StrTokenizer(input);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasic3() {
+        final String input = "a \nb\u0001\fc";
+        final StrTokenizer tok = new StrTokenizer(input);
+        assertEquals("a", tok.next());
+        assertEquals("b\u0001", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasic4() {
+        final String input = "a \"b\" c";
+        final StrTokenizer tok = new StrTokenizer(input);
+        assertEquals("a", tok.next());
+        assertEquals("\"b\"", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasic5() {
+        final String input = "a:b':c";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        assertEquals("a", tok.next());
+        assertEquals("b'", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicDelim1() {
+        final String input = "a:b:c";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicDelim2() {
+        final String input = "a:b:c";
+        final StrTokenizer tok = new StrTokenizer(input, ',');
+        assertEquals("a:b:c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicEmpty1() {
+        final String input = "a  b c";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setIgnoreEmptyTokens(false);
+        assertEquals("a", tok.next());
+        assertEquals("", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicEmpty2() {
+        final String input = "a  b c";
+        final StrTokenizer tok = new StrTokenizer(input);
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals(null, tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted1() {
+        final String input = "a 'b' c";
+        final StrTokenizer tok = new StrTokenizer(input, ' ', '\'');
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted2() {
+        final String input = "a:'b':";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted3() {
+        final String input = "a:'b''c'";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b'c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted4() {
+        final String input = "a: 'b' 'c' :d";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b c", tok.next());
+        assertEquals("d", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted5() {
+        final String input = "a: 'b'x'c' :d";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("bxc", tok.next());
+        assertEquals("d", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted6() {
+        final String input = "a:'b'\"c':d";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setQuoteMatcher(StrMatcher.quoteMatcher());
+        assertEquals("a", tok.next());
+        assertEquals("b\"c:d", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuoted7() {
+        final String input = "a:\"There's a reason here\":b";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setQuoteMatcher(StrMatcher.quoteMatcher());
+        assertEquals("a", tok.next());
+        assertEquals("There's a reason here", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicQuotedTrimmed1() {
+        final String input = "a: 'b' :";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicTrimmed1() {
+        final String input = "a: b :  ";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicTrimmed2() {
+        final String input = "a:  b  :";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setTrimmerMatcher(StrMatcher.stringMatcher("  "));
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicIgnoreTrimmed1() {
+        final String input = "a: bIGNOREc : ";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE"));
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("bc", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicIgnoreTrimmed2() {
+        final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE ";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE"));
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("bc", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicIgnoreTrimmed3() {
+        final String input = "IGNOREaIGNORE: IGNORE bIGNOREc IGNORE : IGNORE ";
+        final StrTokenizer tok = new StrTokenizer(input, ':');
+        tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE"));
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("  bc  ", tok.next());
+        assertEquals("  ", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    @Test
+    public void testBasicIgnoreTrimmed4() {
+        final String input = "IGNOREaIGNORE: IGNORE 'bIGNOREc'IGNORE'd' IGNORE : IGNORE ";
+        final StrTokenizer tok = new StrTokenizer(input, ':', '\'');
+        tok.setIgnoredMatcher(StrMatcher.stringMatcher("IGNORE"));
+        tok.setTrimmerMatcher(StrMatcher.trimMatcher());
+        tok.setIgnoreEmptyTokens(false);
+        tok.setEmptyTokenAsNull(true);
+        assertEquals("a", tok.next());
+        assertEquals("bIGNOREcd", tok.next());
+        assertEquals(null, tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testListArray() {
+        final String input = "a  b c";
+        final StrTokenizer tok = new StrTokenizer(input);
+        final String[] array = tok.getTokenArray();
+        final List<?> list = tok.getTokenList();
+        
+        assertEquals(Arrays.asList(array), list);
+        assertEquals(3, list.size());
+    }
+
+    //-----------------------------------------------------------------------
+    private void testCSV(final String data) {
+        this.testXSVAbc(StrTokenizer.getCSVInstance(data));
+        this.testXSVAbc(StrTokenizer.getCSVInstance(data.toCharArray()));
+    }
+
+    @Test
+    public void testCSVEmpty() {
+        this.testEmpty(StrTokenizer.getCSVInstance());
+        this.testEmpty(StrTokenizer.getCSVInstance(""));
+    }
+
+    @Test
+    public void testCSVSimple() {
+        this.testCSV(CSV_SIMPLE_FIXTURE);
+    }
+
+    @Test
+    public void testCSVSimpleNeedsTrim() {
+        this.testCSV("   " + CSV_SIMPLE_FIXTURE);
+        this.testCSV("   \n\t  " + CSV_SIMPLE_FIXTURE);
+        this.testCSV("   \n  " + CSV_SIMPLE_FIXTURE + "\n\n\r");
+    }
+
+    void testEmpty(final StrTokenizer tokenizer) {
+        this.checkClone(tokenizer);
+        assertFalse(tokenizer.hasNext());
+        assertFalse(tokenizer.hasPrevious());
+        assertEquals(null, tokenizer.nextToken());
+        assertEquals(0, tokenizer.size());
+        try {
+            tokenizer.next();
+            fail();
+        } catch (final NoSuchElementException ex) {}
+    }
+
+    @Test
+    public void testGetContent() {
+        final String input = "a   b c \"d e\" f ";
+        StrTokenizer tok = new StrTokenizer(input);
+        assertEquals(input, tok.getContent());
+
+        tok = new StrTokenizer(input.toCharArray());
+        assertEquals(input, tok.getContent());
+        
+        tok = new StrTokenizer();
+        assertEquals(null, tok.getContent());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testChaining() {
+        final StrTokenizer tok = new StrTokenizer();
+        assertEquals(tok, tok.reset());
+        assertEquals(tok, tok.reset(""));
+        assertEquals(tok, tok.reset(new char[0]));
+        assertEquals(tok, tok.setDelimiterChar(' '));
+        assertEquals(tok, tok.setDelimiterString(" "));
+        assertEquals(tok, tok.setDelimiterMatcher(null));
+        assertEquals(tok, tok.setQuoteChar(' '));
+        assertEquals(tok, tok.setQuoteMatcher(null));
+        assertEquals(tok, tok.setIgnoredChar(' '));
+        assertEquals(tok, tok.setIgnoredMatcher(null));
+        assertEquals(tok, tok.setTrimmerMatcher(null));
+        assertEquals(tok, tok.setEmptyTokenAsNull(false));
+        assertEquals(tok, tok.setIgnoreEmptyTokens(false));
+    }
+
+    /**
+     * Tests that the {@link StrTokenizer#clone()} clone method catches {@link CloneNotSupportedException} and returns
+     * <code>null</code>.
+     */
+    @Test
+    public void testCloneNotSupportedException() {
+        final Object notCloned = new StrTokenizer() {
+            @Override
+            Object cloneReset() throws CloneNotSupportedException {
+                throw new CloneNotSupportedException("test");
+            }
+        }.clone();
+        assertNull(notCloned);
+    }
+
+    @Test
+    public void testCloneNull() {
+        final StrTokenizer tokenizer = new StrTokenizer((char[]) null);
+        // Start sanity check
+        assertEquals(null, tokenizer.nextToken());
+        tokenizer.reset();
+        assertEquals(null, tokenizer.nextToken());
+        // End sanity check
+        final StrTokenizer clonedTokenizer = (StrTokenizer) tokenizer.clone();
+        tokenizer.reset();
+        assertEquals(null, tokenizer.nextToken());
+        assertEquals(null, clonedTokenizer.nextToken());
+    }
+
+    @Test
+    public void testCloneReset() {
+        final char[] input = new char[]{'a'};
+        final StrTokenizer tokenizer = new StrTokenizer(input);
+        // Start sanity check
+        assertEquals("a", tokenizer.nextToken());
+        tokenizer.reset(input);
+        assertEquals("a", tokenizer.nextToken());
+        // End sanity check
+        final StrTokenizer clonedTokenizer = (StrTokenizer) tokenizer.clone();
+        input[0] = 'b';
+        tokenizer.reset(input);
+        assertEquals("b", tokenizer.nextToken());
+        assertEquals("a", clonedTokenizer.nextToken());
+    }
+  
+    // -----------------------------------------------------------------------
+    @Test
+    public void testConstructor_String() {
+        StrTokenizer tok = new StrTokenizer("a b");
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer("");
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((String) null);
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor_String_char() {
+        StrTokenizer tok = new StrTokenizer("a b", ' ');
+        assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1));
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer("", ' ');
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((String) null, ' ');
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor_String_char_char() {
+        StrTokenizer tok = new StrTokenizer("a b", ' ', '"');
+        assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1));
+        assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1));
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer("", ' ', '"');
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((String) null, ' ', '"');
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor_charArray() {
+        StrTokenizer tok = new StrTokenizer("a b".toCharArray());
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer(new char[0]);
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((char[]) null);
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor_charArray_char() {
+        StrTokenizer tok = new StrTokenizer("a b".toCharArray(), ' ');
+        assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1));
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer(new char[0], ' ');
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((char[]) null, ' ');
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor_charArray_char_char() {
+        StrTokenizer tok = new StrTokenizer("a b".toCharArray(), ' ', '"');
+        assertEquals(1, tok.getDelimiterMatcher().isMatch(" ".toCharArray(), 0, 0, 1));
+        assertEquals(1, tok.getQuoteMatcher().isMatch("\"".toCharArray(), 0, 0, 1));
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer(new char[0], ' ', '"');
+        assertFalse(tok.hasNext());
+        
+        tok = new StrTokenizer((char[]) null, ' ', '"');
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReset() {
+        final StrTokenizer tok = new StrTokenizer("a b c");
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok.reset();
+        assertEquals("a", tok.next());
+        assertEquals("b", tok.next());
+        assertEquals("c", tok.next());
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReset_String() {
+        final StrTokenizer tok = new StrTokenizer("x x x");
+        tok.reset("d e");
+        assertEquals("d", tok.next());
+        assertEquals("e", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok.reset((String) null);
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testReset_charArray() {
+        final StrTokenizer tok = new StrTokenizer("x x x");
+        
+        final char[] array = new char[] {'a', 'b', 'c'};
+        tok.reset(array);
+        assertEquals("abc", tok.next());
+        assertFalse(tok.hasNext());
+        
+        tok.reset((char[]) null);
+        assertFalse(tok.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTSV() {
+        this.testXSVAbc(StrTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE));
+        this.testXSVAbc(StrTokenizer.getTSVInstance(TSV_SIMPLE_FIXTURE.toCharArray()));
+    }
+
+    @Test
+    public void testTSVEmpty() {
+        this.testEmpty(StrTokenizer.getTSVInstance());
+        this.testEmpty(StrTokenizer.getTSVInstance(""));
+    }
+
+    void testXSVAbc(final StrTokenizer tokenizer) {
+        this.checkClone(tokenizer);
+        assertEquals(-1, tokenizer.previousIndex());
+        assertEquals(0, tokenizer.nextIndex());
+        assertEquals(null, tokenizer.previousToken());
+        assertEquals("A", tokenizer.nextToken());
+        assertEquals(1, tokenizer.nextIndex());
+        assertEquals("b", tokenizer.nextToken());
+        assertEquals(2, tokenizer.nextIndex());
+        assertEquals("c", tokenizer.nextToken());
+        assertEquals(3, tokenizer.nextIndex());
+        assertEquals(null, tokenizer.nextToken());
+        assertEquals(3, tokenizer.nextIndex());
+        assertEquals("c", tokenizer.previousToken());
+        assertEquals(2, tokenizer.nextIndex());
+        assertEquals("b", tokenizer.previousToken());
+        assertEquals(1, tokenizer.nextIndex());
+        assertEquals("A", tokenizer.previousToken());
+        assertEquals(0, tokenizer.nextIndex());
+        assertEquals(null, tokenizer.previousToken());
+        assertEquals(0, tokenizer.nextIndex());
+        assertEquals(-1, tokenizer.previousIndex());
+        assertEquals(3, tokenizer.size());
+    }
+
+    @Test
+    public void testIteration() {
+        final StrTokenizer tkn = new StrTokenizer("a b c");
+        assertFalse(tkn.hasPrevious());
+        try {
+            tkn.previous();
+            fail();
+        } catch (final NoSuchElementException ex) {}
+        assertTrue(tkn.hasNext());
+        
+        assertEquals("a", tkn.next());
+        try {
+            tkn.remove();
+            fail();
+        } catch (final UnsupportedOperationException ex) {}
+        try {
+            tkn.set("x");
+            fail();
+        } catch (final UnsupportedOperationException ex) {}
+        try {
+            tkn.add("y");
+            fail();
+        } catch (final UnsupportedOperationException ex) {}
+        assertTrue(tkn.hasPrevious());
+        assertTrue(tkn.hasNext());
+        
+        assertEquals("b", tkn.next());
+        assertTrue(tkn.hasPrevious());
+        assertTrue(tkn.hasNext());
+        
+        assertEquals("c", tkn.next());
+        assertTrue(tkn.hasPrevious());
+        assertFalse(tkn.hasNext());
+        
+        try {
+            tkn.next();
+            fail();
+        } catch (final NoSuchElementException ex) {}
+        assertTrue(tkn.hasPrevious());
+        assertFalse(tkn.hasNext());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTokenizeSubclassInputChange() {
+        final StrTokenizer tkn = new StrTokenizer("a b c d e") {
+            @Override
+            protected List<String> tokenize(final char[] chars, final int offset, final int count) {
+                return super.tokenize("w x y z".toCharArray(), 2, 5);
+            }
+        };
+        assertEquals("x", tkn.next());
+        assertEquals("y", tkn.next());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testTokenizeSubclassOutputChange() {
+        final StrTokenizer tkn = new StrTokenizer("a b c") {
+            @Override
+            protected List<String> tokenize(final char[] chars, final int offset, final int count) {
+                final List<String> list = super.tokenize(chars, offset, count);
+                Collections.reverse(list);
+                return list;
+            }
+        };
+        assertEquals("c", tkn.next());
+        assertEquals("b", tkn.next());
+        assertEquals("a", tkn.next());
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testToString() {
+        final StrTokenizer tkn = new StrTokenizer("a b c d e");
+        assertEquals("StrTokenizer[not tokenized yet]", tkn.toString());
+        tkn.next();
+        assertEquals("StrTokenizer[a, b, c, d, e]", tkn.toString());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/commons-text/blob/6f24aa45/src/test/java/org/apache/commons/text/WordUtilsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/commons/text/WordUtilsTest.java b/src/test/java/org/apache/commons/text/WordUtilsTest.java
new file mode 100644
index 0000000..6b57072
--- /dev/null
+++ b/src/test/java/org/apache/commons/text/WordUtilsTest.java
@@ -0,0 +1,421 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ * 
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License 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 org.apache.commons.text;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+
+import org.apache.commons.lang3.SystemUtils;
+import org.junit.Test;
+
+/**
+ * Unit tests for {@link WordUtils} class.
+ */
+public class WordUtilsTest {
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testConstructor() {
+        assertNotNull(new WordUtils());
+        final Constructor<?>[] cons = WordUtils.class.getDeclaredConstructors();
+        assertEquals(1, cons.length);
+        assertTrue(Modifier.isPublic(cons[0].getModifiers()));
+        assertTrue(Modifier.isPublic(WordUtils.class.getModifiers()));
+        assertFalse(Modifier.isFinal(WordUtils.class.getModifiers()));
+    }
+    
+    //-----------------------------------------------------------------------
+    @Test
+    public void testWrap_StringInt() {
+        assertEquals(null, WordUtils.wrap(null, 20));
+        assertEquals(null, WordUtils.wrap(null, -1));
+        
+        assertEquals("", WordUtils.wrap("", 20));
+        assertEquals("", WordUtils.wrap("", -1));
+        
+        // normal
+        final String systemNewLine = SystemUtils.LINE_SEPARATOR;
+        String input = "Here is one line of text that is going to be wrapped after 20 columns.";
+        String expected = "Here is one line of" + systemNewLine + "text that is going" 
+            + systemNewLine + "to be wrapped after" + systemNewLine + "20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20));
+        
+        // long word at end
+        input = "Click here to jump to the commons website - http://commons.apache.org";
+        expected = "Click here to jump" + systemNewLine + "to the commons" + systemNewLine 
+            + "website -" + systemNewLine + "http://commons.apache.org";
+        assertEquals(expected, WordUtils.wrap(input, 20));
+        
+        // long word in middle
+        input = "Click here, http://commons.apache.org, to jump to the commons website";
+        expected = "Click here," + systemNewLine + "http://commons.apache.org," + systemNewLine 
+            + "to jump to the" + systemNewLine + "commons website";
+        assertEquals(expected, WordUtils.wrap(input, 20));
+
+        // leading spaces on a new line are stripped
+        // trailing spaces are not stripped
+        input = "word1             word2                        word3";
+        expected = "word1  " + systemNewLine + "word2  " + systemNewLine + "word3";
+        assertEquals(expected, WordUtils.wrap(input, 7));
+    }
+    
+    @Test
+    public void testWrap_StringIntStringBoolean() {
+        assertEquals(null, WordUtils.wrap(null, 20, "\n", false));
+        assertEquals(null, WordUtils.wrap(null, 20, "\n", true));
+        assertEquals(null, WordUtils.wrap(null, 20, null, true));
+        assertEquals(null, WordUtils.wrap(null, 20, null, false));
+        assertEquals(null, WordUtils.wrap(null, -1, null, true));
+        assertEquals(null, WordUtils.wrap(null, -1, null, false));
+        
+        assertEquals("", WordUtils.wrap("", 20, "\n", false));
+        assertEquals("", WordUtils.wrap("", 20, "\n", true));
+        assertEquals("", WordUtils.wrap("", 20, null, false));
+        assertEquals("", WordUtils.wrap("", 20, null, true));
+        assertEquals("", WordUtils.wrap("", -1, null, false));
+        assertEquals("", WordUtils.wrap("", -1, null, true));
+        
+        // normal
+        String input = "Here is one line of text that is going to be wrapped after 20 columns.";
+        String expected = "Here is one line of\ntext that is going\nto be wrapped after\n20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+
+        // unusual newline char
+        input = "Here is one line of text that is going to be wrapped after 20 columns.";
+        expected = "Here is one line of<br />text that is going<br />to be wrapped after<br />20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, "<br />", false));
+        assertEquals(expected, WordUtils.wrap(input, 20, "<br />", true));
+
+        // short line length
+        input = "Here is one line";
+        expected = "Here\nis one\nline";
+        assertEquals(expected, WordUtils.wrap(input, 6, "\n", false));
+        expected = "Here\nis\none\nline";
+        assertEquals(expected, WordUtils.wrap(input, 2, "\n", false));
+        assertEquals(expected, WordUtils.wrap(input, -1, "\n", false));
+
+        // system newline char
+        final String systemNewLine = SystemUtils.LINE_SEPARATOR;
+        input = "Here is one line of text that is going to be wrapped after 20 columns.";
+        expected = "Here is one line of" + systemNewLine + "text that is going" + systemNewLine 
+            + "to be wrapped after" + systemNewLine + "20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, null, false));
+        assertEquals(expected, WordUtils.wrap(input, 20, null, true));
+
+        // with extra spaces
+        input = " Here:  is  one  line  of  text  that  is  going  to  be  wrapped  after  20  columns.";
+        expected = "Here:  is  one  line\nof  text  that  is \ngoing  to  be \nwrapped  after  20 \ncolumns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+        
+        // with tab
+        input = "Here is\tone line of text that is going to be wrapped after 20 columns.";
+        expected = "Here is\tone line of\ntext that is going\nto be wrapped after\n20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+        
+        // with tab at wrapColumn
+        input = "Here is one line of\ttext that is going to be wrapped after 20 columns.";
+        expected = "Here is one line\nof\ttext that is\ngoing to be wrapped\nafter 20 columns.";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+        
+        // difference because of long word
+        input = "Click here to jump to the commons website - http://commons.apache.org";
+        expected = "Click here to jump\nto the commons\nwebsite -\nhttp://commons.apache.org";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        expected = "Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+        
+        // difference because of long word in middle
+        input = "Click here, http://commons.apache.org, to jump to the commons website";
+        expected = "Click here,\nhttp://commons.apache.org,\nto jump to the\ncommons website";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", false));
+        expected = "Click here,\nhttp://commons.apach\ne.org, to jump to\nthe commons website";
+        assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
+    }
+
+    @Test
+    public void testWrap_StringIntStringBooleanString() {
+
+        //no changes test
+        String input = "flammable/inflammable";
+        String expected = "flammable/inflammable";
+        assertEquals(expected, WordUtils.wrap(input, 30, "\n", false, "/"));
+
+        // wrap on / and small width
+        expected = "flammable\ninflammable";
+        assertEquals(expected, WordUtils.wrap(input, 2, "\n", false, "/"));
+
+        // wrap long words on / 1
+        expected = "flammable\ninflammab\nle";
+        assertEquals(expected, WordUtils.wrap(input, 9, "\n", true, "/"));
+
+        // wrap long words on / 2
+        expected = "flammable\ninflammable";
+        assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));
+
+        // wrap long words on / 3
+        input = "flammableinflammable";
+        expected = "flammableinflam\nmable";
+        assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testCapitalize_String() {
+        assertEquals(null, WordUtils.capitalize(null));
+        assertEquals("", WordUtils.capitalize(""));
+        assertEquals("  ", WordUtils.capitalize("  "));
+        
+        assertEquals("I", WordUtils.capitalize("I") );
+        assertEquals("I", WordUtils.capitalize("i") );
+        assertEquals("I Am Here 123", WordUtils.capitalize("i am here 123") );
+        assertEquals("I Am Here 123", WordUtils.capitalize("I Am Here 123") );
+        assertEquals("I Am HERE 123", WordUtils.capitalize("i am HERE 123") );
+        assertEquals("I AM HERE 123", WordUtils.capitalize("I AM HERE 123") );
+    }
+    
+    @Test
+    public void testCapitalizeWithDelimiters_String() {
+        assertEquals(null, WordUtils.capitalize(null, null));
+        assertEquals("", WordUtils.capitalize("", new char[0]));
+        assertEquals("  ", WordUtils.capitalize("  ", new char[0]));
+        
+        char[] chars = new char[] { '-', '+', ' ', '@' };
+        assertEquals("I", WordUtils.capitalize("I", chars) );
+        assertEquals("I", WordUtils.capitalize("i", chars) );
+        assertEquals("I-Am Here+123", WordUtils.capitalize("i-am here+123", chars) );
+        assertEquals("I Am+Here-123", WordUtils.capitalize("I Am+Here-123", chars) );
+        assertEquals("I+Am-HERE 123", WordUtils.capitalize("i+am-HERE 123", chars) );
+        assertEquals("I-AM HERE+123", WordUtils.capitalize("I-AM HERE+123", chars) );
+        chars = new char[] {'.'};
+        assertEquals("I aM.Fine", WordUtils.capitalize("i aM.fine", chars) );
+        assertEquals("I Am.fine", WordUtils.capitalize("i am.fine", null) );
+    }
+
+    @Test
+    public void testCapitalizeFully_String() {
+        assertEquals(null, WordUtils.capitalizeFully(null));
+        assertEquals("", WordUtils.capitalizeFully(""));
+        assertEquals("  ", WordUtils.capitalizeFully("  "));
+        
+        assertEquals("I", WordUtils.capitalizeFully("I") );
+        assertEquals("I", WordUtils.capitalizeFully("i") );
+        assertEquals("I Am Here 123", WordUtils.capitalizeFully("i am here 123") );
+        assertEquals("I Am Here 123", WordUtils.capitalizeFully("I Am Here 123") );
+        assertEquals("I Am Here 123", WordUtils.capitalizeFully("i am HERE 123") );
+        assertEquals("I Am Here 123", WordUtils.capitalizeFully("I AM HERE 123") );
+    }
+    
+    @Test
+    public void testCapitalizeFullyWithDelimiters_String() {
+        assertEquals(null, WordUtils.capitalizeFully(null, null));
+        assertEquals("", WordUtils.capitalizeFully("", new char[0]));
+        assertEquals("  ", WordUtils.capitalizeFully("  ", new char[0]));
+        
+        char[] chars = new char[] { '-', '+', ' ', '@' };
+        assertEquals("I", WordUtils.capitalizeFully("I", chars) );
+        assertEquals("I", WordUtils.capitalizeFully("i", chars) );
+        assertEquals("I-Am Here+123", WordUtils.capitalizeFully("i-am here+123", chars) );
+        assertEquals("I Am+Here-123", WordUtils.capitalizeFully("I Am+Here-123", chars) );
+        assertEquals("I+Am-Here 123", WordUtils.capitalizeFully("i+am-HERE 123", chars) );
+        assertEquals("I-Am Here+123", WordUtils.capitalizeFully("I-AM HERE+123", chars) );
+        chars = new char[] {'.'};
+        assertEquals("I am.Fine", WordUtils.capitalizeFully("i aM.fine", chars) );
+        assertEquals("I Am.fine", WordUtils.capitalizeFully("i am.fine", null) );
+    }
+
+    @Test
+    public void testContainsAllWords_StringString() {
+        assertFalse(WordUtils.containsAllWords(null, (String) null));
+        assertFalse(WordUtils.containsAllWords(null, ""));
+        assertFalse(WordUtils.containsAllWords(null, "ab"));
+
+        assertFalse(WordUtils.containsAllWords("", (String) null));
+        assertFalse(WordUtils.containsAllWords("", ""));
+        assertFalse(WordUtils.containsAllWords("", "ab"));
+
+        assertFalse(WordUtils.containsAllWords("foo", (String) null));
+        assertFalse(WordUtils.containsAllWords("bar", ""));
+        assertFalse(WordUtils.containsAllWords("zzabyycdxx", "by"));
+        assertTrue(WordUtils.containsAllWords("lorem ipsum dolor sit amet", "ipsum", "lorem", "dolor"));
+        assertFalse(WordUtils.containsAllWords("lorem ipsum dolor sit amet", "ipsum", null, "lorem", "dolor"));
+        assertFalse(WordUtils.containsAllWords("lorem ipsum null dolor sit amet", "ipsum", null, "lorem", "dolor"));
+        assertFalse(WordUtils.containsAllWords("ab", "b"));
+        assertFalse(WordUtils.containsAllWords("ab", "z"));
+    }
+
+    @Test
+    public void testUncapitalize_String() {
+        assertEquals(null, WordUtils.uncapitalize(null));
+        assertEquals("", WordUtils.uncapitalize(""));
+        assertEquals("  ", WordUtils.uncapitalize("  "));
+        
+        assertEquals("i", WordUtils.uncapitalize("I") );
+        assertEquals("i", WordUtils.uncapitalize("i") );
+        assertEquals("i am here 123", WordUtils.uncapitalize("i am here 123") );
+        assertEquals("i am here 123", WordUtils.uncapitalize("I Am Here 123") );
+        assertEquals("i am hERE 123", WordUtils.uncapitalize("i am HERE 123") );
+        assertEquals("i aM hERE 123", WordUtils.uncapitalize("I AM HERE 123") );
+    }
+    
+    @Test
+    public void testUncapitalizeWithDelimiters_String() {
+        assertEquals(null, WordUtils.uncapitalize(null, null));
+        assertEquals("", WordUtils.uncapitalize("", new char[0]));
+        assertEquals("  ", WordUtils.uncapitalize("  ", new char[0]));
+        
+        char[] chars = new char[] { '-', '+', ' ', '@' };
+        assertEquals("i", WordUtils.uncapitalize("I", chars) );
+        assertEquals("i", WordUtils.uncapitalize("i", chars) );
+        assertEquals("i am-here+123", WordUtils.uncapitalize("i am-here+123", chars) );
+        assertEquals("i+am here-123", WordUtils.uncapitalize("I+Am Here-123", chars) );
+        assertEquals("i-am+hERE 123", WordUtils.uncapitalize("i-am+HERE 123", chars) );
+        assertEquals("i aM-hERE+123", WordUtils.uncapitalize("I AM-HERE+123", chars) );
+        chars = new char[] {'.'};
+        assertEquals("i AM.fINE", WordUtils.uncapitalize("I AM.FINE", chars) );
+        assertEquals("i aM.FINE", WordUtils.uncapitalize("I AM.FINE", null) );
+    }
+
+    //-----------------------------------------------------------------------
+    @Test
+    public void testInitials_String() {
+        assertEquals(null, WordUtils.initials(null));
+        assertEquals("", WordUtils.initials(""));
+        assertEquals("", WordUtils.initials("  "));
+
+        assertEquals("I", WordUtils.initials("I"));
+        assertEquals("i", WordUtils.initials("i"));
+        assertEquals("BJL", WordUtils.initials("Ben John Lee"));
+        assertEquals("BJL", WordUtils.initials("   Ben \n   John\tLee\t"));
+        assertEquals("BJ", WordUtils.initials("Ben J.Lee"));
+        assertEquals("BJ.L", WordUtils.initials(" Ben   John  . Lee"));
+        assertEquals("iah1", WordUtils.initials("i am here 123"));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testInitials_String_charArray() {
+        char[] array = null;
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals("", WordUtils.initials("  ", array));
+        assertEquals("I", WordUtils.initials("I", array));
+        assertEquals("i", WordUtils.initials("i", array));
+        assertEquals("S", WordUtils.initials("SJC", array));
+        assertEquals("BJL", WordUtils.initials("Ben John Lee", array));
+        assertEquals("BJL", WordUtils.initials("   Ben \n   John\tLee\t", array));
+        assertEquals("BJ", WordUtils.initials("Ben J.Lee", array));
+        assertEquals("BJ.L", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("KO", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("iah1", WordUtils.initials("i am here 123", array));
+        
+        array = new char[0];
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals("", WordUtils.initials("  ", array));
+        assertEquals("", WordUtils.initials("I", array));
+        assertEquals("", WordUtils.initials("i", array));
+        assertEquals("", WordUtils.initials("SJC", array));
+        assertEquals("", WordUtils.initials("Ben John Lee", array));
+        assertEquals("", WordUtils.initials("   Ben \n   John\tLee\t", array));
+        assertEquals("", WordUtils.initials("Ben J.Lee", array));
+        assertEquals("", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("", WordUtils.initials("i am here 123", array));
+        
+        array = " ".toCharArray();
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals("", WordUtils.initials("  ", array));
+        assertEquals("I", WordUtils.initials("I", array));
+        assertEquals("i", WordUtils.initials("i", array));
+        assertEquals("S", WordUtils.initials("SJC", array));
+        assertEquals("BJL", WordUtils.initials("Ben John Lee", array));
+        assertEquals("BJ", WordUtils.initials("Ben J.Lee", array));
+        assertEquals("B\nJ", WordUtils.initials("   Ben \n   John\tLee\t", array));
+        assertEquals("BJ.L", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("KO", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("iah1", WordUtils.initials("i am here 123", array));
+        
+        array = " .".toCharArray();
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals("", WordUtils.initials("  ", array));
+        assertEquals("I", WordUtils.initials("I", array));
+        assertEquals("i", WordUtils.initials("i", array));
+        assertEquals("S", WordUtils.initials("SJC", array));
+        assertEquals("BJL", WordUtils.initials("Ben John Lee", array));
+        assertEquals("BJL", WordUtils.initials("Ben J.Lee", array));
+        assertEquals("BJL", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("KO", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("iah1", WordUtils.initials("i am here 123", array));
+        
+        array = " .'".toCharArray();
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals("", WordUtils.initials("  ", array));
+        assertEquals("I", WordUtils.initials("I", array));
+        assertEquals("i", WordUtils.initials("i", array));
+        assertEquals("S", WordUtils.initials("SJC", array));
+        assertEquals("BJL", WordUtils.initials("Ben John Lee", array));
+        assertEquals("BJL", WordUtils.initials("Ben J.Lee", array));
+        assertEquals("BJL", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("KOM", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("iah1", WordUtils.initials("i am here 123", array));
+        
+        array = "SIJo1".toCharArray();
+        assertEquals(null, WordUtils.initials(null, array));
+        assertEquals("", WordUtils.initials("", array));
+        assertEquals(" ", WordUtils.initials("  ", array));
+        assertEquals("", WordUtils.initials("I", array));
+        assertEquals("i", WordUtils.initials("i", array));
+        assertEquals("C", WordUtils.initials("SJC", array));
+        assertEquals("Bh", WordUtils.initials("Ben John Lee", array));
+        assertEquals("B.", WordUtils.initials("Ben J.Lee", array));
+        assertEquals(" h", WordUtils.initials(" Ben   John  . Lee", array));
+        assertEquals("K", WordUtils.initials("Kay O'Murphy", array));
+        assertEquals("i2", WordUtils.initials("i am here 123", array));
+    }
+
+    // -----------------------------------------------------------------------
+    @Test
+    public void testSwapCase_String() {
+        assertEquals(null, WordUtils.swapCase(null));
+        assertEquals("", WordUtils.swapCase(""));
+        assertEquals("  ", WordUtils.swapCase("  "));
+        
+        assertEquals("i", WordUtils.swapCase("I") );
+        assertEquals("I", WordUtils.swapCase("i") );
+        assertEquals("I AM HERE 123", WordUtils.swapCase("i am here 123") );
+        assertEquals("i aM hERE 123", WordUtils.swapCase("I Am Here 123") );
+        assertEquals("I AM here 123", WordUtils.swapCase("i am HERE 123") );
+        assertEquals("i am here 123", WordUtils.swapCase("I AM HERE 123") );
+
+        final String test = "This String contains a TitleCase character: \u01C8";
+        final String expect = "tHIS sTRING CONTAINS A tITLEcASE CHARACTER: \u01C9";
+        assertEquals(expect, WordUtils.swapCase(test));
+    }
+
+}