code: /* LanguageTool, a natural language style checker 
   * Copyright (C) 2015 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.de;
  
  import com.google.common.cache.CacheBuilder;
  import com.google.common.cache.CacheLoader;
  import com.google.common.cache.LoadingCache;
  import org.jetbrains.annotations.NotNull;
  import org.languagetool.synthesis.GermanSynthesizer;
  import org.languagetool.tools.StringTools;
  
  import java.io.IOException;
  import java.util.*;
  import java.util.concurrent.TimeUnit;
  
  /**
   * Expand lines according to their suffix, e.g. {@code foo/S} becomes {@code [foo, foos]}.
   * @since 3.0
   */
  public class LineExpander implements org.languagetool.rules.LineExpander {
  
    private static final LoadingCache<String, String[]> verbFormCache = CacheBuilder.newBuilder()
      .expireAfterAccess(10, TimeUnit.MINUTES)
      .build(new CacheLoader<String, String[]>() {
        @Override
        public String[] load(@NotNull String verb) throws IOException {
          return GermanSynthesizer.INSTANCE.synthesizeForPosTags(verb, s -> s.startsWith("VER:"));
        }
      });
  
    private static List<String> handleLineWithPrefix(String line) {
      List<String> result = new ArrayList<>();
      String[] parts = cleanTagsAndEscapeChars(line).split("_");
      if (parts.length != 2) {
        throw new IllegalArgumentException("Unexpected line format, expected at most one '_': " + line);
      }
      if (parts[0].contains("/") || parts[1].contains("/")) {
        throw new IllegalArgumentException("Unexpected line format, '_' cannot be combined with '/': " + line);
      }
      if (parts[1].equals("in")) {
        // special case for the common gender gap characters:
        result.add(parts[0] + "_in");
        result.add(parts[0] + "_innen");
        result.add(parts[0] + "*in");
        result.add(parts[0] + "*innen");
        result.add(parts[0] + ":in");
        result.add(parts[0] + ":innen");
        //result.add(parts[0] + "in");   // see if we can comment in these cases, too
        //result.add(parts[0] + "innen");
        //result.add(parts[0] + "e");
        //result.add(parts[0] + "en");
      } else {
        String[] forms = verbFormCache.getUnchecked(parts[1]);
        if (forms.length == 0) {
          throw new RuntimeException("Could not expand '" + parts[1] + "' from line '" + line + "', no forms found");
        }
        Set<String> formSet = new HashSet<>(Arrays.asList(forms));
        for (String form : formSet) {
          if (!form.contains("ß") && form.length() > 0 && Character.isLowerCase(form.charAt(0))) {
            // skip these, it's too risky to introduce old spellings like "gewußt" from the synthesizer
            result.add(parts[0] + form);
          }
        }
        result.add(parts[0] + "zu" + parts[1]);  //  "zu<verb>" is not part of forms from synthesizer
        result.add(StringTools.uppercaseFirstChar(parts[0]) + parts[1] + "s");  //  Genitiv, e.g. "des Weitergehens"
      }
      return result;
    }
  
    @Override
    public List<String> expandLine(String line) {
      return isLineWithVerbPrefix(line) ? handleLineWithPrefix(line) :
             isLineWithFlag(line) ? handleLineWithFlags(line) :
             Collections.singletonList(cleanTagsAndEscapeChars(line));
    }
  
    private boolean isLineWithFlag(String line) {
      int idx = line.indexOf('/');
      return !line.startsWith("#") && idx > 0 && line.charAt(idx-1) != '\\';
    }
  
    private boolean isLineWithVerbPrefix(String line) {
      int idx = line.indexOf('_');
      return !line.startsWith("#") && idx > 0 && line.charAt(idx-1) != '\\';
    }
  
    private List<String> handleLineWithFlags(String line) {
      List<String> result = new ArrayList<>();
      String[] parts = cleanTagsAndEscapeChars(line).split("/");
      if (parts.length != 2) {
        throw new IllegalArgumentException("Unexpected line format, expected at most one slash: " + line);
      }
      String word = parts[0];
      String suffix = parts[1];
      for (int i = 0; i < suffix.length(); i++) {
        char c = suffix.charAt(i);
        if (c == 'S') {
          add(result, word);
          result.add(word + "s");
        } else if (c == 'N') {
          add(result, word);
          result.add(word + "n");
        } else if (c == 'E') {
          add(result, word);
          result.add(word + "e");
        } else if (c == 'F') {
          add(result, word);
          result.add(word + "in"); // (m/f)
        } else if (c == 'A' || c == 'P') { // Adjektiv / Partizip
          add(result, word);
          if (word.endsWith("e")) {
            result.add(word + "r");
            result.add(word + "s");
            result.add(word + "n");
            result.add(word + "m");
          } else {
            result.add(word + "e");
            result.add(word + "er");
            result.add(word + "es");
            result.add(word + "en");
            result.add(word + "em");
          }
        } else {
          throw new IllegalArgumentException("Unknown suffix: " + suffix + " in line: " + line);
        }
      }
      return result;
    }
  
    private void add(List<String> result, String word) {
      if (!result.contains(word)) {
        result.add(word);
      }
    }
  
    // ignore "#..." so it can be used as a tag:
    private static String cleanTagsAndEscapeChars(String s) {
      int idx = s.indexOf('#');
      if (idx != -1) {
        s = s.substring(0, idx);
      }
      return s.replaceAll("\\\\", "").trim();
    }
  }
  