/** * String pattern functions. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public final class FNPat extends StandardFunc { /** Pattern cache. */ private final TokenObjMap<Pattern> patterns = new TokenObjMap<Pattern>(); /** Slash pattern. */ private static final Pattern SLASH = Pattern.compile("\\$"); /** Slash pattern. */ private static final Pattern BSLASH = Pattern.compile("\\\\"); /** Root element for the analyze-string-result function. */ private static final QNm Q_ANALYZE = new QNm("fn:analyze-string-result", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_MATCH = new QNm("fn:match", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_NONMATCH = new QNm("fn:non-match", FNURI); /** Element for the analyze-string-result function. */ private static final QNm Q_MGROUP = new QNm("fn:group", FNURI); /** Attribute for the analyze-string-result function. */ private static final QNm Q_NR = new QNm("nr"); /** * Constructor. * * @param ii input info * @param f function definition * @param e arguments */ public FNPat(final InputInfo ii, final Function f, final Expr... e) { super(ii, f, e); } @Override public Iter iter(final QueryContext ctx) throws QueryException { switch (sig) { case TOKENIZE: return tokenize(ctx).iter(); default: return super.iter(ctx); } } @Override public Value value(final QueryContext ctx) throws QueryException { switch (sig) { case TOKENIZE: return tokenize(ctx); default: return super.value(ctx); } } @Override public Item item(final QueryContext ctx, final InputInfo ii) throws QueryException { switch (sig) { case MATCHES: return matches(checkEStr(expr[0], ctx), ctx); case REPLACE: return replace(checkEStr(expr[0], ctx), ctx); case ANALYZE_STRING: return analyzeString(checkEStr(expr[0], ctx), ctx); default: return super.item(ctx, ii); } } /** * Evaluates the match function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item matches(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); return Bln.get(p.matcher(string(val)).find()); } /** * Evaluates the analyze-string function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item analyzeString(final byte[] val, final QueryContext ctx) throws QueryException { final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final String str = string(val); final Matcher m = p.matcher(str); final FElem root = new FElem(Q_ANALYZE, new Atts(FN, FNURI)); int s = 0; while (m.find()) { if (s != m.start()) nonmatch(str.substring(s, m.start()), root); match(m, str, root, 0); s = m.end(); } if (s != str.length()) nonmatch(str.substring(s), root); return root; } /** * Processes a match. * * @param m matcher * @param str string * @param par parent * @param g group number * @return next group number and position in string */ private static int[] match(final Matcher m, final String str, final FElem par, final int g) { final FElem nd = new FElem(g == 0 ? Q_MATCH : Q_MGROUP, new Atts(FN, FNURI)); if (g > 0) nd.add(Q_NR, token(g)); final int start = m.start(g), end = m.end(g), gc = m.groupCount(); int[] pos = {g + 1, start}; // group and position in string while (pos[0] <= gc && m.end(pos[0]) <= end) { final int st = m.start(pos[0]); if (st >= 0) { // group matched if (pos[1] < st) nd.add(str.substring(pos[1], st)); pos = match(m, str, nd, pos[0]); } else pos[0]++; // skip it } if (pos[1] < end) { nd.add(str.substring(pos[1], end)); pos[1] = end; } par.add(nd); return pos; } /** * Processes a non-match. * * @param text text * @param par root node */ private static void nonmatch(final String text, final FElem par) { par.add(new FElem(Q_NONMATCH, new Atts(FN, FNURI)).add(text)); } /** * Evaluates the replace function. * * @param val input value * @param ctx query context * @return function result * @throws QueryException query exception */ private Item replace(final byte[] val, final QueryContext ctx) throws QueryException { final byte[] rep = checkStr(expr[2], ctx); for (int i = 0; i < rep.length; ++i) { if (rep[i] == '\\') { if (i + 1 == rep.length || rep[i + 1] != '\\' && rep[i + 1] != '$') FUNREPBS.thrw(info); ++i; } if (rep[i] == '$' && (i == 0 || rep[i - 1] != '\\') && (i + 1 == rep.length || !digit(rep[i + 1]))) FUNREPDOL.thrw(info); } final Pattern p = pattern(expr[1], expr.length == 4 ? expr[3] : null, ctx); if (p.pattern().isEmpty()) REGROUP.thrw(info); String r = string(rep); if ((p.flags() & Pattern.LITERAL) != 0) { r = SLASH.matcher(BSLASH.matcher(r).replaceAll("\\\\\\\\")).replaceAll("\\\\\\$"); } try { return Str.get(p.matcher(string(val)).replaceAll(r)); } catch (final Exception ex) { if (ex.getMessage().contains("No group")) REGROUP.thrw(info); throw REGPAT.thrw(info, ex); } } /** * Evaluates the tokenize function. * * @param ctx query context * @return function result * @throws QueryException query exception */ private Value tokenize(final QueryContext ctx) throws QueryException { final byte[] val = checkEStr(expr[0], ctx); final Pattern p = pattern(expr[1], expr.length == 3 ? expr[2] : null, ctx); if (p.matcher("").matches()) REGROUP.thrw(info); final TokenList tl = new TokenList(); final String str = string(val); if (!str.isEmpty()) { final Matcher m = p.matcher(str); int s = 0; while (m.find()) { tl.add(str.substring(s, m.start())); s = m.end(); } tl.add(str.substring(s, str.length())); } return StrSeq.get(tl); } /** * Returns a regular expression pattern. * * @param pattern input pattern * @param modifier modifier item * @param ctx query context * @return pattern modifier * @throws QueryException query exception */ private Pattern pattern(final Expr pattern, final Expr modifier, final QueryContext ctx) throws QueryException { final byte[] pat = checkStr(pattern, ctx); final byte[] mod = modifier != null ? checkStr(modifier, ctx) : null; final TokenBuilder tb = new TokenBuilder(pat); if (mod != null) tb.add(0).add(mod); final byte[] key = tb.finish(); Pattern p = patterns.get(key); if (p == null) { p = RegExParser.parse(pat, mod, ctx.sc.xquery3(), info); patterns.add(key, p); } return p; } @Override public boolean xquery3() { return sig == ANALYZE_STRING; } @Override public boolean uses(final Use u) { return u == Use.X30 && xquery3() || u == Use.CNS && sig == ANALYZE_STRING || super.uses(u); } }
/** * This class represents a single RESTXQ function. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ final class RestXqFunction implements Comparable<RestXqFunction> { /** Pattern for a single template. */ private static final Pattern TEMPLATE = Pattern.compile("\\s*\\{\\s*\\$(.+?)\\s*\\}\\s*"); /** Supported methods. */ EnumSet<HTTPMethod> methods = EnumSet.allOf(HTTPMethod.class); /** Serialization parameters. */ final SerializerProp output = new SerializerProp(); /** Associated function. */ final StaticUserFunc function; /** Associated module. */ final RestXqModule module; /** Path. */ RestXqPath path; /** Query parameters. */ final ArrayList<RestXqParam> queryParams = new ArrayList<RestXqParam>(); /** Form parameters. */ final ArrayList<RestXqParam> formParams = new ArrayList<RestXqParam>(); /** Header parameters. */ final ArrayList<RestXqParam> headerParams = new ArrayList<RestXqParam>(); /** Cookie parameters. */ final ArrayList<RestXqParam> cookieParams = new ArrayList<RestXqParam>(); /** Query context. */ private final QueryContext context; /** Consumed media types. */ private final StringList consumes = new StringList(); /** Returned media types. */ private final StringList produces = new StringList(); /** Post/Put variable. */ private QNm requestBody; /** * Constructor. * * @param uf associated user function * @param qc query context * @param m associated module */ RestXqFunction(final StaticUserFunc uf, final QueryContext qc, final RestXqModule m) { function = uf; context = qc; module = m; } /** * Processes the HTTP request. Parses new modules and discards obsolete ones. * * @param http HTTP context * @throws Exception exception */ void process(final HTTPContext http) throws Exception { try { module.process(http, this); } catch (final QueryException ex) { if (ex.file() == null) ex.info(function.info); throw ex; } } /** * Checks a function for RESTFful annotations. * * @return {@code true} if module contains relevant annotations * @throws QueryException query exception */ boolean analyze() throws QueryException { // parse all annotations final EnumSet<HTTPMethod> mth = EnumSet.noneOf(HTTPMethod.class); final boolean[] declared = new boolean[function.args.length]; boolean found = false; final int as = function.ann.size(); for (int a = 0; a < as; a++) { final QNm name = function.ann.names[a]; final Value value = function.ann.values[a]; final byte[] local = name.local(); final byte[] uri = name.uri(); final boolean rexq = eq(uri, QueryText.RESTXQURI); if (rexq) { if (eq(PATH, local)) { // annotation "path" if (path != null) error(ANN_TWICE, "%", name.string()); path = new RestXqPath(toString(value, name)); for (final String s : path) { if (s.trim().startsWith("{")) checkVariable(s, AtomType.AAT, declared); } } else if (eq(CONSUMES, local)) { // annotation "consumes" strings(value, name, consumes); } else if (eq(PRODUCES, local)) { // annotation "produces" strings(value, name, produces); } else if (eq(QUERY_PARAM, local)) { // annotation "query-param" queryParams.add(param(value, name, declared)); } else if (eq(FORM_PARAM, local)) { // annotation "form-param" formParams.add(param(value, name, declared)); } else if (eq(HEADER_PARAM, local)) { // annotation "header-param" headerParams.add(param(value, name, declared)); } else if (eq(COOKIE_PARAM, local)) { // annotation "cookie-param" cookieParams.add(param(value, name, declared)); } else { // method annotations final HTTPMethod m = HTTPMethod.get(string(local)); if (m == null) error(ANN_UNKNOWN, "%", name.string()); if (!value.isEmpty()) { // remember post/put variable if (requestBody != null) error(ANN_TWICE, "%", name.string()); if (m != POST && m != PUT) error(METHOD_VALUE, m); requestBody = checkVariable(toString(value, name), declared); } if (mth.contains(m)) error(ANN_TWICE, "%", name.string()); mth.add(m); } } else if (eq(uri, QueryText.OUTPUTURI)) { // serialization parameters final String key = string(local); final String val = toString(value, name); if (output.get(key) == null) error(UNKNOWN_SER, key); output.set(key, val); } found |= rexq; } if (!mth.isEmpty()) methods = mth; if (found) { if (path == null) error(ANN_MISSING, PATH); for (int i = 0; i < declared.length; i++) if (!declared[i]) error(VAR_UNDEFINED, function.args[i].name.string()); } return found; } /** * Checks if an HTTP request matches this function and its constraints. * * @param http http context * @return result of check */ boolean matches(final HTTPContext http) { // check method, path, consumed and produced media type return methods.contains(http.method) && pathMatches(http) && consumes(http) && produces(http); } /** * Binds the annotated variables. * * @param http http context * @param arg argument array * @throws QueryException query exception * @throws IOException I/O exception */ void bind(final HTTPContext http, final Expr[] arg) throws QueryException, IOException { // bind variables from segments for (int s = 0; s < path.size; s++) { final Matcher m = TEMPLATE.matcher(path.segment[s]); if (!m.find()) continue; final QNm qnm = new QNm(token(m.group(1)), context); bind(qnm, arg, new Atm(http.segment(s))); } // cache request body final String ct = http.contentType(); IOContent body = null; if (requestBody != null) { body = cache(http, null); try { // bind request body in the correct format body.name(http.method + IO.XMLSUFFIX); bind(requestBody, arg, Parser.item(body, context.context.prop, ct)); } catch (final IOException ex) { error(INPUT_CONV, ex); } } // bind query parameters final Map<String, String[]> params = http.params(); for (final RestXqParam rxp : queryParams) bind(rxp, arg, params.get(rxp.key)); // bind form parameters if (!formParams.isEmpty()) { if (MimeTypes.APP_FORM.equals(ct)) { // convert parameters encoded in a form body = cache(http, body); addParams(body.toString(), params); } for (final RestXqParam rxp : formParams) bind(rxp, arg, params.get(rxp.key)); } // bind header parameters for (final RestXqParam rxp : headerParams) { final StringList sl = new StringList(); final Enumeration<?> en = http.req.getHeaders(rxp.key); while (en.hasMoreElements()) { for (final String s : en.nextElement().toString().split(", *")) sl.add(s); } bind(rxp, arg, sl.toArray()); } // bind cookie parameters final Cookie[] ck = http.req.getCookies(); for (final RestXqParam rxp : cookieParams) { String v = null; if (ck != null) { for (final Cookie c : ck) { if (rxp.key.equals(c.getName())) v = c.getValue(); } } if (v == null) bind(rxp, arg); else bind(rxp, arg, v); } } /** * Creates an exception with the specified message. * * @param msg message * @param ext error extension * @return exception * @throws QueryException query exception */ QueryException error(final String msg, final Object... ext) throws QueryException { throw new QueryException(function.info, Err.BASX_RESTXQ, Util.info(msg, ext)); } @Override public int compareTo(final RestXqFunction rxf) { return path.compareTo(rxf.path); } // PRIVATE METHODS ==================================================================== /** * Checks the specified template and adds a variable. * * @param tmp template string * @param declared variable declaration flags * @return resulting variable * @throws QueryException query exception */ private QNm checkVariable(final String tmp, final boolean[] declared) throws QueryException { return checkVariable(tmp, AtomType.ITEM, declared); } /** * Checks the specified template and adds a variable. * * @param tmp template string * @param type allowed type * @param declared variable declaration flags * @return resulting variable * @throws QueryException query exception */ private QNm checkVariable(final String tmp, final Type type, final boolean[] declared) throws QueryException { final Var[] args = function.args; final Matcher m = TEMPLATE.matcher(tmp); if (!m.find()) error(INV_TEMPLATE, tmp); final byte[] vn = token(m.group(1)); if (!XMLToken.isQName(vn)) error(INV_VARNAME, vn); final QNm qnm = new QNm(vn, context); int r = -1; while (++r < args.length && !args[r].name.eq(qnm)) ; if (r == args.length) error(UNKNOWN_VAR, vn); if (declared[r]) error(VAR_ASSIGNED, vn); final SeqType st = args[r].declaredType(); if (args[r].checksType() && !st.type.instanceOf(type)) error(INV_VARTYPE, vn, type); declared[r] = true; return qnm; } /** * Checks if the path matches the HTTP request. * * @param http http context * @return result of check */ private boolean pathMatches(final HTTPContext http) { return path.matches(http); } /** * Checks if the consumed content type matches. * * @param http http context * @return result of check */ private boolean consumes(final HTTPContext http) { // return true if no type is given if (consumes.isEmpty()) return true; // return true if no content type is specified by the user final String ct = http.contentType(); if (ct == null) return true; // check if any combination matches for (final String c : consumes) { if (MimeTypes.matches(c, ct)) return true; } return false; } /** * Checks if the produced content type matches. * * @param http http context * @return result of check */ private boolean produces(final HTTPContext http) { // return true if no type is given if (produces.isEmpty()) return true; // check if any combination matches for (final String pr : http.produces()) { for (final String p : produces) { if (MimeTypes.matches(p, pr)) return true; } } return false; } /** * Binds the specified parameter to a variable. * * @param rxp parameter * @param args argument array * @param values values to be bound; the parameter's default value is assigned if the argument is * {@code null} or empty * @throws QueryException query exception */ private void bind(final RestXqParam rxp, final Expr[] args, final String... values) throws QueryException { final Value val; if (values == null || values.length == 0) { val = rxp.value; } else { final ValueBuilder vb = new ValueBuilder(); for (final String s : values) vb.add(new Atm(s)); val = vb.value(); } bind(rxp.name, args, val); } /** * Binds the specified value to a variable. * * @param name variable name * @param args argument array * @param value value to be bound * @throws QueryException query exception */ private void bind(final QNm name, final Expr[] args, final Value value) throws QueryException { // skip nulled values if (value == null) return; for (int i = 0; i < function.args.length; i++) { final Var var = function.args[i]; if (!var.name.eq(name)) continue; // casts and binds the value args[i] = var.checkType(value, context, null); break; } } /** * Returns the specified value as an atomic string. * * @param value value * @param name name * @return string * @throws QueryException HTTP exception */ private String toString(final Value value, final QNm name) throws QueryException { if (!(value instanceof Str)) error(ANN_STRING, "%", name.string(), value); return ((Str) value).toJava(); } /** * Adds items to the specified list. * * @param value value * @param name name * @param list list to add values to * @throws QueryException HTTP exception */ private void strings(final Value value, final QNm name, final StringList list) throws QueryException { final long vs = value.size(); for (int v = 0; v < vs; v++) list.add(toString(value.itemAt(v), name)); } /** * Returns a parameter. * * @param value value * @param name name * @param declared variable declaration flags * @return parameter * @throws QueryException HTTP exception */ private RestXqParam param(final Value value, final QNm name, final boolean[] declared) throws QueryException { // [CG] RESTXQ: allow identical field names? final long vs = value.size(); if (vs < 2) error(ANN_PARAMS, "%", name.string(), 2); // name of parameter final String key = toString(value.itemAt(0), name); // variable template final QNm qnm = checkVariable(toString(value.itemAt(1), name), declared); // default value final ValueBuilder vb = new ValueBuilder(); for (int v = 2; v < vs; v++) vb.add(value.itemAt(v)); return new RestXqParam(qnm, key, vb.value()); } // PRIVATE STATIC METHODS ============================================================= /** * Caches the request body, if not done yet. * * @param http http context * @param cache cache existing cache reference * @return cache * @throws IOException I/O exception */ private static IOContent cache(final HTTPContext http, final IOContent cache) throws IOException { if (cache != null) return cache; final BufferInput bi = new BufferInput(http.req.getInputStream()); final IOContent io = new IOContent(bi.content()); io.name(http.method + IO.XMLSUFFIX); return io; } /** * Adds parameters from the passed on request body. * * @param body request body * @param params map parameters */ private static void addParams(final String body, final Map<String, String[]> params) { for (final String nv : body.split("&")) { final String[] parts = nv.split("=", 2); if (parts.length < 2) continue; try { params.put(parts[0], new String[] {URLDecoder.decode(parts[1], Token.UTF8)}); } catch (final Exception ex) { Util.notexpected(ex); } } } }