class PunctuationMarkAtParagraphEnd extends TextLevelRule {

  private final static String[] PUNCTUATION_MARKS = {".", "!", "?", ":", ",", ";"};
  private final static String[] QUOTATION_MARKS = {"„", "»", "«", "\"", "”", "″", "’", "‚", "‘", "›", "‹", "′", "'"};
  private final static Pattern P_NUMERIC = Pattern.compile("[0-9.]+");
  
  private final Language lang;

  /**
   * @since 4.5
   */
  public PunctuationMarkAtParagraphEnd(ResourceBundle messages, Language lang, boolean defaultActive) {
    super(messages);
    this.lang = Objects.requireNonNull(lang);
    super.setCategory(Categories.PUNCTUATION.getCategory(messages));
    setLocQualityIssueType(ITSIssueType.Grammar);
    setTags(Collections.singletonList(Tag.picky));
    if (!defaultActive) {
      setDefaultOff();
    }
  }

  public PunctuationMarkAtParagraphEnd(ResourceBundle messages, Language lang) {
    this(messages, lang, true);
  }

  @Override
  public String getId() {
    return "PUNCTUATION_PARAGRAPH_END";
  }

  @Override
  public String getDescription() {
    return messages.getString("punctuation_mark_paragraph_end_desc");
  }
  
  private static boolean stringEqualsAny(String token, String[] any) {
    for (String s : any) {
      if (token.equals(s)) {
        return true;
      }
    }
    return false;
  }

  private static boolean isQuotationMark(AnalyzedTokenReadings tk) {
    return stringEqualsAny(tk.getToken(), QUOTATION_MARKS);
  }

  private static boolean isPunctuationMark(AnalyzedTokenReadings tk) {
    return stringEqualsAny(tk.getToken(), PUNCTUATION_MARKS);
  }

  private static boolean isWord(AnalyzedTokenReadings tk) {
    return Character.isLetter(tk.getToken().charAt(0));
  }
  
  private static boolean isNumeric(String s) {
   return P_NUMERIC.matcher(s.trim()).matches();
  }

  @Override
  public RuleMatch[] match(List<AnalyzedSentence> sentences) throws IOException {<FILL_FUNCTION_BODY>}

  @Override
  public int minToCheckParagraph() {
    return 0;
  }

}

    List<RuleMatch> ruleMatches = new ArrayList<>();
    int lastPara = -1;
    int pos = 0;
    boolean isFirstWord;
    for (int n = 0; n < sentences.size(); n++) {
      AnalyzedSentence sentence = sentences.get(n);
      if (Tools.isParagraphEnd(sentences, n, lang)) {
        AnalyzedTokenReadings[] tokens = sentence.getTokensWithoutWhitespace();
        if (tokens.length > 2) {
          isFirstWord = (isWord(tokens[1]) && !isPunctuationMark(tokens[2]))
                || (tokens.length > 3 && isQuotationMark(tokens[1]) && isWord(tokens[2]) && !isPunctuationMark(tokens[3]));
          // ignore sentences like "2.2.2. This is an item" (two sentences, first sentence only numbers)
          boolean ignoreSentence = false;
          if (n == 1 && isNumeric(sentences.get(0).getText())) {
            ignoreSentence = true;
          }
          if (n > 0 && isNumeric(sentences.get(n - 1).getText())) {
            ignoreSentence = true;
          }
          // paragraphs containing less than two sentences (e.g. headlines, listings) are excluded from rule
          if (n - lastPara > 1 && isFirstWord && !ignoreSentence) {
            int lastNWToken = tokens.length - 1;
            while (tokens[lastNWToken].isLinebreak()) {
              lastNWToken--;
            }
            if (tokens[tokens.length-2].getToken().equalsIgnoreCase(":") &&
                WordTokenizer.isUrl(tokens[tokens.length-1].getToken())) {
              // e.g. "find it at: http://example.com" should not be an error
              lastPara = n;
              pos += sentence.getText().length();
              continue;
            }
            if (isWord(tokens[lastNWToken]) 
                || (isQuotationMark(tokens[lastNWToken]) && isWord(tokens[lastNWToken-1]))) {
              int fromPos = pos + tokens[lastNWToken].getStartPos();
              int toPos = pos + tokens[lastNWToken].getEndPos();
              RuleMatch ruleMatch = new RuleMatch(this, sentence, fromPos, toPos, 
                  messages.getString("punctuation_mark_paragraph_end_msg"));
              List<String> replacements = new ArrayList<>();
              for (String mark : PUNCTUATION_MARKS) {
                replacements.add(tokens[lastNWToken].getToken() + mark);
              }
              ruleMatch.setSuggestedReplacements(replacements);
              ruleMatches.add(ruleMatch);
            }
          }
        }
        lastPara = n;
      }
      pos += sentence.getCorrectedTextLength();
    }
    return toRuleMatchArray(ruleMatches);
  
<methods>public void <init>() ,public void <init>(java.util.ResourceBundle) ,public int estimateContextForSureMatch() ,public org.languagetool.rules.RuleMatch[] match(List<org.languagetool.AnalyzedSentence>, org.languagetool.markup.AnnotatedText) throws java.io.IOException,public abstract org.languagetool.rules.RuleMatch[] match(List<org.languagetool.AnalyzedSentence>) throws java.io.IOException,public final org.languagetool.rules.RuleMatch[] match(org.languagetool.AnalyzedSentence) throws java.io.IOException,public abstract int minToCheckParagraph() <variables>