001package org.editorconfig.core;
002
003import java.io.*;
004import java.util.*;
005import java.util.regex.Matcher;
006import java.util.regex.Pattern;
007
008/**
009 * EditorConfig handler
010 *
011 * @author Dennis.Ushakov
012 */
013public class EditorConfig {
014  public static String VERSION = "0.11.3-final";
015
016  private static final Pattern SECTION_PATTERN = Pattern.compile("\\s*\\[(([^#;]|\\\\#|\\\\;)+)]" +
017                                                                 ".*"); // Python match searches from the line start
018  private static final int HEADER = 1;
019
020  private static final Pattern OPTION_PATTERN = Pattern.compile("\\s*([^:=\\s][^:=]*)\\s*[:=]\\s*(.*)");
021  private static final int OPTION = 1;
022  private static final int VAL = 2;
023
024  private final String configFilename;
025  private final String version;
026
027  /**
028   * Creates EditorConfig handler with default configuration filename (.editorconfig) and
029   * version {@link EditorConfig#VERSION}
030   */
031  public EditorConfig() {
032    this(".editorconfig", VERSION);
033  }
034
035  /**
036   * Creates EditorConfig handler with specified configuration filename and version.
037   * Used mostly for debugging/testing.
038   * @param configFilename configuration file name to be searched for instead of .editorconfig
039   * @param version required version
040   */
041  public EditorConfig(String configFilename, String version) {
042    this.configFilename = configFilename;
043    this.version = version;
044  }
045
046  /**
047   * Parse editorconfig files corresponding to the file path given by filename, and return the parsing result.
048   *
049   * @param filePath The full path to be parsed. The path is usually the path of the file which is currently edited
050   *                 by the editor.
051   * @return The parsing result stored in a list of {@link EditorConfig.OutPair}.
052   * @throws org.editorconfig.core.ParsingException      If an {@code .editorconfig} file could not be parsed
053   * @throws org.editorconfig.core.VersionException      If version greater than actual is specified in constructor
054   * @throws org.editorconfig.core.EditorConfigException If an EditorConfig exception occurs. Usually one of
055   *                                                     {@link ParsingException} or {@link VersionException}
056   */
057  public List<OutPair> getProperties(String filePath) throws EditorConfigException {
058    checkAssertions();
059    Map<String, String> oldOptions = Collections.emptyMap();
060    Map<String, String> options = new LinkedHashMap<String, String>();
061    try {
062      boolean root = false;
063      String dir = new File(filePath).getParent();
064      while (dir != null && !root) {
065        String configPath = dir + "/" + configFilename;
066        if (new File(configPath).exists()) {
067          FileInputStream stream = new FileInputStream(configPath);
068          InputStreamReader reader = new InputStreamReader(stream, "UTF-8");
069          BufferedReader bufferedReader = new BufferedReader(reader);
070          try {
071            root = parseFile(bufferedReader, dir + "/", filePath, options);
072          } finally {
073            bufferedReader.close();
074            reader.close();
075            stream.close();
076          }
077        }
078        options.putAll(oldOptions);
079        oldOptions = options;
080        options = new LinkedHashMap<String, String>();
081        dir = new File(dir).getParent();
082      }
083    } catch (IOException e) {
084      throw new EditorConfigException(null, e);
085    }
086
087    preprocessOptions(oldOptions);
088
089    final List<OutPair> result = new ArrayList<OutPair>();
090    for (Map.Entry<String, String> keyValue : oldOptions.entrySet()) {
091      result.add(new OutPair(keyValue.getKey(), keyValue.getValue()));
092    }
093    return result;
094  }
095
096  private void checkAssertions() throws VersionException {
097    if (compareVersions(version, VERSION) > 0) {
098      throw new VersionException("Required version is greater than the current version.");
099    }
100  }
101
102  private static int compareVersions(String version1, String version2) {
103    String[] version1Components = version1.split("(\\.|-)");
104    String[] version2Components = version2.split("(\\.|-)");
105    for (int i = 0; i < 3; i++) {
106      String version1Component = version1Components[i];
107      String version2Component = version2Components[i];
108      int v1 = -1;
109      int v2 = -1;
110      try {
111        v1 = Integer.parseInt(version1Component);
112      } catch (NumberFormatException ignored) {}
113      try {
114        v2 = Integer.parseInt(version2Component);
115      } catch (NumberFormatException ignored) {}
116      if (v1 != v2) return v1 - v2;
117    }
118    return 0;
119  }
120
121  private void preprocessOptions(Map<String, String> options) {
122    // Lowercase option value for certain options
123    for (String key : new String[]{"end_of_line", "indent_style", "indent_size", "insert_final_newline",
124                                   "trim_trailing_whitespace", "charset"}) {
125      String value = options.get(key);
126      if (value != null) {
127        options.put(key, value.toLowerCase(Locale.US));
128      }
129    }
130
131    // Set indent_size to "tab" if indent_size is unspecified and
132    // indent_style is set to "tab".
133    if ("tab".equals(options.get("indent_style")) && !options.containsKey("indent_size") &&
134        compareVersions(version, "0.10.0") >= 0) {
135      options.put("indent_size", "tab");
136    }
137
138    // Set tab_width to indent_size if indent_size is specified and
139    // tab_width is unspecified
140    String indent_size = options.get("indent_size");
141    if (indent_size != null && !"tab".equals(indent_size) && !options.containsKey("tab_width")) {
142      options.put("tab_width", indent_size);
143    }
144
145    // Set indent_size to tab_width if indent_size is "tab"
146    String tab_width = options.get("tab_width");
147    if ("tab".equals(indent_size) && tab_width != null) {
148      options.put("indent_size", tab_width);
149    }
150  }
151
152  private static boolean parseFile(BufferedReader bufferedReader, String dirName, String filePath, Map<String, String> result) throws IOException, EditorConfigException {
153    final StringBuilder malformedLines = new StringBuilder();
154    boolean root = false;
155    boolean inSection = false;
156    boolean matchingSection = false;
157    while (bufferedReader.ready()) {
158      String line = bufferedReader.readLine().trim();
159
160      if (line.startsWith("\ufeff")) {
161        line = line.substring(1);
162      }
163
164      // comment or blank line?
165      if (line.isEmpty() || line.startsWith("#") || line.startsWith(";")) continue;
166
167      Matcher matcher = SECTION_PATTERN.matcher(line);
168      if (matcher.matches()) {
169        inSection = true;
170        matchingSection = filenameMatches(dirName, matcher.group(HEADER), filePath);
171        continue;
172      }
173      matcher = OPTION_PATTERN.matcher(line);
174      if (matcher.matches()) {
175        String key = matcher.group(OPTION).trim().toLowerCase(Locale.US);
176        String value = matcher.group(VAL);
177        value = value.equals("\"\"") ? "" : value;
178        if (!inSection && "root".equals(key)) {
179          root = true;
180        } else if (matchingSection) {
181          int commentPos = value.indexOf(" ;");
182          commentPos = commentPos < 0 ? value.indexOf(" #") : commentPos;
183          value = commentPos >= 0 ? value.substring(0, commentPos) : value;
184          result.put(key, value);
185        }
186        continue;
187      }
188      malformedLines.append(line).append("\n");
189    }
190    if (malformedLines.length() > 0) {
191      throw new ParsingException(malformedLines.toString(), null);
192    }
193    return root;
194  }
195
196  private static boolean filenameMatches(String configDirname, String pattern, String filePath) {
197    pattern = pattern.replace(File.separatorChar, '/');
198    pattern = pattern.replaceAll("\\\\#", "#");
199    pattern = pattern.replaceAll("\\\\;", ";");
200    int separator = pattern.indexOf("/");
201    if (separator >= 0) {
202      pattern = configDirname + (separator == 0 ? pattern.substring(1) : pattern);
203    } else {
204      pattern = "**/" + pattern;
205    }
206    return Pattern.compile(convertGlobToRegEx(pattern)).matcher(filePath).matches();
207  }
208
209  private static String convertGlobToRegEx(String pattern) {
210    int length = pattern.length();
211    StringBuilder result = new StringBuilder(length);
212    int i = 0;
213    boolean escaped = false;
214    while (i < length) {
215      char current = pattern.charAt(i);
216      i++;
217      if ('*' == current) {
218        if (i < length && pattern.charAt(i) == '*') {
219          result.append(".*");
220          i++;
221        } else {
222          result.append("[^/]*");
223        }
224      } else if ('?' == current) {
225        result.append(".");
226      } else if ('[' == current) {
227        int j = i;
228        if (j < length && pattern.charAt(j) == '!') {
229          j++;
230        }
231        if (j < length && pattern.charAt(j) == ']') {
232          j++;
233        }
234        while (j < length && (pattern.charAt(j) != ']' || escaped)) {
235          escaped = pattern.charAt(j) == '\\' && !escaped;
236          j++;
237        }
238        if (j >= length) {
239          result.append("\\[");
240        } else {
241          String charClass = pattern.substring(i, j);
242          i = j + 1;
243          if (charClass.charAt(0) == '!') {
244            charClass = '^' + charClass;
245          } else if (charClass.charAt(0) == '^') {
246            charClass = "\\" + charClass;
247          }
248          result.append('[').append(charClass).append("]");
249        }
250      } else if ('{' == current) {
251        int j = i;
252        final List<String> groups = new ArrayList<String>();
253        while (j < length && pattern.charAt(j) != '}') {
254          int k = j;
255          while (k < length && (",}".indexOf(pattern.charAt(k)) < 0 || escaped)) {
256            escaped = pattern.charAt(k) == '\\' && !escaped;
257            k++;
258          }
259          String group = pattern.substring(j, k);
260          for (char c : new char[] {',', '}', '\\'}){
261            group = group.replace("\\" + c, String.valueOf(c));
262          }
263          groups.add(group);
264          j = k;
265          if (j < length && pattern.charAt(j) == ',') {
266            j++;
267            if (j < length && pattern.charAt(j) == '}') {
268              groups.add("");
269            }
270          }
271        }
272        if (j >= length || groups.size() < 2) {
273          result.append("\\{");
274        } else {
275          result.append("(");
276          for (int groupNumber = 0; groupNumber < groups.size(); groupNumber++) {
277            String group = groups.get(groupNumber);
278            if (groupNumber > 0) result.append("|");
279            result.append(escapeToRegex(group));
280          }
281          result.append(")");
282          i = j + 1;
283        }
284      } else {
285        result.append(escapeToRegex(String.valueOf(current)));
286      }
287    }
288
289    return result.toString();
290  }
291
292  private static String escapeToRegex(String group) {
293    final StringBuilder builder = new StringBuilder(group.length());
294    for (char c : group.toCharArray()) {
295      if (c == ' ' || Character.isLetter(c) || Character.isDigit(c) || c == '_') {
296        builder.append(c);
297      } else if (c == '\n') {
298        builder.append("\\n");
299      } else {
300        builder.append("\\").append(c);
301      }
302    }
303    return builder.toString();
304  }
305
306  /**
307   * String-String pair to store the parsing result.
308   */
309  public static class OutPair {
310    private final String key;
311    private final String val;
312
313    public OutPair(String key, String val) {
314      this.key = key;
315      this.val = val;
316    }
317
318    public String getKey(){
319      return key;
320    }
321
322    public String getVal() {
323      return val;
324    }
325  }
326}