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