code: package org.languagetool.remote;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.FileWriter;
  import java.io.IOException;
  import java.io.InputStreamReader;
  import java.nio.charset.Charset;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.NoSuchElementException;
  import java.util.Properties;
  import java.util.Scanner;
  
  import org.languagetool.tools.Tools;
  import org.languagetool.tools.DiffsAsMatches;
  import org.languagetool.tools.PseudoMatch;
  
  public class SentenceAnnotator {
  
    static HashMap<String, List<RemoteRuleMatch>> cachedMatches;
  
    public static void main(String[] args) throws IOException {
      long start = System.currentTimeMillis();
      // use configuration file
      if (args.length == 1) {
        String configurationFilename = args[0];
        Properties prop = new Properties();
        FileInputStream fis = new FileInputStream(configurationFilename);
        prop.load(new InputStreamReader(fis, Charset.forName("UTF-8")));
        AnnotatorConfig cfg = new AnnotatorConfig();
        cfg.remoteServer = prop.getProperty("remoteServer", "http://localhost:8081").trim();
        cfg.userName = prop.getProperty("userName", "").trim();
        cfg.apiKey = prop.getProperty("apiKey", "").trim();
        cfg.inputFilePath = prop.getProperty("inputFile", "").trim();
        cfg.outputFilePath = prop.getProperty("outputFile", "").trim();
        cfg.languageCode = prop.getProperty("languageCode").trim();
        String customParamsStr = prop.getProperty("customParams", "").trim();
        if (!customParamsStr.isEmpty()) {
          for (String customParam : customParamsStr.split(";")) {
            String[] parts = customParam.split(",");
            cfg.customParams.put(parts[0], parts[1]);
          }
        }
        String automaticAnnotationStr = prop.getProperty("automaticAnnotation", "").trim();
        cfg.automaticAnnotation = automaticAnnotationStr.equalsIgnoreCase("yes")
            || automaticAnnotationStr.equalsIgnoreCase("true");
        String enabledOnlyRulesStr = prop.getProperty("enabledOnlyRules", "").trim();
        if (!enabledOnlyRulesStr.isEmpty()) {
          cfg.enabledOnlyRules = Arrays.asList(enabledOnlyRulesStr.split(","));
        }
        String disabledRulesStr = prop.getProperty("disabledRules", "").trim();
        if (!disabledRulesStr.isEmpty()) {
          cfg.disabledRules = Arrays.asList(disabledRulesStr.split(","));
        }
        // defaultColor="\u001B[0m"
        // highlightColor="\u001B[97m"
        cfg.ansiDefault = prop.getProperty("defaultColor", "").trim().replaceAll("\"", "");
        cfg.ansiHighlight = prop.getProperty("highlightColor", "").trim().replaceAll("\"", "");
        cfg.prepareConfiguration();
        if (cfg.automaticAnnotation) {
          runAutomaticAnnotation(cfg);
        } else {
          runAnnotation(cfg);
        }
  
      } else {
        writeHelp();
        System.exit(1);
      }
      System.out.println(printTimeFromStart(start, "Total time:"));
    }
  
    private static void runAnnotation(AnnotatorConfig cfg) throws IOException {
  
      List<String> lines = Files.readAllLines(Paths.get(cfg.inputFilePath));
      int numSentence = 0;
      Scanner sc = new Scanner(System.in);
      System.out.print("Start at line? ");
      String response = sc.nextLine();
      int startLine = 0;
      try {
        startLine = Integer.valueOf(response);
      } catch (NumberFormatException ex) {
        startLine = 0;
      }
      System.out.println("Starting at line " + String.valueOf(startLine) + " of file " + cfg.inputFilePath);
      boolean quit = false;
      for (String line : lines) {
        if (quit) {
          break;
        }
        String sentence = line;
        numSentence++;
        if (numSentence < startLine) {
          continue;
        }
        boolean done = false;
        List<String> fpMatches = new ArrayList<>();
        int annotationsPerSentence = 0;
        while (!done) {
          List<RemoteRuleMatch> matches = getMatches(cfg, sentence);
          RemoteRuleMatch match = null;
          int i = 0;
          boolean isValidMatch = false;
          while (!isValidMatch && i < matches.size()) {
            match = matches.get(i);
            i++;
            isValidMatch = !fpMatches.contains(getMatchIdentifier(sentence, match));
            if (!isValidMatch) {
              match = null;
            }
          }
          String formattedSentence = formatedSentence(sentence, match);
          String formattedCorrectedSentence = formattedSentence;
          String detectedErrorStr = "";
          System.out.println(cfg.ansiDefault + "=============================================");
          System.out.println("Sentence no. " + String.valueOf(numSentence));
          System.out.println("---------------------------------------------");
          System.out.println(cfg.ansiHighlight + formattedSentence + cfg.ansiDefault);
          System.out.println("---------------------------------------------");
          if (match != null) {
            System.out.println(match.getMessage());
            detectedErrorStr = sentence.substring(match.getErrorOffset(),
                match.getErrorOffset() + match.getErrorLength());
          }
          System.out.println(listSuggestions(match));
          System.out.println("---------------------------------------------");
          System.out.print("Action? ");
          response = sc.nextLine();
          if (!response.contains(">>")) {
            response = response.toLowerCase();
          }
          String errorType = "";
          int suggestionPos = -1;
          String suggestionApplied = "";
          int suggestionsTotal = 0;
          if (match != null) {
            suggestionsTotal = match.getReplacements().get().size();
          }
          switch (response) {
          case "r":
            sentence = line;
            cfg.outStrB = new StringBuilder();
            break;
          case "q":
            done = true;
            quit = true;
            writeToOutputFile(cfg);
            break;
          case "d":
            done = true;
            if (annotationsPerSentence == 0) {
              errorType = "OK";
            }
            writeToOutputFile(cfg);
            break;
          case "g":
            done = true;
            errorType = "IG";
            cfg.outStrB = new StringBuilder();
            match = null;
            break;
          case "i":
            fpMatches.add(getMatchIdentifier(sentence, match));
            errorType = "IM";
            break;
          case "b":
            fpMatches.add(getMatchIdentifier(sentence, match));
            errorType = "BO";
            break;
          case "f":
            fpMatches.add(getMatchIdentifier(sentence, match));
            errorType = "FP";
            break;
          case "1":
          case "2":
          case "3":
          case "4":
          case "5":
            errorType = "TP";
            if (suggestionsTotal > 1) {
              errorType = "TPmultiple";
            }
            int r = Integer.valueOf(response);
            if (match != null && r >= 1 && r <= 5) {
              formattedCorrectedSentence = formattedCorrectedSentence(sentence, match, r);
              sentence = replaceSuggestion(sentence, match, r);
              suggestionPos = r;
              suggestionApplied = match.getReplacements().get().get(suggestionPos - 1);
            }
            break;
          }
          if (quit) {
            break;
          }
          if (response.startsWith(">>") && match != null) { // alternative suggestion
            formattedCorrectedSentence = sentence.substring(0, match.getErrorOffset()) + "___" + response.substring(2)
                + "___" + sentence.substring(match.getErrorOffset() + match.getErrorLength());
            sentence = sentence.substring(0, match.getErrorOffset()) + response.substring(2)
                + sentence.substring(match.getErrorOffset() + match.getErrorLength());
            if (suggestionsTotal == 0) {
              errorType = "TPno";
            } else {
              errorType = "TPwrong";
            }
            suggestionApplied = response.substring(2);
          } else if (response.contains(">>")) {
            String[] parts = response.split(">>");
            String toReplace = parts[0];
            String replacement = parts[1];
            int ind = sentence.indexOf(toReplace);
            if (ind > -1) {
              if (sentence.substring(ind + toReplace.length()).indexOf(toReplace) > -1) {
                System.out.println("Cannot replace duplicate string in sentence.");
              } else {
                formattedSentence = sentence.substring(0, ind) + "___" + toReplace + "___"
                    + sentence.substring(ind + toReplace.length());
                formattedCorrectedSentence = sentence.substring(0, ind) + "___" + replacement + "___"
                    + sentence.substring(ind + toReplace.length());
                sentence = sentence.substring(0, ind) + replacement + sentence.substring(ind + toReplace.length());
                System.out.println("FN: replacement done.");
                errorType = "FN";
                suggestionApplied = replacement;
                detectedErrorStr = toReplace;
              }
            }
          }
  
          if (!errorType.isEmpty()) {
            printOutputLine(cfg, numSentence, formattedSentence, formattedCorrectedSentence, errorType, detectedErrorStr,
                suggestionApplied, suggestionPos, suggestionsTotal, getFullId(match), getRuleCategoryId(match),
                getRuleType(match));
            annotationsPerSentence++;
            if (errorType.equals("OK") || errorType.equals("IG")) {
              writeToOutputFile(cfg);
              cfg.outStrB = new StringBuilder();
            }
          }
        }
      }
      sc.close();
      cfg.out.close();
    }
  
    private static void runAutomaticAnnotation(AnnotatorConfig cfg) throws IOException {
      DiffsAsMatches diffsAsMatches = new DiffsAsMatches();
      List<String> lines = Files.readAllLines(Paths.get(cfg.inputFilePath));
      int numSentence = 0;
      System.out.println("Starting at line 1 of file " + cfg.inputFilePath);
      for (String line : lines) {
        numSentence++;
        String[] parts = line.split("\t");
        String sentence = parts[0].replaceAll("__", "");
        String correctedSentence = parts[1].replaceAll("__", "");
        List<PseudoMatch> matchesGolden = diffsAsMatches.getPseudoMatches(sentence, correctedSentence);
        List<RemoteRuleMatch> matches = getMatches(cfg, sentence);
        RemoteRuleMatch match = null;
        correctedSentence = applyAllMatches(sentence, matches);
        List<PseudoMatch> matchesEval = diffsAsMatches.getPseudoMatches(sentence, correctedSentence);
        String errorType = "";
        int iGolden = 0;
        int iEval = 0;
        while (iGolden < matchesGolden.size() || iEval < matchesEval.size()) {
          PseudoMatch iGMatch = null;
          PseudoMatch iEMatch = null;
          if (iGolden < matchesGolden.size()) {
            iGMatch = matchesGolden.get(iGolden);
          }
          if (iEval < matchesEval.size()) {
            iEMatch = matchesEval.get(iEval);
          }
          String formattedOriginalSentence = "";
          String formattedCorrectSentence = "";
          String detectedErrorStr = "";
          String replacement = "";
          if (iGMatch == null) {
            errorType = "FP";
            iEval++;
          } else if (iEMatch == null) {
            errorType = "FN";
            iGolden++;
          } else {
            if (iGMatch.getFromPos() == iEMatch.getFromPos()) {
              if (iEMatch.getReplacements().size() == 0) {
                errorType = "TPns";
              } else if (iGMatch.getReplacements().get(0).equals(iEMatch.getReplacements().get(0))) {
                errorType = "TP";
              } else {
                errorType = "TPws";
              }
              iGolden++;
              iEval++;
            } else if (iGMatch.getFromPos() < iEMatch.getFromPos()) {
              errorType = "FN";
              iGolden++;
            } else if (iGMatch.getFromPos() > iEMatch.getFromPos()) {
              errorType = "FP";
              iEval++;
            }
          }
          switch (errorType) {
          case "FP":
            formattedOriginalSentence = formatedSentence2(sentence, iEMatch);
            formattedCorrectSentence = formattedOriginalSentence;
            detectedErrorStr = sentence.substring(iEMatch.getFromPos(), iEMatch.getToPos());
            replacement = iEMatch.getReplacements().get(0);
            break;
          case "FN":
            formattedOriginalSentence = formatedSentence2(sentence, iGMatch);
            formattedCorrectSentence = formattedCorrectedSentence2(sentence, iGMatch);
            detectedErrorStr = sentence.substring(iGMatch.getFromPos(), iGMatch.getToPos());
            replacement = "";
            break;
          case "TP":
          case "TPns":
          case "TPws":
            formattedOriginalSentence = formatedSentence2(sentence, iGMatch);
            formattedCorrectSentence = formattedCorrectedSentence2(sentence, iGMatch);
            detectedErrorStr = sentence.substring(iGMatch.getFromPos(), iGMatch.getToPos());
            replacement = iEMatch.getReplacements().get(0);
            break;
          }
          printOutputLine(cfg, numSentence, formattedOriginalSentence, formattedCorrectSentence, errorType,
              detectedErrorStr, replacement, -1, 1, getFullId(match), getRuleCategoryId(match), getRuleType(match));
        }
        writeToOutputFile(cfg);
      }
      cfg.out.close();
    }
  
    static private void printOutputLine(AnnotatorConfig cfg, int numSentence, String errorSentence,
        String correctedSentence, String errorType, String detectedErrorStr, String suggestion, int suggestionPos,
        int suggestionsTotal, String ruleId, String ruleCategory, String ruleType) {
      cfg.outStrB.append(String.valueOf(numSentence) + "\t" + errorSentence + "\t" + correctedSentence + "\t" + errorType
          + "\t" + detectedErrorStr + "\t" + suggestion + "\t" + ruleId + "\t" + String.valueOf(suggestionPos) + "\t"
          + String.valueOf(suggestionsTotal) + "\t" + ruleCategory + "\t" + ruleType + "\n");
    }
  
    static private void writeToOutputFile(AnnotatorConfig cfg) throws IOException {
      cfg.out.write(cfg.outStrB.toString());
      cfg.out.flush();
      cfg.outStrB = new StringBuilder();
    }
  
    static private String getMatchIdentifier(String sentence, RemoteRuleMatch match) {
      StringBuilder sb = new StringBuilder();
      sb.append(sentence.substring(match.getErrorOffset(), match.getErrorOffset() + match.getErrorLength()));
      sb.append(getFullId(match));
      sb.append(match.getReplacements().get().toString());
      return sb.toString();
    }
  
    static private String getFullId(RemoteRuleMatch match) {
      String ruleId = "";
      if (match != null) {
        String subId = null;
        try {
          subId = match.getRuleSubId().get();
        } catch (NoSuchElementException e) {
        }
        if (subId != null) {
          ruleId = match.getRuleId() + "[" + subId + "]";
        } else {
          ruleId = match.getRuleId();
        }
      }
      return ruleId;
    }
  
    static private String getRuleCategoryId(RemoteRuleMatch match) {
      String categoryId = "";
      if (match != null) {
        try {
          categoryId = match.getCategoryId().get();
        } catch (NoSuchElementException e) {
        }
      }
      return categoryId;
    }
  
    static private String getRuleType(RemoteRuleMatch match) {
      String ruleType = "";
      if (match != null) {
        try {
          ruleType = match.getLocQualityIssueType().get().toString();
        } catch (NoSuchElementException e) {
        }
      }
      return ruleType;
    }
  
    static private String listSuggestions(RemoteRuleMatch match) {
      StringBuilder sb = new StringBuilder();
      sb.append("(Q)uit (D)one (G)arbled (R)estartSentence ");
      if (match == null) {
        sb.append("NO MATCHES");
        return sb.toString();
      }
      sb.append("(I)gnoreMatch ");
      sb.append("(B)othOK ");
      sb.append("(F)P ");
      if (match.getReplacements().get().size() > 0) {
        sb.append("\nSUGGESTIONS: ");
      }
      int i = 1;
      for (String suggestion : match.getReplacements().get()) {
        sb.append(String.valueOf(i) + ") " + suggestion + " ");
        i++;
        if (i > 5) {
          break;
        }
      }
      return sb.toString();
    }
  
    static private String formatedSentence2(String line, PseudoMatch match) {
      if (match == null) {
        return line;
      }
      StringBuilder sb = new StringBuilder();
      sb.append(line.substring(0, match.getFromPos()));
      sb.append("___");
      sb.append(line.substring(match.getFromPos(), match.getToPos()));
      sb.append("___");
      sb.append(line.substring(match.getToPos()));
      return sb.toString();
    }
  
    static private String formattedCorrectedSentence2(String line, PseudoMatch match) {
      if (match == null) {
        return line;
      }
      StringBuilder sb = new StringBuilder();
      sb.append(line.substring(0, match.getFromPos()));
      sb.append("___");
      sb.append(match.getReplacements().get(0));
      sb.append("___");
      sb.append(line.substring(match.getToPos()));
      return sb.toString();
    }
  
    static private String formatedSentence(String line, RemoteRuleMatch match) {
      if (match == null) {
        return line;
      }
      StringBuilder sb = new StringBuilder();
      sb.append(line.substring(0, match.getErrorOffset()));
      sb.append("___");
      sb.append(line.substring(match.getErrorOffset(), match.getErrorOffset() + match.getErrorLength()));
      sb.append("___");
      sb.append(line.substring(match.getErrorOffset() + match.getErrorLength()));
      return sb.toString();
    }
  
    static private String applyAllMatches(String line, List<RemoteRuleMatch> matches) {
      if (matches == null) {
        return line;
      }
      int correctedPos = 0;
      String sentence = line;
  
      for (RemoteRuleMatch match : matches) {
        StringBuilder sb = new StringBuilder();
        sb.append(sentence.substring(0, match.getErrorOffset() + correctedPos));
        sb.append(match.getReplacements().get().get(0));
        sb.append(sentence.substring(match.getErrorOffset() + match.getErrorLength() + correctedPos));
        sentence = sb.toString();
        correctedPos += match.getReplacements().get().get(0).length() - match.getErrorLength();
      }
      return sentence;
    }
  
    static private String formattedCorrectedSentence(String line, RemoteRuleMatch match, int i) {
      if (match == null) {
        return line;
      }
      StringBuilder sb = new StringBuilder();
      sb.append(line.substring(0, match.getErrorOffset()));
      sb.append("___");
      sb.append(match.getReplacements().get().get(i - 1));
      sb.append("___");
      sb.append(line.substring(match.getErrorOffset() + match.getErrorLength()));
      return sb.toString();
    }
  
    static private String replaceSuggestion(String line, RemoteRuleMatch match, int i) {
      StringBuilder sb = new StringBuilder();
      sb.append(line.substring(0, match.getErrorOffset()));
      sb.append(match.getReplacements().get().get(i - 1));
      sb.append(line.substring(match.getErrorOffset() + match.getErrorLength()));
      return sb.toString();
    }
  
    private static List<RemoteRuleMatch> getMatches(AnnotatorConfig cfg, String sentence) {
      List<RemoteRuleMatch> matches;
      if (cachedMatches.containsKey(sentence)) {
        matches = cachedMatches.get(sentence);
      } else {
        try {
          matches = cfg.lt.check(sentence, cfg.ltConfig, cfg.customParams).getMatches();
        } catch (RuntimeException e) {
          e.printStackTrace();
          wait(1000);
          matches = cfg.lt.check(sentence, cfg.ltConfig, cfg.customParams).getMatches();
        }
        cachedMatches.put(sentence, matches);
      }
      return matches;
    }
  
    static private class AnnotatorConfig {
      String remoteServer;
      String userName;
      String apiKey;
      String inputFilePath;
      String outputFilePath;
      String languageCode;
      File inputFile;
      File outputFile;
      boolean automaticAnnotation;
      CheckConfiguration ltConfig;
      RemoteLanguageTool lt;
      Map<String, String> customParams = new HashMap<>();
      FileWriter out;
      StringBuilder outStrB;
      String ansiDefault = "";
      String ansiHighlight = "";
      List<String> enabledOnlyRules = new ArrayList<String>();
      List<String> disabledRules = new ArrayList<String>();
  
      void prepareConfiguration() throws IOException {
        CheckConfigurationBuilder cfgBuilder = new CheckConfigurationBuilder(languageCode);
        // cfgBuilder.textSessionID("-2");
        if (enabledOnlyRules.isEmpty()) {
          cfgBuilder.disabledRuleIds("WHITESPACE_RULE");
          if (!disabledRules.isEmpty()) {
            cfgBuilder.disabledRuleIds(disabledRules);
          }
        } else {
          cfgBuilder.enabledRuleIds(enabledOnlyRules).enabledOnly();
        }
        if (!userName.isEmpty() && !apiKey.isEmpty()) {
          cfgBuilder.username(userName).apiKey(apiKey).build();
        }
        ltConfig = cfgBuilder.build();
        inputFile = new File(inputFilePath);
        if (!inputFile.exists() || inputFile.isDirectory()) {
          throw new IOException("File not found: " + inputFile);
        }
        String fileName = inputFile.getName();
        // System.out.println("Analyzing file: " + fileName);
        fileName = fileName.substring(0, fileName.lastIndexOf('.'));
        if (outputFilePath.isEmpty()) {
          outputFile = new File(inputFile.getParentFile() + "/" + fileName + "-annotations.tsv");
        } else {
          outputFile = new File(outputFilePath);
        }
        outStrB = new StringBuilder();
        out = new FileWriter(outputFile, true);
        cachedMatches = new HashMap<>();
        lt = new RemoteLanguageTool(Tools.getUrl(remoteServer));
      }
  
    }
  
    private static String printTimeFromStart(long start, String tag) {
      if (tag.isEmpty()) {
        tag = "Time:";
      }
      long totalSecs = (long) ((System.currentTimeMillis() - start) / 1000.0);
      long hours = totalSecs / 3600;
      int minutes = (int) ((totalSecs % 3600) / 60);
      int seconds = (int) (totalSecs % 60);
      return String.format(tag + " %02d:%02d:%02d\n", hours, minutes, seconds);
    }
  
    private static void writeHelp() {
      System.out.println("Usage: " + SentenceAnnotator.class.getSimpleName() + " <configuration file>");
    }
  
    public static void wait(int ms) {
      try {
        Thread.sleep(ms);
      } catch (InterruptedException ex) {
        Thread.currentThread().interrupt();
      }
    }
  
  }
  