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 {@inheritDoc}. 542 * 543 * @param root the root node to examine. 544 * @return {@code true} if the javadoc starts with an {@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}