    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      http://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.text;
    018
    019import java.util.HashSet;
    020import java.util.Set;
    021import java.util.regex.Matcher;
    022import java.util.regex.Pattern;
    023
    024import org.apache.commons.lang3.ArrayUtils;
    025import org.apache.commons.lang3.StringUtils;
    026import org.apache.commons.lang3.Validate;
    027
    028/**
    029 * Operations on Strings that contain words.
    030 *
    031 * <p>
    032 * This class tries to handle {@code null} input gracefully. An exception will not be thrown for a
    033 * {@code null} input. Each method documents its behavior in more detail.
    034 * </p>
    035 *
    036 * @since 1.1
    037 */
    038public class WordUtils {
    039
    040    /**
    041     * Abbreviates the words nicely.
    042     *
    043     * <p>
    044     * This method searches for the first space after the lower limit and abbreviates
    045     * the String there. It will also append any String passed as a parameter
    046     * to the end of the String. The upper limit can be specified to forcibly
    047     * abbreviate a String.
    048     * </p>
    049     *
    050     * @param str         the string to be abbreviated. If null is passed, null is returned.
    051     *                    If the empty String is passed, the empty string is returned.
    052     * @param lower       the lower limit; negative value is treated as zero.
    053     * @param upper       the upper limit; specify -1 if no limit is desired.
    054     *                    The upper limit cannot be lower than the lower limit.
    055     * @param appendToEnd String to be appended to the end of the abbreviated string.
    056     *                    This is appended ONLY if the string was indeed abbreviated.
    057     *                    The append does not count towards the lower or upper limits.
    058     * @return The abbreviated String.
    059     *
    060     * <pre>
    061     * WordUtils.abbreviate("Now is the time for all good men", 0, 40, null));     = "Now"
    062     * WordUtils.abbreviate("Now is the time for all good men", 10, 40, null));    = "Now is the"
    063     * WordUtils.abbreviate("Now is the time for all good men", 20, 40, null));    = "Now is the time for all"
    064     * WordUtils.abbreviate("Now is the time for all good men", 0, 40, ""));       = "Now"
    065     * WordUtils.abbreviate("Now is the time for all good men", 10, 40, ""));      = "Now is the"
    066     * WordUtils.abbreviate("Now is the time for all good men", 20, 40, ""));      = "Now is the time for all"
    067     * WordUtils.abbreviate("Now is the time for all good men", 0, 40, " ..."));   = "Now ..."
    068     * WordUtils.abbreviate("Now is the time for all good men", 10, 40, " ..."));  = "Now is the ..."
    069     * WordUtils.abbreviate("Now is the time for all good men", 20, 40, " ..."));  = "Now is the time for all ..."
    070     * WordUtils.abbreviate("Now is the time for all good men", 0, -1, ""));       = "Now"
    071     * WordUtils.abbreviate("Now is the time for all good men", 10, -1, ""));      = "Now is the"
    072     * WordUtils.abbreviate("Now is the time for all good men", 20, -1, ""));      = "Now is the time for all"
    073     * WordUtils.abbreviate("Now is the time for all good men", 50, -1, ""));      = "Now is the time for all good men"
    074     * WordUtils.abbreviate("Now is the time for all good men", 1000, -1, ""));    = "Now is the time for all good men"
    075     * WordUtils.abbreviate("Now is the time for all good men", 9, -10, null));    = IllegalArgumentException
    076     * WordUtils.abbreviate("Now is the time for all good men", 10, 5, null));     = IllegalArgumentException
    077     * </pre>
    078     */
    079    public static String abbreviate(final String str, int lower, int upper, final String appendToEnd) {
    080        Validate.isTrue(upper >= -1, "upper value cannot be less than -1");
    081        Validate.isTrue(upper >= lower || upper == -1, "upper value is less than lower value");
    082        if (StringUtils.isEmpty(str)) {
    083            return str;
    084        }
    085
    086        // if the lower value is greater than the length of the string,
    087        // set to the length of the string
    088        if (lower > str.length()) {
    089            lower = str.length();
    090        }
    091
    092        // if the upper value is -1 (i.e. no limit) or is greater
    093        // than the length of the string, set to the length of the string
    094        if (upper == -1 || upper > str.length()) {
    095            upper = str.length();
    096        }
    097
    098        final StringBuilder result = new StringBuilder();
    099        final int index = StringUtils.indexOf(str, " ", lower);
    100        if (index == -1) {
    101            result.append(str, 0, upper);
    102            // only if abbreviation has occurred do we append the appendToEnd value
    103            if (upper != str.length()) {
    104                result.append(StringUtils.defaultString(appendToEnd));
    105            }
    106        } else {
    107            result.append(str, 0, Math.min(index, upper));
    108            result.append(StringUtils.defaultString(appendToEnd));
    109        }
    110
    111        return result.toString();
    112    }
    113
    114    /**
    115     * Capitalizes all the whitespace separated words in a String.
    116     * Only the first character of each word is changed. To convert the
    117     * rest of each word to lowercase at the same time,
    118     * use {@link #capitalizeFully(String)}.
    119     *
    120     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    121     * A {@code null} input String returns {@code null}.
    122     * Capitalization uses the Unicode title case, normally equivalent to
    123     * upper case.</p>
    124     *
    125     * <pre>
    126     * WordUtils.capitalize(null)        = null
    127     * WordUtils.capitalize("")          = ""
    128     * WordUtils.capitalize("i am FINE") = "I Am FINE"
    129     * </pre>
    130     *
    131     * @param str  the String to capitalize, may be null
    132     * @return capitalized String, {@code null} if null String input
    133     * @see #uncapitalize(String)
    134     * @see #capitalizeFully(String)
    135     */
    136    public static String capitalize(final String str) {
    137        return capitalize(str, null);
    138    }
    139
    140    /**
    141     * Capitalizes all the delimiter separated words in a String.
    142     * Only the first character of each word is changed. To convert the
    143     * rest of each word to lowercase at the same time,
    144     * use {@link #capitalizeFully(String, char[])}.
    145     *
    146     * <p>The delimiters represent a set of characters understood to separate words.
    147     * The first string character and the first non-delimiter character after a
    148     * delimiter will be capitalized.</p>
    149     *
    150     * <p>A {@code null} input String returns {@code null}.
    151     * Capitalization uses the Unicode title case, normally equivalent to
    152     * upper case.</p>
    153     *
    154     * <pre>
    155     * WordUtils.capitalize(null, *)            = null
    156     * WordUtils.capitalize("", *)              = ""
    157     * WordUtils.capitalize(*, new char[0])     = *
    158     * WordUtils.capitalize("i am fine", null)  = "I Am Fine"
    159     * WordUtils.capitalize("i aM.fine", {'.'}) = "I aM.Fine"
    160     * WordUtils.capitalize("i am fine", new char[]{}) = "I am fine"
    161     * </pre>
    162     *
    163     * @param str  the String to capitalize, may be null
    164     * @param delimiters  set of characters to determine capitalization, null means whitespace
    165     * @return capitalized String, {@code null} if null String input
    166     * @see #uncapitalize(String)
    167     * @see #capitalizeFully(String)
    168     */
    169    public static String capitalize(final String str, final char... delimiters) {
    170        if (StringUtils.isEmpty(str)) {
    171            return str;
    172        }
    173        final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
    174        final int strLen = str.length();
    175        final int[] newCodePoints = new int[strLen];
    176        int outOffset = 0;
    177
    178        boolean capitalizeNext = true;
    179        for (int index = 0; index < strLen;) {
    180            final int codePoint = str.codePointAt(index);
    181
    182            if (delimiterSet.contains(codePoint)) {
    183                capitalizeNext = true;
    184                newCodePoints[outOffset++] = codePoint;
    185                index += Character.charCount(codePoint);
    186            } else if (capitalizeNext) {
    187                final int titleCaseCodePoint = Character.toTitleCase(codePoint);
    188                newCodePoints[outOffset++] = titleCaseCodePoint;
    189                index += Character.charCount(titleCaseCodePoint);
    190                capitalizeNext = false;
    191            } else {
    192                newCodePoints[outOffset++] = codePoint;
    193                index += Character.charCount(codePoint);
    194            }
    195        }
    196        return new String(newCodePoints, 0, outOffset);
    197    }
    198
    199    /**
    200     * Converts all the whitespace separated words in a String into capitalized words,
    201     * that is each word is made up of a titlecase character and then a series of
    202     * lowercase characters.
    203     *
    204     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    205     * A {@code null} input String returns {@code null}.
    206     * Capitalization uses the Unicode title case, normally equivalent to
    207     * upper case.</p>
    208     *
    209     * <pre>
    210     * WordUtils.capitalizeFully(null)        = null
    211     * WordUtils.capitalizeFully("")          = ""
    212     * WordUtils.capitalizeFully("i am FINE") = "I Am Fine"
    213     * </pre>
    214     *
    215     * @param str  the String to capitalize, may be null
    216     * @return capitalized String, {@code null} if null String input
    217     */
    218    public static String capitalizeFully(final String str) {
    219        return capitalizeFully(str, null);
    220    }
    221
    222    /**
    223     * Converts all the delimiter separated words in a String into capitalized words,
    224     * that is each word is made up of a titlecase character and then a series of
    225     * lowercase characters.
    226     *
    227     * <p>The delimiters represent a set of characters understood to separate words.
    228     * The first string character and the first non-delimiter character after a
    229     * delimiter will be capitalized.</p>
    230     *
    231     * <p>A {@code null} input String returns {@code null}.
    232     * Capitalization uses the Unicode title case, normally equivalent to
    233     * upper case.</p>
    234     *
    235     * <pre>
    236     * WordUtils.capitalizeFully(null, *)            = null
    237     * WordUtils.capitalizeFully("", *)              = ""
    238     * WordUtils.capitalizeFully(*, null)            = *
    239     * WordUtils.capitalizeFully(*, new char[0])     = *
    240     * WordUtils.capitalizeFully("i aM.fine", {'.'}) = "I am.Fine"
    241     * </pre>
    242     *
    243     * @param str  the String to capitalize, may be null
    244     * @param delimiters  set of characters to determine capitalization, null means whitespace
    245     * @return capitalized String, {@code null} if null String input
    246     */
    247    public static String capitalizeFully(String str, final char... delimiters) {
    248        if (StringUtils.isEmpty(str)) {
    249            return str;
    250        }
    251        str = str.toLowerCase();
    252        return capitalize(str, delimiters);
    253    }
    254
    255    /**
    256     * Checks if the String contains all words in the given array.
    257     *
    258     * <p>
    259     * A {@code null} String will return {@code false}. A {@code null}, zero
    260     * length search array or if one element of array is null will return {@code false}.
    261     * </p>
    262     *
    263     * <pre>
    264     * WordUtils.containsAllWords(null, *)            = false
    265     * WordUtils.containsAllWords("", *)              = false
    266     * WordUtils.containsAllWords(*, null)            = false
    267     * WordUtils.containsAllWords(*, [])              = false
    268     * WordUtils.containsAllWords("abcd", "ab", "cd") = false
    269     * WordUtils.containsAllWords("abc def", "def", "abc") = true
    270     * </pre>
    271     *
    272     * @param word The CharSequence to check, may be null
    273     * @param words The array of String words to search for, may be null
    274     * @return {@code true} if all search words are found, {@code false} otherwise
    275     */
    276    public static boolean containsAllWords(final CharSequence word, final CharSequence... words) {
    277        if (StringUtils.isEmpty(word) || ArrayUtils.isEmpty(words)) {
    278            return false;
    279        }
    280        for (final CharSequence w : words) {
    281            if (StringUtils.isBlank(w)) {
    282                return false;
    283            }
    284            final Pattern p = Pattern.compile(".*\\b" + w + "\\b.*");
    285            if (!p.matcher(word).matches()) {
    286                return false;
    287            }
    288        }
    289        return true;
    290    }
    291
    292    /**
    293     * Converts an array of delimiters to a hash set of code points. Code point of space(32) is added as the default
    294     * value if delimiters is null. The generated hash set provides O(1) lookup time.
    295     *
    296     * @param delimiters set of characters to determine capitalization, null means whitespace
    297     * @return Set<Integer>
    298     */
    299    private static Set<Integer> generateDelimiterSet(final char[] delimiters) {
    300        final Set<Integer> delimiterHashSet = new HashSet<>();
    301        if (delimiters == null || delimiters.length == 0) {
    302            if (delimiters == null) {
    303                delimiterHashSet.add(Character.codePointAt(new char[] {' '}, 0));
    304            }
    305
    306            return delimiterHashSet;
    307        }
    308
    309        for (int index = 0; index < delimiters.length; index++) {
    310            delimiterHashSet.add(Character.codePointAt(delimiters, index));
    311        }
    312        return delimiterHashSet;
    313    }
    314
    315    /**
    316     * Extracts the initial characters from each word in the String.
    317     *
    318     * <p>All first characters after whitespace are returned as a new string.
    319     * Their case is not changed.</p>
    320     *
    321     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    322     * A {@code null} input String returns {@code null}.</p>
    323     *
    324     * <pre>
    325     * WordUtils.initials(null)             = null
    326     * WordUtils.initials("")               = ""
    327     * WordUtils.initials("Ben John Lee")   = "BJL"
    328     * WordUtils.initials("Ben J.Lee")      = "BJ"
    329     * </pre>
    330     *
    331     * @param str  the String to get initials from, may be null
    332     * @return String of initial letters, {@code null} if null String input
    333     * @see #initials(String,char[])
    334     */
    335    public static String initials(final String str) {
    336        return initials(str, null);
    337    }
    338
    339    /**
    340     * Extracts the initial characters from each word in the String.
    341     *
    342     * <p>All first characters after the defined delimiters are returned as a new string.
    343     * Their case is not changed.</p>
    344     *
    345     * <p>If the delimiters array is null, then Whitespace is used.
    346     * Whitespace is defined by {@link Character#isWhitespace(char)}.
    347     * A {@code null} input String returns {@code null}.
    348     * An empty delimiter array returns an empty String.</p>
    349     *
    350     * <pre>
    351     * WordUtils.initials(null, *)                = null
    352     * WordUtils.initials("", *)                  = ""
    353     * WordUtils.initials("Ben John Lee", null)   = "BJL"
    354     * WordUtils.initials("Ben J.Lee", null)      = "BJ"
    355     * WordUtils.initials("Ben J.Lee", [' ','.']) = "BJL"
    356     * WordUtils.initials(*, new char[0])         = ""
    357     * </pre>
    358     *
    359     * @param str  the String to get initials from, may be null
    360     * @param delimiters  set of characters to determine words, null means whitespace
    361     * @return String of initial characters, {@code null} if null String input
    362     * @see #initials(String)
    363     */
    364    public static String initials(final String str, final char... delimiters) {
    365        if (StringUtils.isEmpty(str)) {
    366            return str;
    367        }
    368        if (delimiters != null && delimiters.length == 0) {
    369            return StringUtils.EMPTY;
    370        }
    371        final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
    372        final int strLen = str.length();
    373        final int[] newCodePoints = new int[strLen / 2 + 1];
    374        int count = 0;
    375        boolean lastWasGap = true;
    376        for (int i = 0; i < strLen;) {
    377            final int codePoint = str.codePointAt(i);
    378
    379            if (delimiterSet.contains(codePoint) || delimiters == null && Character.isWhitespace(codePoint)) {
    380                lastWasGap = true;
    381            } else if (lastWasGap) {
    382                newCodePoints[count++] = codePoint;
    383                lastWasGap = false;
    384            }
    385
    386            i += Character.charCount(codePoint);
    387        }
    388        return new String(newCodePoints, 0, count);
    389    }
    390
    391    /**
    392     * Is the character a delimiter.
    393     *
    394     * @param ch the character to check
    395     * @param delimiters the delimiters
    396     * @return true if it is a delimiter
    397     * @deprecated as of 1.2 and will be removed in 2.0
    398     */
    399    @Deprecated
    400    public static boolean isDelimiter(final char ch, final char[] delimiters) {
    401        if (delimiters == null) {
    402            return Character.isWhitespace(ch);
    403        }
    404        for (final char delimiter : delimiters) {
    405            if (ch == delimiter) {
    406                return true;
    407            }
    408        }
    409        return false;
    410    }
    411
    412    /**
    413     * Is the codePoint a delimiter.
    414     *
    415     * @param codePoint the codePint to check
    416     * @param delimiters the delimiters
    417     * @return true if it is a delimiter
    418     * @deprecated as of 1.2 and will be removed in 2.0
    419     */
    420    @Deprecated
    421    public static boolean isDelimiter(final int codePoint, final char[] delimiters) {
    422        if (delimiters == null) {
    423            return Character.isWhitespace(codePoint);
    424        }
    425        for (int index = 0; index < delimiters.length; index++) {
    426            final int delimiterCodePoint = Character.codePointAt(delimiters, index);
    427            if (delimiterCodePoint == codePoint) {
    428                return true;
    429            }
    430        }
    431        return false;
    432    }
    433
    434    /**
    435     * Swaps the case of a String using a word based algorithm.
    436     *
    437     * <ul>
    438     *  <li>Upper case character converts to Lower case</li>
    439     *  <li>Title case character converts to Lower case</li>
    440     *  <li>Lower case character after Whitespace or at start converts to Title case</li>
    441     *  <li>Other Lower case character converts to Upper case</li>
    442     * </ul>
    443     *
    444     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    445     * A {@code null} input String returns {@code null}.</p>
    446     *
    447     * <pre>
    448     * StringUtils.swapCase(null)                 = null
    449     * StringUtils.swapCase("")                   = ""
    450     * StringUtils.swapCase("The dog has a BONE") = "tHE DOG HAS A bone"
    451     * </pre>
    452     *
    453     * @param str  the String to swap case, may be null
    454     * @return The changed String, {@code null} if null String input
    455     */
    456    public static String swapCase(final String str) {
    457        if (StringUtils.isEmpty(str)) {
    458            return str;
    459        }
    460        final int strLen = str.length();
    461        final int[] newCodePoints = new int[strLen];
    462        int outOffset = 0;
    463        boolean whitespace = true;
    464        for (int index = 0; index < strLen;) {
    465            final int oldCodepoint = str.codePointAt(index);
    466            final int newCodePoint;
    467            if (Character.isUpperCase(oldCodepoint) || Character.isTitleCase(oldCodepoint)) {
    468                newCodePoint = Character.toLowerCase(oldCodepoint);
    469                whitespace = false;
    470            } else if (Character.isLowerCase(oldCodepoint)) {
    471                if (whitespace) {
    472                    newCodePoint = Character.toTitleCase(oldCodepoint);
    473                    whitespace = false;
    474                } else {
    475                    newCodePoint = Character.toUpperCase(oldCodepoint);
    476                }
    477            } else {
    478                whitespace = Character.isWhitespace(oldCodepoint);
    479                newCodePoint = oldCodepoint;
    480            }
    481            newCodePoints[outOffset++] = newCodePoint;
    482            index += Character.charCount(newCodePoint);
    483        }
    484        return new String(newCodePoints, 0, outOffset);
    485    }
    486
    487    /**
    488     * Uncapitalizes all the whitespace separated words in a String.
    489     * Only the first character of each word is changed.
    490     *
    491     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    492     * A {@code null} input String returns {@code null}.</p>
    493     *
    494     * <pre>
    495     * WordUtils.uncapitalize(null)        = null
    496     * WordUtils.uncapitalize("")          = ""
    497     * WordUtils.uncapitalize("I Am FINE") = "i am fINE"
    498     * </pre>
    499     *
    500     * @param str  the String to uncapitalize, may be null
    501     * @return uncapitalized String, {@code null} if null String input
    502     * @see #capitalize(String)
    503     */
    504    public static String uncapitalize(final String str) {
    505        return uncapitalize(str, null);
    506    }
    507
    508    /**
    509     * Uncapitalizes all the whitespace separated words in a String.
    510     * Only the first character of each word is changed.
    511     *
    512     * <p>The delimiters represent a set of characters understood to separate words.
    513     * The first string character and the first non-delimiter character after a
    514     * delimiter will be uncapitalized.</p>
    515     *
    516     * <p>Whitespace is defined by {@link Character#isWhitespace(char)}.
    517     * A {@code null} input String returns {@code null}.</p>
    518     *
    519     * <pre>
    520     * WordUtils.uncapitalize(null, *)            = null
    521     * WordUtils.uncapitalize("", *)              = ""
    522     * WordUtils.uncapitalize(*, null)            = *
    523     * WordUtils.uncapitalize(*, new char[0])     = *
    524     * WordUtils.uncapitalize("I AM.FINE", {'.'}) = "i AM.fINE"
    525     * WordUtils.uncapitalize("I am fine", new char[]{}) = "i am fine"
    526     * </pre>
    527     *
    528     * @param str  the String to uncapitalize, may be null
    529     * @param delimiters  set of characters to determine uncapitalization, null means whitespace
    530     * @return uncapitalized String, {@code null} if null String input
    531     * @see #capitalize(String)
    532     */
    533    public static String uncapitalize(final String str, final char... delimiters) {
    534        if (StringUtils.isEmpty(str)) {
    535            return str;
    536        }
    537        final Set<Integer> delimiterSet = generateDelimiterSet(delimiters);
    538        final int strLen = str.length();
    539        final int[] newCodePoints = new int[strLen];
    540        int outOffset = 0;
    541
    542        boolean uncapitalizeNext = true;
    543        for (int index = 0; index < strLen;) {
    544            final int codePoint = str.codePointAt(index);
    545
    546            if (delimiterSet.contains(codePoint)) {
    547                uncapitalizeNext = true;
    548                newCodePoints[outOffset++] = codePoint;
    549                index += Character.charCount(codePoint);
    550            } else if (uncapitalizeNext) {
    551                final int titleCaseCodePoint = Character.toLowerCase(codePoint);
    552                newCodePoints[outOffset++] = titleCaseCodePoint;
    553                index += Character.charCount(titleCaseCodePoint);
    554                uncapitalizeNext = false;
    555            } else {
    556                newCodePoints[outOffset++] = codePoint;
    557                index += Character.charCount(codePoint);
    558            }
    559        }
    560        return new String(newCodePoints, 0, outOffset);
    561    }
    562
    563    /**
    564     * Wraps a single line of text, identifying words by {@code ' '}.
    565     *
    566     * <p>New lines will be separated by the system property line separator.
    567     * Very long words, such as URLs will <i>not</i> be wrapped.</p>
    568     *
    569     * <p>Leading spaces on a new line are stripped.
    570     * Trailing spaces are not stripped.</p>
    571     *
    572     * <table border="1">
    573     *  <caption>Examples</caption>
    574     *  <tr>
    575     *   <th>input</th>
    576     *   <th>wrapLength</th>
    577     *   <th>result</th>
    578     *  </tr>
    579     *  <tr>
    580     *   <td>null</td>
    581     *   <td>*</td>
    582     *   <td>null</td>
    583     *  </tr>
    584     *  <tr>
    585     *   <td>""</td>
    586     *   <td>*</td>
    587     *   <td>""</td>
    588     *  </tr>
    589     *  <tr>
    590     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    591     *   <td>20</td>
    592     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
    593     *  </tr>
    594     *  <tr>
    595     *   <td>"Click here to jump to the commons website - https://commons.apache.org"</td>
    596     *   <td>20</td>
    597     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttps://commons.apache.org"</td>
    598     *  </tr>
    599     *  <tr>
    600     *   <td>"Click here, https://commons.apache.org, to jump to the commons website"</td>
    601     *   <td>20</td>
    602     *   <td>"Click here,\nhttps://commons.apache.org,\nto jump to the\ncommons website"</td>
    603     *  </tr>
    604     * </table>
    605     *
    606     * (assuming that '\n' is the systems line separator)
    607     *
    608     * @param str  the String to be word wrapped, may be null
    609     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
    610     * @return a line with newlines inserted, {@code null} if null input
    611     */
    612    public static String wrap(final String str, final int wrapLength) {
    613        return wrap(str, wrapLength, null, false);
    614    }
    615
    616    /**
    617     * Wraps a single line of text, identifying words by {@code ' '}.
    618     *
    619     * <p>Leading spaces on a new line are stripped.
    620     * Trailing spaces are not stripped.</p>
    621     *
    622     * <table border="1">
    623     *  <caption>Examples</caption>
    624     *  <tr>
    625     *   <th>input</th>
    626     *   <th>wrapLength</th>
    627     *   <th>newLineString</th>
    628     *   <th>wrapLongWords</th>
    629     *   <th>result</th>
    630     *  </tr>
    631     *  <tr>
    632     *   <td>null</td>
    633     *   <td>*</td>
    634     *   <td>*</td>
    635     *   <td>true/false</td>
    636     *   <td>null</td>
    637     *  </tr>
    638     *  <tr>
    639     *   <td>""</td>
    640     *   <td>*</td>
    641     *   <td>*</td>
    642     *   <td>true/false</td>
    643     *   <td>""</td>
    644     *  </tr>
    645     *  <tr>
    646     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    647     *   <td>20</td>
    648     *   <td>"\n"</td>
    649     *   <td>true/false</td>
    650     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
    651     *  </tr>
    652     *  <tr>
    653     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    654     *   <td>20</td>
    655     *   <td>"&lt;br /&gt;"</td>
    656     *   <td>true/false</td>
    657     *   <td>"Here is one line of&lt;br /&gt;text that is going&lt;
    658     *   br /&gt;to be wrapped after&lt;br /&gt;20 columns."</td>
    659     *  </tr>
    660     *  <tr>
    661     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    662     *   <td>20</td>
    663     *   <td>null</td>
    664     *   <td>true/false</td>
    665     *   <td>"Here is one line of" + systemNewLine + "text that is going"
    666     *   + systemNewLine + "to be wrapped after" + systemNewLine + "20 columns."</td>
    667     *  </tr>
    668     *  <tr>
    669     *   <td>"Click here to jump to the commons website - https://commons.apache.org"</td>
    670     *   <td>20</td>
    671     *   <td>"\n"</td>
    672     *   <td>false</td>
    673     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttps://commons.apache.org"</td>
    674     *  </tr>
    675     *  <tr>
    676     *   <td>"Click here to jump to the commons website - https://commons.apache.org"</td>
    677     *   <td>20</td>
    678     *   <td>"\n"</td>
    679     *   <td>true</td>
    680     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org"</td>
    681     *  </tr>
    682     * </table>
    683     *
    684     * @param str  the String to be word wrapped, may be null
    685     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
    686     * @param newLineStr  the string to insert for a new line,
    687     *  {@code null} uses the system property line separator
    688     * @param wrapLongWords  true if long words (such as URLs) should be wrapped
    689     * @return a line with newlines inserted, {@code null} if null input
    690     */
    691    public static String wrap(final String str,
    692                              final int wrapLength,
    693                              final String newLineStr,
    694                              final boolean wrapLongWords) {
    695        return wrap(str, wrapLength, newLineStr, wrapLongWords, " ");
    696    }
    697
    698    /**
    699     * Wraps a single line of text, identifying words by {@code wrapOn}.
    700     *
    701     * <p>Leading spaces on a new line are stripped.
    702     * Trailing spaces are not stripped.</p>
    703     *
    704     * <table border="1">
    705     *  <caption>Examples</caption>
    706     *  <tr>
    707     *   <th>input</th>
    708     *   <th>wrapLength</th>
    709     *   <th>newLineString</th>
    710     *   <th>wrapLongWords</th>
    711     *   <th>wrapOn</th>
    712     *   <th>result</th>
    713     *  </tr>
    714     *  <tr>
    715     *   <td>null</td>
    716     *   <td>*</td>
    717     *   <td>*</td>
    718     *   <td>true/false</td>
    719     *   <td>*</td>
    720     *   <td>null</td>
    721     *  </tr>
    722     *  <tr>
    723     *   <td>""</td>
    724     *   <td>*</td>
    725     *   <td>*</td>
    726     *   <td>true/false</td>
    727     *   <td>*</td>
    728     *   <td>""</td>
    729     *  </tr>
    730     *  <tr>
    731     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    732     *   <td>20</td>
    733     *   <td>"\n"</td>
    734     *   <td>true/false</td>
    735     *   <td>" "</td>
    736     *   <td>"Here is one line of\ntext that is going\nto be wrapped after\n20 columns."</td>
    737     *  </tr>
    738     *  <tr>
    739     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    740     *   <td>20</td>
    741     *   <td>"&lt;br /&gt;"</td>
    742     *   <td>true/false</td>
    743     *   <td>" "</td>
    744     *   <td>"Here is one line of&lt;br /&gt;text that is going&lt;br /&gt;
    745     *   to be wrapped after&lt;br /&gt;20 columns."</td>
    746     *  </tr>
    747     *  <tr>
    748     *   <td>"Here is one line of text that is going to be wrapped after 20 columns."</td>
    749     *   <td>20</td>
    750     *   <td>null</td>
    751     *   <td>true/false</td>
    752     *   <td>" "</td>
    753     *   <td>"Here is one line of" + systemNewLine + "text that is going"
    754     *   + systemNewLine + "to be wrapped after" + systemNewLine + "20 columns."</td>
    755     *  </tr>
    756     *  <tr>
    757     *   <td>"Click here to jump to the commons website - https://commons.apache.org"</td>
    758     *   <td>20</td>
    759     *   <td>"\n"</td>
    760     *   <td>false</td>
    761     *   <td>" "</td>
    762     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttps://commons.apache.org"</td>
    763     *  </tr>
    764     *  <tr>
    765     *   <td>"Click here to jump to the commons website - https://commons.apache.org"</td>
    766     *   <td>20</td>
    767     *   <td>"\n"</td>
    768     *   <td>true</td>
    769     *   <td>" "</td>
    770     *   <td>"Click here to jump\nto the commons\nwebsite -\nhttp://commons.apach\ne.org"</td>
    771     *  </tr>
    772     *  <tr>
    773     *   <td>"flammable/inflammable"</td>
    774     *   <td>20</td>
    775     *   <td>"\n"</td>
    776     *   <td>true</td>
    777     *   <td>"/"</td>
    778     *   <td>"flammable\ninflammable"</td>
    779     *  </tr>
    780     * </table>
    781     * @param str  the String to be word wrapped, may be null
    782     * @param wrapLength  the column to wrap the words at, less than 1 is treated as 1
    783     * @param newLineStr  the string to insert for a new line,
    784     *  {@code null} uses the system property line separator
    785     * @param wrapLongWords  true if long words (such as URLs) should be wrapped
    786     * @param wrapOn regex expression to be used as a breakable characters,
    787     *               if blank string is provided a space character will be used
    788     * @return a line with newlines inserted, {@code null} if null input
    789     */
    790    public static String wrap(final String str,
    791                              int wrapLength,
    792                              String newLineStr,
    793                              final boolean wrapLongWords,
    794                              String wrapOn) {
    795        if (str == null) {
    796            return null;
    797        }
    798        if (newLineStr == null) {
    799            newLineStr = System.lineSeparator();
    800        }
    801        if (wrapLength < 1) {
    802            wrapLength = 1;
    803        }
    804        if (StringUtils.isBlank(wrapOn)) {
    805            wrapOn = " ";
    806        }
    807        final Pattern patternToWrapOn = Pattern.compile(wrapOn);
    808        final int inputLineLength = str.length();
    809        int offset = 0;
    810        final StringBuilder wrappedLine = new StringBuilder(inputLineLength + 32);
    811        int matcherSize = -1;
    812
    813        while (offset < inputLineLength) {
    814            int spaceToWrapAt = -1;
    815            Matcher matcher = patternToWrapOn.matcher(str.substring(offset,
    816                    Math.min((int) Math.min(Integer.MAX_VALUE, offset + wrapLength + 1L), inputLineLength)));
    817            if (matcher.find()) {
    818                if (matcher.start() == 0) {
    819                    matcherSize = matcher.end();
    820                    if (matcherSize != 0) {
    821                        offset += matcher.end();
    822                        continue;
    823                    }
    824                    offset += 1;
    825                }
    826                spaceToWrapAt = matcher.start() + offset;
    827            }
    828
    829            // only last line without leading spaces is left
    830            if (inputLineLength - offset <= wrapLength) {
    831                break;
    832            }
    833
    834            while (matcher.find()) {
    835                spaceToWrapAt = matcher.start() + offset;
    836            }
    837
    838            if (spaceToWrapAt >= offset) {
    839                // normal case
    840                wrappedLine.append(str, offset, spaceToWrapAt);
    841                wrappedLine.append(newLineStr);
    842                offset = spaceToWrapAt + 1;
    843
    844            } else // really long word or URL
    845            if (wrapLongWords) {
    846                if (matcherSize == 0) {
    847                    offset--;
    848                }
    849                // wrap really long word one line at a time
    850                wrappedLine.append(str, offset, wrapLength + offset);
    851                wrappedLine.append(newLineStr);
    852                offset += wrapLength;
    853                matcherSize = -1;
    854            } else {
    855                // do not wrap really long word, just extend beyond limit
    856                matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength));
    857                if (matcher.find()) {
    858                    matcherSize = matcher.end() - matcher.start();
    859                    spaceToWrapAt = matcher.start() + offset + wrapLength;
    860                }
    861
    862                if (spaceToWrapAt >= 0) {
    863                    if (matcherSize == 0 && offset != 0) {
    864                        offset--;
    865                    }
    866                    wrappedLine.append(str, offset, spaceToWrapAt);
    867                    wrappedLine.append(newLineStr);
    868                    offset = spaceToWrapAt + 1;
    869                } else {
    870                    if (matcherSize == 0 && offset != 0) {
    871                        offset--;
    872                    }
    873                    wrappedLine.append(str, offset, str.length());
    874                    offset = inputLineLength;
    875                    matcherSize = -1;
    876                }
    877            }
    878        }
    879
    880        if (matcherSize == 0 && offset < inputLineLength) {
    881            offset--;
    882        }
    883
    884        // Whatever is left in line is short enough to just pass through
    885        wrappedLine.append(str, offset, str.length());
    886
    887        return wrappedLine.toString();
    888    }
    889
    890    /**
    891     * {@code WordUtils} instances should NOT be constructed in
    892     * standard programming. Instead, the class should be used as
    893     * {@code WordUtils.wrap("foo bar", 20);}.
    894     *
    895     * <p>This constructor is public to permit tools that require a JavaBean
    896     * instance to operate.</p>
    897     */
    898    public WordUtils() {
    899    }
    900 }