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            isDefinedFirst = switch (currentAst.getType()) {
287                case JavadocTokenTypes.TEXT -> currentAst.getText().isBlank();
288                case JavadocTokenTypes.HTML_ELEMENT -> !isTextPresentInsideHtmlTag(currentAst);
289                default -> isDefinedFirst;
290            };
291            currentAst = JavadocUtil.getPreviousSibling(currentAst);
292        }
293        return isDefinedFirst;
294    }
295
296    /**
297     * Whether some text is present inside the HTML element or tag.
298     *
299     * @param node DetailNode of type {@link JavadocTokenTypes#HTML_TAG}
300     *             or {@link JavadocTokenTypes#HTML_ELEMENT}
301     * @return {@code true} if some text is present inside the HTML element or tag
302     */
303    public static boolean isTextPresentInsideHtmlTag(DetailNode node) {
304        DetailNode nestedChild = JavadocUtil.getFirstChild(node);
305        if (node.getType() == JavadocTokenTypes.HTML_ELEMENT) {
306            nestedChild = JavadocUtil.getFirstChild(nestedChild);
307        }
308        boolean isTextPresentInsideHtmlTag = false;
309        while (nestedChild != null && !isTextPresentInsideHtmlTag) {
310            isTextPresentInsideHtmlTag = switch (nestedChild.getType()) {
311                case JavadocTokenTypes.TEXT -> !nestedChild.getText().isBlank();
312                case JavadocTokenTypes.HTML_TAG, JavadocTokenTypes.HTML_ELEMENT ->
313                    isTextPresentInsideHtmlTag(nestedChild);
314                default -> isTextPresentInsideHtmlTag;
315            };
316            nestedChild = JavadocUtil.getNextSibling(nestedChild);
317        }
318        return isTextPresentInsideHtmlTag;
319    }
320
321    /**
322     * Checks if the inline tag node is present.
323     *
324     * @param ast ast node to check.
325     * @return true, if the inline tag node is present.
326     */
327    private static boolean isInlineTagPresent(DetailNode ast) {
328        return getInlineTagNodeForAst(ast) != null;
329    }
330
331    /**
332     * Returns an inline javadoc tag node that is within a html tag.
333     *
334     * @param ast html tag node.
335     * @return inline summary javadoc tag node or null if no node is found.
336     */
337    private static DetailNode getInlineTagNodeForAst(DetailNode ast) {
338        DetailNode node = ast;
339        DetailNode result = null;
340        // node can never be null as this method is called when there is a HTML_ELEMENT
341        if (node.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG) {
342            result = node;
343        }
344        else if (node.getType() == JavadocTokenTypes.HTML_TAG) {
345            // HTML_TAG always has more than 2 children.
346            node = node.getChildren()[1];
347            result = getInlineTagNodeForAst(node);
348        }
349        else if (node.getType() == JavadocTokenTypes.HTML_ELEMENT
350                // Condition for SINGLETON html element which cannot contain summary node
351                && node.getChildren()[0].getChildren().length > 1) {
352            // Html elements have one tested tag before actual content inside it
353            node = node.getChildren()[0].getChildren()[1];
354            result = getInlineTagNodeForAst(node);
355        }
356        return result;
357    }
358
359    /**
360     * Checks if the javadoc inline tag is {@code {@summary}} tag.
361     *
362     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
363     * @return {@code true} if inline tag is summary tag.
364     */
365    private static boolean isSummaryTag(DetailNode javadocInlineTag) {
366        return isInlineTagWithName(javadocInlineTag, SUMMARY_TEXT);
367    }
368
369    /**
370     * Checks if the first tag inside ast is {@code {@return}} tag.
371     *
372     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
373     * @return {@code true} if first tag is return tag.
374     */
375    private static boolean isInlineReturnTag(DetailNode javadocInlineTag) {
376        return isInlineTagWithName(javadocInlineTag, RETURN_TEXT);
377    }
378
379    /**
380     * Checks if the first tag inside ast is a tag with the given name.
381     *
382     * @param javadocInlineTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
383     * @param name name of inline tag.
384     *
385     * @return {@code true} if first tag is a tag with the given name.
386     */
387    private static boolean isInlineTagWithName(DetailNode javadocInlineTag, String name) {
388        final DetailNode[] child = javadocInlineTag.getChildren();
389
390        // Checking size of ast is not required, since ast contains
391        // children of Inline Tag, as at least 2 children will be present which are
392        // RCURLY and LCURLY.
393        return name.equals(child[1].getText());
394    }
395
396    /**
397     * Checks the inline summary (if present) for {@code period} at end and forbidden fragments.
398     *
399     * @param inlineSummaryTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
400     */
401    private void validateSummaryTag(DetailNode inlineSummaryTag) {
402        final String inlineSummary = getContentOfInlineCustomTag(inlineSummaryTag);
403        final String summaryVisible = getVisibleContent(inlineSummary);
404        if (summaryVisible.isEmpty()) {
405            log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
406        }
407        else if (!period.isEmpty()) {
408            final boolean isPeriodNotAtEnd =
409                    summaryVisible.lastIndexOf(period) != summaryVisible.length() - 1;
410            if (isPeriodNotAtEnd) {
411                log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_MISSING_PERIOD);
412            }
413            else if (containsForbiddenFragment(inlineSummary)) {
414                log(inlineSummaryTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
415            }
416        }
417    }
418
419    /**
420     * Checks the inline return for forbidden fragments.
421     *
422     * @param inlineReturnTag node of type {@link JavadocTokenTypes#JAVADOC_INLINE_TAG}
423     */
424    private void validateInlineReturnTag(DetailNode inlineReturnTag) {
425        final String inlineReturn = getContentOfInlineCustomTag(inlineReturnTag);
426        final String returnVisible = getVisibleContent(inlineReturn);
427        if (returnVisible.isEmpty()) {
428            log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC_MISSING);
429        }
430        else if (containsForbiddenFragment(inlineReturn)) {
431            log(inlineReturnTag.getLineNumber(), MSG_SUMMARY_JAVADOC);
432        }
433    }
434
435    /**
436     * Gets the content of inline custom tag.
437     *
438     * @param inlineTag inline tag node.
439     * @return String consisting of the content of inline custom tag.
440     */
441    public static String getContentOfInlineCustomTag(DetailNode inlineTag) {
442        final DetailNode[] childrenOfInlineTag = inlineTag.getChildren();
443        final StringBuilder customTagContent = new StringBuilder(256);
444        final int indexOfContentOfSummaryTag = 3;
445        if (childrenOfInlineTag.length != indexOfContentOfSummaryTag) {
446            DetailNode currentNode = childrenOfInlineTag[indexOfContentOfSummaryTag];
447            while (currentNode.getType() != JavadocTokenTypes.JAVADOC_INLINE_TAG_END) {
448                extractInlineTagContent(currentNode, customTagContent);
449                currentNode = JavadocUtil.getNextSibling(currentNode);
450            }
451        }
452        return customTagContent.toString();
453    }
454
455    /**
456     * Extracts the content of inline custom tag recursively.
457     *
458     * @param node DetailNode
459     * @param customTagContent content of custom tag
460     */
461    private static void extractInlineTagContent(DetailNode node,
462        StringBuilder customTagContent) {
463        final DetailNode[] children = node.getChildren();
464        if (children.length == 0) {
465            customTagContent.append(node.getText());
466        }
467        else {
468            for (DetailNode child : children) {
469                if (child.getType() != JavadocTokenTypes.LEADING_ASTERISK) {
470                    extractInlineTagContent(child, customTagContent);
471                }
472            }
473        }
474    }
475
476    /**
477     * Gets the string that is visible to user in javadoc.
478     *
479     * @param summary entire content of summary javadoc.
480     * @return string that is visible to user in javadoc.
481     */
482    private static String getVisibleContent(String summary) {
483        final String visibleSummary = HTML_ELEMENTS.matcher(summary).replaceAll("");
484        return visibleSummary.trim();
485    }
486
487    /**
488     * Tests if first sentence contains forbidden summary fragment.
489     *
490     * @param firstSentence string with first sentence.
491     * @return {@code true} if first sentence contains forbidden summary fragment.
492     */
493    private boolean containsForbiddenFragment(String firstSentence) {
494        final String javadocText = JAVADOC_MULTILINE_TO_SINGLELINE_PATTERN
495                .matcher(firstSentence).replaceAll(" ");
496        return forbiddenSummaryFragments.matcher(trimExcessWhitespaces(javadocText)).find();
497    }
498
499    /**
500     * Trims the given {@code text} of duplicate whitespaces.
501     *
502     * @param text the text to transform.
503     * @return the finalized form of the text.
504     */
505    private static String trimExcessWhitespaces(String text) {
506        final StringBuilder result = new StringBuilder(256);
507        boolean previousWhitespace = true;
508
509        for (char letter : text.toCharArray()) {
510            final char print;
511            if (Character.isWhitespace(letter)) {
512                if (previousWhitespace) {
513                    continue;
514                }
515
516                previousWhitespace = true;
517                print = ' ';
518            }
519            else {
520                previousWhitespace = false;
521                print = letter;
522            }
523
524            result.append(print);
525        }
526
527        return result.toString();
528    }
529
530    /**
531     * Checks if the node starts with an {&#64;inheritDoc}.
532     *
533     * @param root the root node to examine.
534     * @return {@code true} if the javadoc starts with an {&#64;inheritDoc}.
535     */
536    private static boolean startsWithInheritDoc(DetailNode root) {
537        boolean found = false;
538
539        for (DetailNode child : root.getChildren()) {
540            if (child.getType() == JavadocTokenTypes.JAVADOC_INLINE_TAG
541                    && child.getChildren()[1].getType() == JavadocTokenTypes.INHERIT_DOC_LITERAL) {
542                found = true;
543            }
544            if ((child.getType() == JavadocTokenTypes.TEXT
545                    || child.getType() == JavadocTokenTypes.HTML_ELEMENT)
546                    && !CommonUtil.isBlank(child.getText())) {
547                break;
548            }
549        }
550
551        return found;
552    }
553
554    /**
555     * Finds and returns summary sentence.
556     *
557     * @param ast javadoc root node.
558     * @return violation string.
559     */
560    private static String getSummarySentence(DetailNode ast) {
561        final StringBuilder result = new StringBuilder(256);
562        for (DetailNode child : ast.getChildren()) {
563            if (child.getType() != JavadocTokenTypes.EOF
564                    && ALLOWED_TYPES.get(child.getType())) {
565                result.append(child.getText());
566            }
567            else {
568                final String summary = result.toString();
569                if (child.getType() == JavadocTokenTypes.HTML_ELEMENT
570                        && CommonUtil.isBlank(summary)) {
571                    result.append(getStringInsideTag(summary,
572                            child.getChildren()[0].getChildren()[0]));
573                }
574            }
575        }
576        return result.toString().trim();
577    }
578
579    /**
580     * Get concatenated string within text of html tags.
581     *
582     * @param result javadoc string
583     * @param detailNode javadoc tag node
584     * @return java doc tag content appended in result
585     */
586    private static String getStringInsideTag(String result, DetailNode detailNode) {
587        final StringBuilder contents = new StringBuilder(result);
588        DetailNode tempNode = detailNode;
589        while (tempNode != null) {
590            if (tempNode.getType() == JavadocTokenTypes.TEXT) {
591                contents.append(tempNode.getText());
592            }
593            tempNode = JavadocUtil.getNextSibling(tempNode);
594        }
595        return contents.toString();
596    }
597
598    /**
599     * Finds the first sentence.
600     *
601     * @param ast The Javadoc root node.
602     * @param period The configured period symbol.
603     * @return An Optional containing the first sentence
604     *     up to and excluding the period, or an empty
605     *     Optional if no ending was found.
606     */
607    private static Optional<String> getFirstSentence(DetailNode ast, String period) {
608        final List<String> sentenceParts = new ArrayList<>();
609        Optional<String> result = Optional.empty();
610        for (String text : (Iterable<String>) streamTextParts(ast)::iterator) {
611            final Optional<String> sentenceEnding = findSentenceEnding(text, period);
612
613            if (sentenceEnding.isPresent()) {
614                sentenceParts.add(sentenceEnding.get());
615                result = Optional.of(String.join("", sentenceParts));
616                break;
617            }
618            sentenceParts.add(text);
619        }
620        return result;
621    }
622
623    /**
624     * Streams through all the text under the given node.
625     *
626     * @param node The Javadoc node to examine.
627     * @return All the text in all nodes that have no child nodes.
628     */
629    private static Stream<String> streamTextParts(DetailNode node) {
630        final Stream<String> stream;
631        if (node.getChildren().length == 0) {
632            stream = Stream.of(node.getText());
633        }
634        else {
635            stream = Stream.of(node.getChildren())
636                .flatMap(SummaryJavadocCheck::streamTextParts);
637        }
638        return stream;
639    }
640
641    /**
642     * Finds the end of a sentence. The end of sentence detection here could be replaced in the
643     * future by Java's built-in BreakIterator class.
644     *
645     * @param text The string to search.
646     * @param period The period character to find.
647     * @return An Optional containing the string up to and excluding the period,
648     *     or empty Optional if no ending was found.
649     */
650    private static Optional<String> findSentenceEnding(String text, String period) {
651        int periodIndex = text.indexOf(period);
652        Optional<String> result = Optional.empty();
653        while (periodIndex >= 0) {
654            final int afterPeriodIndex = periodIndex + period.length();
655
656            // Handle western period separately as it is only the end of a sentence if followed
657            // by whitespace. Other period characters often include whitespace in the character.
658            if (!DEFAULT_PERIOD.equals(period)
659                || afterPeriodIndex >= text.length()
660                || Character.isWhitespace(text.charAt(afterPeriodIndex))) {
661                final String resultStr = text.substring(0, periodIndex);
662                result = Optional.of(resultStr);
663                break;
664            }
665            periodIndex = text.indexOf(period, afterPeriodIndex);
666        }
667        return result;
668    }
669}