package com.github.marschall.memoryfilesystem; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static java.util.regex.Pattern.UNICODE_CASE; import java.nio.file.InvalidPathException; import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import com.github.marschall.memoryfilesystem.GlobPathMatcher.GlobMatch; abstract class PathParser { static final String[] EMPTY = new String[0]; final char separator; private final CharacterSet forbiddenCharacters; PathParser(String separator, CharacterSet forbiddenCharacters) { this.forbiddenCharacters = forbiddenCharacters; if (separator.length() != 1) { throw new IllegalArgumentException("separator must have length 1 but was \"" + separator + "\""); } this.separator = separator.charAt(0); } void check(char c) { if (this.forbiddenCharacters.contains(c)) { throw new InvalidPathException(Character.toString(c), "contains a not allowed character"); } } void check(List<String> elements) { for (String element : elements) { if (this.forbiddenCharacters.containsAny(element)) { throw new InvalidPathException(element, "contains a not allowed character"); } } } boolean startWithSeparator(String s) { char first = s.charAt(0); return first == '/' || first == this.separator; } abstract AbstractPath parse(Map<String, Root> roots, String first, String... more); abstract AbstractPath parseUri(Map<String, Root> rootByKey, String uri); boolean startWithSeparator(String first, String... more) { if (!first.isEmpty()) { return this.startWithSeparator(first); } if (more != null && more.length > 0) { for (String s : more) { if (!s.isEmpty()) { return this.startWithSeparator(s); } } } // only empty strings return false; } abstract PathMatcher parseGlob(String pattern); static List<GlobMatch> convertToMatches(List<String> elements) { List<GlobMatch> matches = new ArrayList<>(elements.size()); for (String element : elements) { matches.add(convertToMatch(element)); } return matches; } private static GlobMatch convertToMatch(String element) { if (element.equals("**")) { return FlexibleMatch.INSTANCE; } Stream stream = new Stream(element); StringBuilder buffer = new StringBuilder(); parseGeneric(stream, buffer, ExitHandler.EMPTY, element); // TODO Pattern#CANON_EQ ? Pattern pattern = Pattern.compile(buffer.toString(), CASE_INSENSITIVE | UNICODE_CASE); return new PatternMatch(pattern); } private static char parseGeneric(Stream stream, StringBuilder buffer, ExitHandler exitHandler, String element) { while (stream.hasNext()) { char next = stream.next(); if (exitHandler.isExit(next)) { return next; } switch (next) { case '*': buffer.append(".*"); break; case '?': buffer.append('.'); break; case '[': parseRange(stream, buffer, element); break; case '{': parseGroup(stream, buffer, element); break; case '\\': if (!stream.hasNext()) { throw new PatternSyntaxException("\\must be followed by content", element, element.length() - 1); } buffer.append('\\').append(stream.next()); break; default: appendSafe(next, buffer); break; } } return exitHandler.endOfStream(element); } private static void appendSafe(char c, StringBuilder buffer) { if (c == '^' || c == '$' || c == '.' ) { buffer.append('\\'); } buffer.append(c); } private static void parseGroup(Stream stream, StringBuilder buffer, String element) { List<String> groups = new ArrayList<>(4); StringBuilder groupBuffer = new StringBuilder(); while (parseGeneric(stream, groupBuffer, ExitHandler.GROUP, element) != '}') { groups.add(groupBuffer.toString()); groupBuffer = new StringBuilder(groupBuffer.length()); } groups.add(groupBuffer.toString()); boolean first = true; buffer.append('('); for (String group : groups) { if (!first) { buffer.append('|'); } else { first = false; } buffer.append('('); buffer.append(group); buffer.append(')'); } buffer.append(')'); } private static void parseRange(Stream stream, StringBuilder buffer, String element) { StringBuilder rangeBuffer = new StringBuilder(); parseGeneric(stream, rangeBuffer, ExitHandler.RANGE, element); buffer.append('['); // TODO escape, think about ignoring - buffer.append(rangeBuffer); buffer.append(']'); } enum ExitHandler { EMPTY { @Override boolean isExit(char c) { return false; } @Override char endOfStream(String element) { return 0; // doesn't matter, will be ignored } }, GROUP { @Override boolean isExit(char c) { return c == ',' || c == '}'; } @Override char endOfStream(String element) { throw new PatternSyntaxException("expected }", element, element.length() - 1); } }, RANGE { @Override boolean isExit(char c) { return c == ']'; } @Override char endOfStream(String element) { throw new PatternSyntaxException("expected ]", element, element.length() - 1); } }; abstract boolean isExit(char c); abstract char endOfStream(String element); } static final class Stream { private final String contents; private int position; Stream(String contents) { this.contents = contents; this.position = 0; } boolean hasNext() { return this.position < this.contents.length(); } char next() { char value = this.contents.charAt(this.position); this.position += 1; return value; } } enum FlexibleMatch implements GlobMatch { INSTANCE; @Override public boolean isFlexible() { return true; } @Override public boolean matches(String element) { return true; } @Override public String toString() { return "**"; } } static final class PatternMatch implements GlobMatch { private final Pattern pattern; PatternMatch(Pattern pattern) { this.pattern = pattern; } @Override public boolean isFlexible() { return false; } @Override public boolean matches(String element) { return this.pattern.matcher(element).matches(); } @Override public String toString() { return this.pattern.toString(); } } }