001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.checks.javadoc;
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.BitSet;
025import java.util.List;
026import java.util.Optional;
027import java.util.regex.Pattern;
028import java.util.stream.Stream;
029
030import com.puppycrawl.tools.checkstyle.StatelessCheck;
031import com.puppycrawl.tools.checkstyle.api.DetailNode;
032import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
033import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
034import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
035import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
036
037/**
038 * <div>
039 * Checks that
040 * <a href="https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html#firstsentence">
041 * Javadoc summary sentence</a> does not contain phrases that are not recommended to use.
042 * Summaries that contain only the {@code {@inheritDoc}} tag are skipped.
043 * Summaries that contain a non-empty {@code {@return}} are allowed.
044 * Check also violate Javadoc that does not contain first sentence, though with {@code {@return}} a
045 * period is not required as the Javadoc tool adds it.
046 * </div>
047 *
048 * <p>
049 * Note: For defining a summary, both the first sentence and the @summary tag approaches
050 * are supported.
051 * </p>
052 *
053 * <ul>
054 * <li>
055 * Property {@code forbiddenSummaryFragments} - Specify the regexp for forbidden summary fragments.
056 * Type is {@code java.util.regex.Pattern}.
057 * Default value is {@code "^$"}.
058 * </li>
059 * <li>
060 * Property {@code period} - Specify the period symbol. Used to check the first sentence ends with a
061 * period. Periods that are not followed by a whitespace character are ignored (eg. the period in
062 * v1.0). Because some periods include whitespace built into the character, if this is set to a
063 * non-default value any period will end the sentence, whether it is followed by whitespace or not.
064 * Type is {@code java.lang.String}.
065 * Default value is {@code "."}.
066 * </li>
067 * <li>
068 * Property {@code violateExecutionOnNonTightHtml} - Control when to print violations
069 * if the Javadoc being examined by this check violates the tight html rules defined at
070 * <a href="https://checkstyle.org/writingjavadocchecks.html#Tight-HTML_rules">Tight-HTML Rules</a>.
071 * Type is {@code boolean}.
072 * Default value is {@code false}.
073 * </li>
074 * </ul>
075 *
076 * <p>
077 * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
078 * </p>
079 *
080 * <p>
081 * Violation Message Keys:
082 * </p>
083 * <ul>
084 * <li>
085 * {@code javadoc.missed.html.close}
086 * </li>
087 * <li>
088 * {@code javadoc.parse.rule.error}
089 * </li>
090 * <li>
091 * {@code javadoc.unclosedHtml}
092 * </li>
093 * <li>
094 * {@code javadoc.wrong.singleton.html.tag}
095 * </li>
096 * <li>
097 * {@code summary.first.sentence}
098 * </li>
099 * <li>
100 * {@code summary.javaDoc}
101 * </li>
102 * <li>
103 * {@code summary.javaDoc.missing}
104 * </li>
105 * <li>
106 * {@code summary.javaDoc.missing.period}
107 * </li>
108 * </ul>
109 *
110 * @since 6.0
111 */
112@StatelessCheck
113public class SummaryJavadocCheck extends AbstractJavadocCheck {
114
115    /**
116     * A key is pointing to the warning message text in "messages.properties"
117     * file.
118     */
119    public static final String MSG_SUMMARY_FIRST_SENTENCE = "summary.first.sentence";
120
121    /**
122     * A key is pointing to the warning message text in "messages.properties"
123     * file.
124     */
125    public static final String MSG_SUMMARY_JAVADOC = "summary.javaDoc";
126
127    /**
128     * A key is pointing to the warning message text in "messages.properties"
129     * file.
130     */
131    public static final String MSG_SUMMARY_JAVADOC_MISSING = "summary.javaDoc.missing";
132
133    /**
134     * A key is pointing to the warning message text in "messages.properties" file.
135     */
136    public static final String MSG_SUMMARY_MISSING_PERIOD = "summary.javaDoc.missing.period";
137
138    /**
139     * This regexp is used to convert multiline javadoc to single-line without stars.
140     */
141    private static final Pattern JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN =
142            Pattern.compile("\n +(\\*)|^ +(\\*)");
143
144    /**
145     * This regexp is used to remove html tags, whitespace, and asterisks from a string.
146     */
147    private static final Pattern HTML_ELEMENTS =
148            Pattern.compile("<[^>]*>");
149
150    /** Default period literal. */
151    private static final String DEFAULT_PERIOD = ".";
152
153    /** Summary tag text. */
154    private static final String SUMMARY_TEXT = "@summary";
155
156    /** Return tag text. */
157    private static final String RETURN_TEXT = "@return";
158
159    /** Set of allowed Tokens tags in summary java doc. */
160    private static final BitSet ALLOWED_TYPES = TokenUtil.asBitSet(
161                    JavadocTokenTypes.WS,
162                    JavadocTokenTypes.DESCRIPTION,
163                    JavadocTokenTypes.TEXT);
164
165    /**
166     * Specify the regexp for forbidden summary fragments.
167     */
168    private Pattern forbiddenSummaryFragments = CommonUtil.createPattern("^$");
169
170    /**
171     * Specify the period symbol. Used to check the first sentence ends with a period. Periods that
172     * are not followed by a whitespace character are ignored (eg. the period in v1.0). Because some
173     * periods include whitespace built into the character, if this is set to a non-default value
174     * any period will end the sentence, whether it is followed by whitespace or not.
175     */
176    private String period = DEFAULT_PERIOD;
177
178    /**
179     * Setter to specify the regexp for forbidden summary fragments.
180     *
181     * @param pattern a pattern.
182     * @since 6.0
183     */
184    public void setForbiddenSummaryFragments(Pattern pattern) {
185        forbiddenSummaryFragments = pattern;
186    }
187
188    /**
189     * Setter to specify the period symbol. Used to check the first sentence ends with a period.
190     * Periods that are not followed by a whitespace character are ignored (eg. the period in v1.0).
191     * Because some periods include whitespace built into the character, if this is set to a
192     * non-default value any period will end the sentence, whether it is followed by whitespace or
193     * not.
194     *
195     * @param period period's value.
196     * @since 6.2
197     */
198    public void setPeriod(String period) {
199        this.period = period;
200    }
201
202    @Override
203    public int[] getDefaultJavadocTokens() {
204        return new int[] {
205            JavadocTokenTypes.JAVADOC,
206        };
207    }
208
209    @Override
210    public int[] getRequiredJavadocTokens() {
211        return getAcceptableJavadocTokens();
212    }
213
214    @Override
215    public void visitJavadocToken(DetailNode ast) {
216        final Optional<DetailNode> inlineTagNode = getInlineTagNode(ast);
217        boolean shouldValidateUntaggedSummary = true;
218        if (inlineTagNode.isPresent()) {
219            final DetailNode node = inlineTagNode.get();
220            if (isSummaryTag(node) && isDefinedFirst(node)) {
221                shouldValidateUntaggedSummary = false;
222                validateSummaryTag(node);
223            }
224            else if (isInlineReturnTag(node)) {
225                shouldValidateUntaggedSummary = false;
226                validateInlineReturnTag(node);
227            }
228        }
229        if (shouldValidateUntaggedSummary && !startsWithInheritDoc(ast)) {
230            validateUntaggedSummary(ast);
231        }
232    }
233
234    /**
235     * Checks the javadoc text for {@code period} at end and forbidden fragments.
236     *
237     * @param ast the javadoc text node
238     */
239    private void validateUntaggedSummary(DetailNode ast) {
240        final String summaryDoc = getSummarySentence(ast);
241        if (summaryDoc.isEmpty()) {
242            log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
243        }
244        else if (!period.isEmpty()) {
245            if (summaryDoc.contains(period)) {
246                final Optional<String> firstSentence = getFirstSentence(ast, period);
247
248                if (firstSentence.isPresent()) {
249                    if (containsForbiddenFragment(firstSentence.get())) {
250                        log(ast.getLineNumber(), MSG_SUMMARY_JAVADOC);
251                    }
252                }
253                else {
254                    log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
255                }
256            }
257            else {
258                log(ast.getLineNumber(), MSG_SUMMARY_FIRST_SENTENCE);
259            }
260        }
261    }
262
263    /**
264     * Gets the node for the inline tag if present.
265     *
266     * @param javadoc javadoc root node.
267     * @return the node for the inline tag if present.
268     */
269    private static Optional<DetailNode> getInlineTagNode(DetailNode javadoc) {
270        return Arrays.stream(javadoc.getChildren())
271            .filter(SummaryJavadocCheck::isInlineTagPresent)
272            .findFirst()
273            .map(SummaryJavadocCheck::getInlineTagNodeForAst);
274    }
275
276    /**
277     * Whether the {@code {@summary}} tag is defined first in the javadoc.
278     *
279     * @param inlineSummaryTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
280     * @return {@code true} if the {@code {@summary}} tag is defined first in the javadoc
281     */
282    private static boolean isDefinedFirst(DetailNode inlineSummaryTag) {
283        boolean isDefinedFirst = true;
284        DetailNode currentAst = inlineSummaryTag;
285        while (currentAst != null && isDefinedFirst) {
286            switch (currentAst.getType()) {
287                case JavadocTokenTypes.TEXT:
288                    isDefinedFirst = currentAst.getText().isBlank();
289                    break;
290                case JavadocTokenTypes.HTML_ELEMENT:
291                    isDefinedFirst = !isTextPresentInsideHtmlTag(currentAst);
292                    break;
293                default:
294                    break;
295            }
296            currentAst = JavadocUtil.getPreviousSibling(currentAst);
297        }
298        return isDefinedFirst;
299    }
300
301    /**
302     * Whether some text is present inside the HTML element or tag.
303     *
304     * @param node DetailNode of type {@link JavadocTokenTypes#HTML_TAG}
305     *             or {@link JavadocTokenTypes#HTML_ELEMENT}
306     * @return {@code true} if some text is present inside the HTML element or tag
307     */
308    public static boolean isTextPresentInsideHtmlTag(DetailNode node) {
309        DetailNode nestedChild = JavadocUtil.getFirstChild(node);
310        if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
311            nestedChild = JavadocUtil.getFirstChild(nestedChild);
312        }
313        boolean isTextPresentInsideHtmlTag = false;
314        while (nestedChild != null && !isTextPresentInsideHtmlTag) {
315            switch (nestedChild.getType()) {
316                case JavadocTokenTypes.TEXT:
317                    isTextPresentInsideHtmlTag = !nestedChild.getText().isBlank();
318                    break;
319                case JavadocTokenTypes.HTML_TAG:
320                case JavadocTokenTypes.HTML_ELEMENT:
321                    isTextPresentInsideHtmlTag = isTextPresentInsideHtmlTag(nestedChild);
322                    break;
323                default:
324                    break;
325            }
326            nestedChild = JavadocUtil.getNextSibling(nestedChild);
327        }
328        return isTextPresentInsideHtmlTag;
329    }
330
331    /**
332     * Checks if the inline tag node is present.
333     *
334     * @param ast ast node to check.
335     * @return true, if the inline tag node is present.
336     */
337    private static boolean isInlineTagPresent(DetailNode ast) {
338        return getInlineTagNodeForAst(ast) != null;
339    }
340
341    /**
342     * Returns an inline javadoc tag node that is within a html tag.
343     *
344     * @param ast html tag node.
345     * @return inline summary javadoc tag node or null if no node is found.
346     */
347    private static DetailNode getInlineTagNodeForAst(DetailNode ast) {
348        DetailNode node = ast;
349        DetailNode result = null;
350        // node can never be null as this method is called when there is a HTML_ELEMENT
351        if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
352            result = node;
353        }
354        else if (node.getType() == JavadocTokenTypes.HTML_TAG) {
355            // HTML_TAG always has more than 2 children.
356            node = node.getChildren()[1];
357            result = getInlineTagNodeForAst(node);
358        }
359        else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT
360                // Condition for SINGLETON html element which cannot contain summary node
361                && node.getChildren()[0].getChildren().length > 1) {
362            // Html elements have one tested tag before actual content inside it
363            node = node.getChildren()[0].getChildren()[1];
364            result = getInlineTagNodeForAst(node);
365        }
366        return result;
367    }
368
369    /**
370     * Checks if the javadoc inline tag is {@code {@summary}} tag.
371     *
372     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
373     * @return {@code true} if inline tag is summary tag.
374     */
375    private static boolean isSummaryTag(DetailNode javadocInlineTag) {
376        return isInlineTagWithName(javadocInlineTag, SUMMARY_TEXT);
377    }
378
379    /**
380     * Checks if the first tag inside ast is {@code {@return}} tag.
381     *
382     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
383     * @return {@code true} if first tag is return tag.
384     */
385    private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
386        return isInlineTagWithName(javadocInlineTag, RETURN_TEXT);
387    }
388
389    /**
390     * Checks if the first tag inside ast is a tag with the given name.
391     *
392     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
393     * @param name name of inline tag.
394     *
395     * @return {@code true} if first tag is a tag with the given name.
396     */
397    private static boolean isInlineTagWithName(DetailNode javadocInlineTag, String name) {
398        final DetailNode[] child = javadocInlineTag.getChildren();
399
400        // Checking size of ast is not required, since ast contains
401        // children of Inline Tag, as at least 2 children will be present which are
402        // RCURLY and LCURLY.
403        return name.equals(child[1].getText());
404    }
405
406    /**
407     * Checks the inline summary (if present) for {@code period} at end and forbidden fragments.
408     *
409     * @param inlineSummaryTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
410     */
411    private void validateSummaryTag(DetailNode inlineSummaryTag) {
412        final String inlineSummary = getContentOfInlineCustomTag(inlineSummaryTag);
413        final String summaryVisible = getVisibleContent(inlineSummary);
414        if (summaryVisible.isEmpty()) {
415            log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
416        }
417        else if (!period.isEmpty()) {
418            final boolean isPeriodNotAtEnd =
419                    summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
420            if (isPeriodNotAtEnd) {
421                log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD);
422            }
423            else if (containsForbiddenFragment(inlineSummary)) {
424                log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
425            }
426        }
427    }
428
429    /**
430     * Checks the inline return for forbidden fragments.
431     *
432     * @param inlineReturnTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
433     */
434    private void validateInlineReturnTag(DetailNode inlineReturnTag) {
435        final String inlineReturn = getContentOfInlineCustomTag(inlineReturnTag);
436        final String returnVisible = getVisibleContent(inlineReturn);
437        if (returnVisible.isEmpty()) {
438            log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
439        }
440        else if (containsForbiddenFragment(inlineReturn)) {
441            log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
442        }
443    }
444
445    /**
446     * Gets the content of inline custom tag.
447     *
448     * @param inlineTag inline tag node.
449     * @return String consisting of the content of inline custom tag.
450     */
451    public static String getContentOfInlineCustomTag(DetailNode inlineTag) {
452        final DetailNode[] childrenOfInlineTag = inlineTag.getChildren();
453        final StringBuilder customTagContent = new StringBuilder(256);
454        final int indexOfContentOfSummaryTag = 3;
455        if (childrenOfInlineTag.length != indexOfContentOfSummaryTag) {
456            DetailNode currentNode = childrenOfInlineTag[indexOfContentOfSummaryTag];
457            while (currentNode.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
458                extractInlineTagContent(currentNode, customTagContent);
459                currentNode = JavadocUtil.getNextSibling(currentNode);
460            }
461        }
462        return customTagContent.toString();
463    }
464
465    /**
466     * Extracts the content of inline custom tag recursively.
467     *
468     * @param node DetailNode
469     * @param customTagContent content of custom tag
470     */
471    private static void extractInlineTagContent(DetailNode node,
472        StringBuilder customTagContent) {
473        final DetailNode[] children = node.getChildren();
474        if (children.length == 0) {
475            customTagContent.append(node.getText());
476        }
477        else {
478            for (DetailNode child : children) {
479                if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
480                    extractInlineTagContent(child, customTagContent);
481                }
482            }
483        }
484    }
485
486    /**
487     * Gets the string that is visible to user in javadoc.
488     *
489     * @param summary entire content of summary javadoc.
490     * @return string that is visible to user in javadoc.
491     */
492    private static String getVisibleContent(String summary) {
493        final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
494        return visibleSummary.trim();
495    }
496
497    /**
498     * Tests if first sentence contains forbidden summary fragment.
499     *
500     * @param firstSentence string with first sentence.
501     * @return {@code true} if first sentence contains forbidden summary fragment.
502     */
503    private boolean containsForbiddenFragment(String firstSentence) {
504        final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
505                .matcher(firstSentence).replaceAll(" ");
506        return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
507    }
508
509    /**
510     * Trims the given {@code text} of duplicate whitespaces.
511     *
512     * @param text the text to transform.
513     * @return the finalized form of the text.
514     */
515    private static String trimExcessWhitespaces(String text) {
516        final StringBuilder result = new StringBuilder(256);
517        boolean previousWhitespace = true;
518
519        for (char letter : text.toCharArray()) {
520            final char print;
521            if (Character.isWhitespace(letter)) {
522                if (previousWhitespace) {
523                    continue;
524                }
525
526                previousWhitespace = true;
527                print = ' ';
528            }
529            else {
530                previousWhitespace = false;
531                print = letter;
532            }
533
534            result.append(print);
535        }
536
537        return result.toString();
538    }
539
540    /**
541     * Checks if the node starts with an {&#64;inheritDoc}.
542     *
543     * @param root the root node to examine.
544     * @return {@code true} if the javadoc starts with an {&#64;inheritDoc}.
545     */
546    private static boolean startsWithInheritDoc(DetailNode root) {
547        boolean found = false;
548
549        for (DetailNode child : root.getChildren()) {
550            if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG
551                    && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) {
552                found = true;
553            }
554            if ((child.getType() == JavadocTokenTypes.TEXT
555                    || child.getType() == JavadocTokenTypes.HTML_ELEMENT)
556                    && !CommonUtil.isBlank(child.getText())) {
557                break;
558            }
559        }
560
561        return found;
562    }
563
564    /**
565     * Finds and returns summary sentence.
566     *
567     * @param ast javadoc root node.
568     * @return violation string.
569     */
570    private static String getSummarySentence(DetailNode ast) {
571        final StringBuilder result = new StringBuilder(256);
572        for (DetailNode child : ast.getChildren()) {
573            if (child.getType() != JavadocTokenTypes.EOF
574                    && ALLOWED_TYPES.get(child.getType())) {
575                result.append(child.getText());
576            }
577            else {
578                final String summary = result.toString();
579                if (child.getType() == JavadocTokenTypes.HTML_ELEMENT
580                        && CommonUtil.isBlank(summary)) {
581                    result.append(getStringInsideTag(summary,
582                            child.getChildren()[0].getChildren()[0]));
583                }
584            }
585        }
586        return result.toString().trim();
587    }
588
589    /**
590     * Get concatenated string within text of html tags.
591     *
592     * @param result javadoc string
593     * @param detailNode javadoc tag node
594     * @return java doc tag content appended in result
595     */
596    private static String getStringInsideTag(String result, DetailNode detailNode) {
597        final StringBuilder contents = new StringBuilder(result);
598        DetailNode tempNode = detailNode;
599        while (tempNode != null) {
600            if (tempNode.getType() == JavadocTokenTypes.TEXT) {
601                contents.append(tempNode.getText());
602            }
603            tempNode = JavadocUtil.getNextSibling(tempNode);
604        }
605        return contents.toString();
606    }
607
608    /**
609     * Finds the first sentence.
610     *
611     * @param ast The Javadoc root node.
612     * @param period The configured period symbol.
613     * @return An Optional containing the first sentence
614     *     up to and excluding the period, or an empty
615     *     Optional if no ending was found.
616     */
617    private static Optional<String> getFirstSentence(DetailNode ast, String period) {
618        final List<String> sentenceParts = new ArrayList<>();
619        Optional<String> result = Optional.empty();
620        for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
621            final Optional<String> sentenceEnding = findSentenceEnding(text, period);
622
623            if (sentenceEnding.isPresent()) {
624                sentenceParts.add(sentenceEnding.get());
625                result = Optional.of(String.join("", sentenceParts));
626                break;
627            }
628            else {
629                sentenceParts.add(text);
630            }
631        }
632        return result;
633    }
634
635    /**
636     * Streams through all the text under the given node.
637     *
638     * @param node The Javadoc node to examine.
639     * @return All the text in all nodes that have no child nodes.
640     */
641    private static Stream<String> streamTextParts(DetailNode node) {
642        final Stream<String> stream;
643        if (node.getChildren().length == 0) {
644            stream = Stream.of(node.getText());
645        }
646        else {
647            stream = Stream.of(node.getChildren())
648                .flatMap(SummaryJavadocCheck::streamTextParts);
649        }
650        return stream;
651    }
652
653    /**
654     * Finds the end of a sentence. The end of sentence detection here could be replaced in the
655     * future by Java's built-in BreakIterator class.
656     *
657     * @param text The string to search.
658     * @param period The period character to find.
659     * @return An Optional containing the string up to and excluding the period,
660     *     or empty Optional if no ending was found.
661     */
662    private static Optional<String> findSentenceEnding(String text, String period) {
663        int periodIndex = text.indexOf(period);
664        Optional<String> result = Optional.empty();
665        while (periodIndex >= 0) {
666            final int afterPeriodIndex = periodIndex + period.length();
667
668            // Handle western period separately as it is only the end of a sentence if followed
669            // by whitespace. Other period characters often include whitespace in the character.
670            if (!DEFAULT_PERIOD.equals(period)
671                || afterPeriodIndex >= text.length()
672                || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
673                final String resultStr = text.substring(0, periodIndex);
674                result = Optional.of(resultStr);
675                break;
676            }
677            else {
678                periodIndex = text.indexOf(period, afterPeriodIndex);
679            }
680        }
681        return result;
682    }
683}