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