package org.basex.http.restxq;
import static java.math.BigInteger.*;
import static org.basex.http.restxq.RestXqText.*;
import java.math.*;
import java.util.*;
import java.util.regex.*;
import org.basex.http.*;
import org.basex.query.*;
import org.basex.query.value.item.*;
import org.basex.util.*;
/**
* RESTXQ path template.
*
* @author BaseX Team 2005-17, BSD License
* @author Dimitar Popov
*/
final class RestXqPathMatcher {
/** Default matcher for empty path templates. */
private static final RestXqPathMatcher EMPTY =
new RestXqPathMatcher("/", Collections.<QNm>emptyList(), 0, ZERO);
/** Variable names defined in the path template. */
final List<QNm> vars;
/** Compiled regular expression which matches paths defined by the path annotation. */
final Pattern pattern;
/** Number of path segments. */
final int segments;
/** Bit array with variable positions within the path template. */
final BigInteger varsPos;
/**
* Constructor.
* @param regex regular expression which matches paths defined by the path annotation
* @param vars variable names defined in the path template
* @param segments segment count
* @param varsPos variable position
*/
private RestXqPathMatcher(final String regex, final List<QNm> vars, final int segments,
final BigInteger varsPos) {
this.vars = vars;
this.segments = segments;
this.varsPos = varsPos;
pattern = Pattern.compile(regex);
}
/**
* Checks if the given path matches.
* @param path path to match
* @return result of check
*/
boolean matches(final String path) {
return matcher(path).matches();
}
/**
* Gets variable values for the given path.
* @param path from which to read the values
* @return map with variable values
*/
Map<QNm, String> values(final String path) {
final Map<QNm, String> result = new HashMap<>();
final Matcher m = matcher(path);
if(m.matches()) {
final int groupCount = m.groupCount();
if(vars.size() <= groupCount) {
int group = 1;
for(final QNm var : vars) {
result.put(var, m.group(group));
// skip nested groups
final int end = m.end(group);
while(++group <= groupCount && m.start(group) < end);
}
}
}
return result;
}
/**
* Creates a pattern matcher for the given string.
* @param input input string
* @return pattern matcher
*/
private Matcher matcher(final String input) {
return pattern.matcher(input);
}
/**
* Parses a path template.
* @param path path template string to be parsed
* @param ii input info
* @return parsed path template
* @throws QueryException if given template is invalid
*/
static RestXqPathMatcher parse(final String path, final InputInfo ii) throws QueryException {
if(path.isEmpty()) return EMPTY;
final ArrayList<QNm> vars = new ArrayList<>();
final StringBuilder result = new StringBuilder();
final StringBuilder literals = new StringBuilder();
final TokenBuilder variable = new TokenBuilder();
final StringBuilder regex = new StringBuilder();
final BitSet varsPos = new BitSet();
int segment = 0;
final CharIterator i = new CharIterator(path);
if(path.charAt(0) == '/') i.next();
literals.append('/');
while(i.hasNext()) {
char ch = i.next();
if(ch == '{') {
decodeAndEscape(literals, result);
// variable
if(!i.hasNext() || i.nextNonWS() != '$') throw error(ii, INV_TEMPLATE, path);
// default variable regular expression
regex.append("[^/]+?");
int braces = 1;
while(i.hasNext()) {
ch = i.nextNonWS();
if(ch == '=') {
regex.setLength(0);
addRegex(i, regex);
if(regex.length() == 0) throw error(ii, INV_TEMPLATE, path);
break;
} else if(ch == '{') {
++braces;
variable.add(ch);
} else if(ch == '}' && --braces == 0) {
break;
} else {
variable.add(ch);
}
}
final byte[] var = variable.toArray();
if(!XMLToken.isQName(var)) throw error(ii, INV_VARNAME, variable);
vars.add(new QNm(var));
variable.reset();
varsPos.set(segment);
result.append('(').append(regex).append(')');
regex.setLength(0);
} else {
if(ch == '/') ++segment;
literals.append(ch);
}
}
decodeAndEscape(literals, result);
final BigInteger vp = varsPos.cardinality() == 0 ? ZERO : new BigInteger(varsPos.toByteArray());
return new RestXqPathMatcher(result.toString(), vars, segment + 1, vp);
}
/**
* Parses a regular expression defined for a template variable.
* @param i character iterator positioned before the first character of the regex.
* @param result string builder where the parsed regular expression will be appended to.
*/
private static void addRegex(final CharIterator i, final StringBuilder result) {
int braces = 1;
while(i.hasNext()) {
final char ch = i.nextNonWS();
if(ch == '{') ++braces;
else if(ch == '}' && --braces == 0) break;
result.append(ch);
}
}
/**
* Decodes the URL and escapes regex characters in path template literals.
* @param literals literals to escape
* @param result string builder where the escaped literals will be appended to.
*/
private static void decodeAndEscape(final StringBuilder literals, final StringBuilder result) {
if(literals.length() > 0) {
final String decoded = HTTPConnection.decode(literals.toString());
final int n = decoded.length();
for(int i = 0; i < n; ++i) {
final char c = decoded.charAt(i);
if(isRegexChar(c)) result.append('\\');
result.append(c);
}
literals.setLength(0);
}
}
/**
* Checks if a character is a regex character.
* @param c character to check.
* @return result of check
*/
private static boolean isRegexChar(final char c) {
return ".^&!?-:<>()[]{}$=,*+|".indexOf(c) >= 0;
}
/**
* Creates a query exception.
* @param ii input info
* @param msg exception message
* @param e text extensions
* @return query exception
*/
private static QueryException error(final InputInfo ii, final String msg, final Object... e) {
return QueryError.BASX_RESTXQ_X.get(ii, Util.info(msg, e));
}
/** Character iterator. */
private static final class CharIterator {
/** Input text to iterate over. */
private final String input;
/** Input text length. */
private final int len;
/** Current iterator position. */
private int pos;
/**
* Construct a new character iterator for the given input text.
* @param input input text to iterator over.
*/
CharIterator(final String input) {
this.input = input;
len = input.length();
}
/**
* Check if there are more characters to iterate over.
* @return {@code false} if text end is reached
*/
boolean hasNext() {
return pos < len;
}
/**
* Get next character.
* @return next character
*/
char next() {
return input.charAt(pos++);
}
/**
* Get next non-white-space character.
* @return non-white-space character
*/
char nextNonWS() {
char ch;
do {
ch = next();
} while(Character.isWhitespace(ch) && hasNext());
return ch;
}
}
}