code: /* LanguageTool, a natural language style checker
   * Copyright (C) 2014 Daniel Naber (http://danielnaber.de/)
   *
   * This library is free software; you can redistribute it and/or
   * modify it under the terms of the GNU Lesser General Public
   * License as published by the Free Software Foundation; either
   * version 2.1 of the License, or (at your option) any later version.
   *
   * This library is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   * Lesser General Public License for more details.
   *
   * You should have received a copy of the GNU Lesser General Public
   * License along with this library; if not, write to the Free Software
   * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
   * USA
   */
  package org.languagetool.rules;
  
  import java.io.IOException;
  import java.util.*;
  import java.util.regex.Pattern;
  
  import org.languagetool.AnalyzedSentence;
  import org.languagetool.AnalyzedTokenReadings;
  import org.languagetool.Language;
  import org.languagetool.Tag;
  import org.languagetool.tokenizers.WordTokenizer;
  import org.languagetool.tools.Tools;
  
  /**
   * A rule that checks for a punctuation mark at the end of a paragraph.
   * @author Fred Kruse
   * @since 4.1
   */
  public 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 {
      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);
    }
  
    @Override
    public int minToCheckParagraph() {
      return 0;
    }
  
  }
  