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