code: /* LanguageTool, a natural language style checker 
   * Copyright (C) 2020 Daniel Naber (http://www.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 org.apache.commons.lang3.StringUtils;
  import org.languagetool.AnalyzedSentence;
  import org.languagetool.AnalyzedTokenReadings;
  import org.languagetool.Language;
  import org.languagetool.tools.StringTools;
  
  import java.util.*;
  import java.util.concurrent.ArrayBlockingQueue;
  
  /**
   * A rule that checks case in phrases
   * 
   * @author Jaume Ortolà
   */
  public abstract class AbstractCheckCaseRule extends AbstractSimpleReplaceRule2 {
  
    public AbstractCheckCaseRule(ResourceBundle messages, Language language) {
      super(messages, language);
      super.setLocQualityIssueType(ITSIssueType.Typographical);
      super.setCategory(Categories.CASING.getCategory(messages));
    }
  
    @Override
    public RuleMatch[] match(AnalyzedSentence sentence) {
      List<RuleMatch> ruleMatches = new ArrayList<>();
      AnalyzedTokenReadings[] tokens = sentence.getTokensWithoutWhitespace();
  
      List<Map<String, SuggestionWithMessage>> wrongWords = getWrongWords(true);
      if (wrongWords.size() == 0) {
        return toRuleMatchArray(ruleMatches);
      }
      Queue<AnalyzedTokenReadings> prevTokens = new ArrayBlockingQueue<>(wrongWords.size());
      int sentStart = 0;
      while (sentStart + 1 < tokens.length && isPunctuationStart(tokens[sentStart + 1].getToken())) {
        sentStart++;
      }
      for (int i = 1; i < tokens.length; i++) {
        addToQueue(tokens[i], prevTokens);
        StringBuilder sb = new StringBuilder();
        List<String> phrases = new ArrayList<>();
        List<AnalyzedTokenReadings> prevTokensList = Arrays.asList(prevTokens.toArray(new AnalyzedTokenReadings[0]));
        for (int j = prevTokensList.size() - 1; j >= 0; j--) {
          if (j != prevTokensList.size() - 1 && prevTokensList.get(j + 1).isWhitespaceBefore()) {
            sb.insert(0, " ");
          }
          sb.insert(0, prevTokensList.get(j).getToken());
          phrases.add(0, sb.toString());
        }
        if (isTokenException(tokens[i])) {
          continue;
        }
        int len = phrases.size(); // prevTokensList and variants have now the same length
        for (int j = 0; j < len; j++) { // longest words first
          String originalPhrase = phrases.get(j);
          int crtWordCount = len - j;
          SuggestionWithMessage suggMess = wrongWords.get(crtWordCount - 1).get(originalPhrase.toLowerCase(getLocale()));
          if (suggMess == null) {
            continue;
          }
          String correctPhrase = suggMess.getSuggestion();
          String capitalizedCorrect = StringTools.uppercaseFirstChar(correctPhrase);
          int startPos = prevTokensList.get(len - crtWordCount).getStartPos();
          int endPos = prevTokensList.get(len - 1).getEndPos();
          if ((crtWordCount + sentStart == i && originalPhrase.equals(capitalizedCorrect))
              || correctPhrase.equals(originalPhrase)) {
            // remove last match if is contained in a correct phrase
            if (ruleMatches.size() > 0) {
              RuleMatch lastRuleMatch = ruleMatches.get(ruleMatches.size() - 1);
              if (lastRuleMatch.getToPos() > startPos) {
                ruleMatches.remove(ruleMatches.size() - 1);
              }
            }
            // The phrase is correct. Don't look into shorter phrases inside this phrase.
            break;
          }
          if (originalPhrase.equals(originalPhrase.toUpperCase())) {
            continue;
          }
          if (correctPhrase != null && !correctPhrase.equals(originalPhrase)) {
            RuleMatch ruleMatch;
            String msg = suggMess.getMessage();
            if (msg == null) {
              msg = getMessage();
            }
            ruleMatch = new RuleMatch(this, sentence, startPos, endPos, msg, getShort());
            if (subRuleSpecificIds) {
              ruleMatch.setSpecificRuleId(StringTools.toId(getId() + "_" + correctPhrase));
            }
            if (crtWordCount + sentStart == i) {
              // Capitalize suggestion at the sentence start
              correctPhrase = StringTools.uppercaseFirstChar(correctPhrase);
            }
            ruleMatch.addSuggestedReplacement(correctPhrase);
            if (!isException(sentence.getText().substring(startPos, endPos))) {
              // keep only the longest match
              if (ruleMatches.size() > 0) {
                RuleMatch lastRuleMatch = ruleMatches.get(ruleMatches.size() - 1);
                if (lastRuleMatch.getFromPos() == ruleMatch.getFromPos()
                    && lastRuleMatch.getToPos() < ruleMatch.getToPos()) {
                  ruleMatches.remove(ruleMatches.size() - 1);
                }
              }
              ruleMatches.add(ruleMatch);
            }
            break;
          }
        }
      }
      return toRuleMatchArray(ruleMatches);
    }
  
    private boolean isPunctuationStart(String word) {
      return StringUtils.getDigits(word).length() > 0 // e.g. postal codes
          || StringTools.isPunctuationMark(word) || StringTools.isNotWordCharacter(word);
    }
  
  }
  