| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.netbeans.modules.docker; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| import org.netbeans.api.annotations.common.CheckForNull; |
| import org.openide.util.Pair; |
| |
| /** |
| * |
| * @author Petr Hejl |
| */ |
| public class IgnorePattern { |
| |
| private final List<Rule> rules; |
| |
| private final boolean negative; |
| |
| private IgnorePattern(List<Rule> rules, boolean negative) { |
| this.rules = rules; |
| this.negative = negative; |
| } |
| |
| @CheckForNull |
| public static IgnorePattern compile(String pattern, Character separator, boolean exclusionSupported) { |
| String trimmed = pattern.trim(); |
| boolean negative = false; |
| if (exclusionSupported && trimmed.startsWith("!")) { |
| negative = true; |
| trimmed = trimmed.substring(1).trim(); |
| } |
| char sep = separator != null ? separator : File.separatorChar; |
| String preprocessed = preprocess(trimmed, sep); |
| // remove the leading / or \ we will match the relative paths |
| if (preprocessed.startsWith(Character.toString(sep))) { |
| preprocessed = preprocessed.substring(1); |
| } |
| return compilePattern(preprocessed, sep, negative); |
| } |
| |
| static String preprocess(String pattern, char separator) { |
| String sep = Character.toString(separator); |
| String trimmed = pattern.trim(); |
| String volume = getVolume(trimmed, separator); |
| String path = trimmed.substring(volume.length()); |
| String ret = path.replaceAll("(" + Pattern.quote(sep) + "){2,}", Matcher.quoteReplacement(sep)) |
| .replaceAll("(" + Pattern.quote(sep) + "\\.)+(" + Pattern.quote(sep) + "|$)", Matcher.quoteReplacement(sep)); |
| if (ret.endsWith(sep) && ret.length() > 1) { |
| ret = ret.substring(0, ret.length() - sep.length()); |
| } |
| String[] parts = ret.split(Pattern.quote(sep)); |
| if (parts.length > 1) { |
| boolean root = false; |
| StringBuilder removed = new StringBuilder(); |
| int count = 0; |
| for (int i = parts.length - 1; i >= 0; i--) { |
| if (parts[i].isEmpty()) { |
| root = true; |
| break; |
| } |
| if (parts[i].equals("..")) { |
| count++; |
| } else { |
| if (count == 0) { |
| if (removed.length() > 0) { |
| removed.insert(0, sep); |
| } |
| removed.insert(0, parts[i]); |
| } else { |
| count--; |
| } |
| } |
| } |
| |
| for (int i = 0; i < count; i++) { |
| if (removed.length() > 0) { |
| removed.insert(0, sep); |
| } |
| removed.insert(0, ".."); |
| } |
| if (root) { |
| removed.insert(0, sep); |
| } |
| ret = removed.toString(); |
| } |
| |
| ret = ret.replaceAll("^(" + Pattern.quote(sep) + "\\.\\.)+(" + Pattern.quote(sep) +")?", Matcher.quoteReplacement(sep)) |
| .replaceAll("/", Matcher.quoteReplacement(sep)); |
| if (ret.isEmpty()) { |
| ret = "."; |
| } |
| return volume + ret; |
| } |
| |
| static IgnorePattern compilePattern(String pattern, char separator, boolean negative) { |
| String trimmed = pattern.trim(); |
| List<Rule> ret = new ArrayList<>(); |
| char[] patternChars = trimmed.toCharArray(); |
| List<Character> buffer = new ArrayList<>(); |
| for (int i = 0; i < patternChars.length; i++) { |
| char c = patternChars[i]; |
| switch (c) { |
| case '*': |
| addCharacterListRule(ret, buffer); |
| if (ret.isEmpty() || !(ret.get(ret.size() - 1) instanceof StarRule)) { |
| ret.add(new StarRule(separator)); |
| } |
| break; |
| case '?': |
| addCharacterListRule(ret, buffer); |
| ret.add(new QuestionRule(separator)); |
| break; |
| case '[': |
| addCharacterListRule(ret, buffer); |
| Pair<? extends Rule, Integer> p = createRange(patternChars, i, separator); |
| ret.add(p.first()); |
| if (p.second() < 0) { |
| return new IgnorePattern(ret, negative); |
| } |
| i = p.second(); |
| break; |
| case '\\': |
| if (separator == '\\') { |
| buffer.add(patternChars[i]); |
| } else { |
| if (i < patternChars.length - 1) { |
| buffer.add(patternChars[++i]); |
| } else { |
| addCharacterListRule(ret, buffer); |
| ret.add(new ErrorRule(trimmed, i)); |
| return new IgnorePattern(ret, negative); |
| } |
| } |
| break; |
| default: |
| buffer.add(patternChars[i]); |
| break; |
| } |
| } |
| addCharacterListRule(ret, buffer); |
| return new IgnorePattern(ret, negative); |
| } |
| |
| private static void addCharacterListRule(List<Rule> rules, List<Character> buffer) { |
| if (!buffer.isEmpty()) { |
| rules.add(new CharacterListRule(buffer)); |
| buffer.clear(); |
| } |
| } |
| |
| public boolean matches(String input) throws PatternSyntaxException { |
| return matches(rules, input); |
| } |
| |
| public boolean isNegative() { |
| return negative; |
| } |
| |
| boolean isError() { |
| for (Rule r : rules) { |
| if (r instanceof ErrorRule) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean matches(List<Rule> rules, String input) throws PatternSyntaxException { |
| char[] inputChars = input.toCharArray(); |
| int i = 0; |
| int listIndex = 0; |
| for (Iterator<Rule> it = rules.iterator(); it.hasNext();) { |
| Rule r = it.next(); |
| //try { |
| if (inputChars.length == 0) { |
| // star matches even empty string |
| return rules.size() == 1 && r.matchesEmpty(); |
| } |
| int[] test = r.consume(inputChars, i); |
| if (test == null) { |
| return false; |
| } |
| |
| if (test.length == 1) { |
| i = test[0]; |
| } else if (listIndex == rules.size() - 1 |
| && test[test.length - 1] >= input.length()) { |
| // last rule - take the longest one |
| i = test[test.length - 1]; |
| } else { |
| for (int j = test.length - 1; j >= 0; j--) { |
| if (matches(rules.subList(listIndex + 1, rules.size()), input.substring(test[j]))) { |
| return true; |
| } |
| } |
| return false; |
| } |
| // } catch (PatternSyntaxException ex) { |
| // return false; |
| // } |
| listIndex++; |
| if (i >= inputChars.length) { |
| if (!it.hasNext()) { |
| return true; |
| } else { |
| return it.next().matchesEmpty(); |
| } |
| } |
| } |
| return i >= inputChars.length; |
| } |
| |
| private static Pair<? extends Rule, Integer> createRange(char[] chars, int offset, char separator) { |
| if (chars[offset] != '[' || offset >= chars.length - 1) { |
| return Pair.of(new ErrorRule(new String(chars), offset), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), offset); |
| } |
| |
| boolean negated = false; |
| int start = offset + 1; |
| char first = chars[offset + 1]; |
| if (first == '^') { |
| negated = true; |
| start++; |
| } |
| |
| if (start >= chars.length - 1) { |
| return Pair.of(new ErrorRule(new String(chars), start), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), start); |
| } |
| |
| Character last = null; |
| LinkedList<Character> singles = new LinkedList<>(); |
| List<Pair<Character, Character>> ranges = new LinkedList<>(); |
| boolean inRange = false; |
| for (int i = start; i < chars.length; i++) { |
| char c = chars[i]; |
| switch (c) { |
| case '\\': |
| if (separator == '\\') { |
| char l = chars[i]; |
| if (inRange) { |
| ranges.add(Pair.of(last, l)); |
| inRange = false; |
| last = null; |
| } else { |
| last = l; |
| singles.add(l); |
| } |
| } else { |
| if (i < chars.length - 1) { |
| char l = chars[++i]; |
| // XXX is backslash allowed in range ? |
| if (inRange) { |
| ranges.add(Pair.of(last, l)); |
| inRange = false; |
| last = null; |
| } else { |
| last = l; |
| singles.add(l); |
| } |
| } else { |
| return Pair.of(new ErrorRule(new String(chars), i), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), i); |
| } |
| } |
| break; |
| case ']': |
| if (inRange || i == start) { |
| return Pair.of(new ErrorRule(new String(chars), i), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), i); |
| } |
| return Pair.of(new RangeRule(negated, ranges, singles), i); |
| case '-': |
| if (last == null) { |
| return Pair.of(new ErrorRule(new String(chars), i), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), i); |
| } |
| singles.removeLast(); |
| inRange = true; |
| break; |
| default: |
| char l = chars[i]; |
| if (inRange) { |
| ranges.add(Pair.of(last, l)); |
| inRange = false; |
| last = null; |
| } else { |
| last = l; |
| singles.add(l); |
| } |
| break; |
| } |
| } |
| return Pair.of(new ErrorRule(new String(chars), chars.length - 1), -1); |
| //throw new PatternSyntaxException("Malformed range", new String(chars), chars.length - 1); |
| } |
| |
| private static String getVolume(String path, char separator) { |
| if (separator != '\\') { |
| return ""; |
| } |
| if (path.length() < 2) { |
| return ""; |
| } |
| char drive = path.charAt(0); |
| if (path.charAt(1) == ':' && ('a' <= drive && drive <= 'z' || 'A' <= drive && drive <= 'Z')) { // NOI18N |
| return path.substring(0, 2); |
| } |
| // FIXME UNC |
| |
| return ""; |
| } |
| |
| private static interface Rule { |
| |
| int[] consume(char[] chars, int offset); |
| |
| boolean matchesEmpty(); |
| |
| } |
| |
| private static class StarRule implements Rule { |
| |
| private final char separator; |
| |
| public StarRule(char separator) { |
| this.separator = separator; |
| } |
| |
| @Override |
| public int[] consume(char[] chars, int offset) { |
| if (offset >= chars.length) { |
| throw new IllegalArgumentException(); |
| } |
| |
| int limit = -1; |
| for (int i = offset; i < chars.length; i++) { |
| if (chars[i] == separator) { |
| limit = i; |
| break; |
| } |
| } |
| if (limit < 0) { |
| limit = chars.length; |
| } |
| int[] ret = new int[limit - offset + 1]; |
| for (int i = 0; i < ret.length; i++) { |
| ret[i] = offset + i; |
| } |
| return ret; |
| } |
| |
| @Override |
| public boolean matchesEmpty() { |
| return true; |
| } |
| } |
| |
| private static class QuestionRule implements Rule { |
| |
| private final char separator; |
| |
| public QuestionRule(char separator) { |
| this.separator = separator; |
| } |
| |
| @Override |
| public int[] consume(char[] chars, int offset) { |
| if (offset >= chars.length) { |
| throw new IllegalArgumentException(); |
| } |
| if (chars[offset] == separator) { |
| return null; |
| } |
| return new int[]{offset + 1}; |
| } |
| |
| @Override |
| public boolean matchesEmpty() { |
| return false; |
| } |
| } |
| |
| private static class RangeRule implements Rule { |
| |
| private final boolean negated; |
| |
| private final List<Pair<Character, Character>> ranges; |
| |
| private final List<Character> singles; |
| |
| public RangeRule(boolean negated, List<Pair<Character, Character>> ranges, List<Character> singles) { |
| this.negated = negated; |
| this.ranges = ranges; |
| this.singles = singles; |
| } |
| |
| @Override |
| public int[] consume(char[] chars, int offset) { |
| if (offset >= chars.length) { |
| throw new IllegalArgumentException(); |
| } |
| boolean ok = check(chars[offset]); |
| if (negated) { |
| ok = !ok; |
| } |
| if (!ok) { |
| return null; |
| } |
| return new int[]{offset + 1}; |
| } |
| |
| @Override |
| public boolean matchesEmpty() { |
| return false; |
| } |
| |
| private boolean check(char c) { |
| for (Character s : singles) { |
| if (s == c) { |
| return true; |
| } |
| } |
| for (Pair<Character, Character> r : ranges) { |
| if (r.first() <= c && c <= r.second()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| } |
| |
| private static class CharacterListRule implements Rule { |
| |
| private final List<Character> array; |
| |
| public CharacterListRule(List<Character> array) { |
| this.array = new ArrayList<>(array); |
| } |
| |
| @Override |
| public int[] consume(char[] chars, int offset) throws IllegalStateException { |
| if (offset >= chars.length) { |
| throw new IllegalArgumentException(); |
| } |
| |
| for (int i = 0; i < array.size(); i++) { |
| if (array.get(i) != chars[offset + i]) { |
| return null; |
| } |
| } |
| return new int[]{offset + array.size()}; |
| } |
| |
| @Override |
| public boolean matchesEmpty() { |
| return false; |
| } |
| } |
| |
| private static class ErrorRule implements Rule { |
| |
| private final String regex; |
| |
| private final int index; |
| |
| public ErrorRule(String regex, int index) { |
| this.regex = regex; |
| this.index = index; |
| } |
| |
| @Override |
| public int[] consume(char[] chars, int offset) { |
| throw new PatternSyntaxException("Malformed pattern", regex, index); |
| } |
| |
| @Override |
| public boolean matchesEmpty() { |
| return false; |
| } |
| } |
| } |