package org.basex.http.restxq; import static org.basex.http.restxq.RestXqText.*; import static org.basex.query.QueryError.*; import static org.basex.query.ann.Annotation.*; import static org.basex.util.Token.*; import java.io.*; import java.util.*; import java.util.Map.*; import java.util.Set; import java.util.regex.*; import javax.servlet.http.*; import org.basex.build.csv.*; import org.basex.build.html.*; import org.basex.build.json.*; import org.basex.build.text.*; import org.basex.core.*; import org.basex.http.*; import org.basex.io.serial.*; import org.basex.query.*; import org.basex.query.ann.*; import org.basex.query.expr.*; import org.basex.query.expr.path.*; import org.basex.query.expr.path.Test.*; import org.basex.query.func.*; import org.basex.query.value.*; import org.basex.query.value.item.*; import org.basex.query.value.seq.*; import org.basex.query.value.type.*; import org.basex.query.var.*; import org.basex.util.*; import org.basex.util.http.*; import org.basex.util.list.*; import org.basex.util.options.*; /** * This class represents a single RESTXQ function. * * @author BaseX Team 2005-17, BSD License * @author Christian Gruen */ final class RestXqFunction implements Comparable<RestXqFunction> { /** Single template pattern. */ private static final Pattern TEMPLATE = Pattern.compile("\\s*\\{\\s*\\$(.+?)\\s*\\}\\s*"); /** EQName pattern. */ private static final Pattern EQNAME = Pattern.compile("^Q\\{(.*?)\\}(.*)$"); /** Query parameters. */ final ArrayList<RestXqParam> queryParams = new ArrayList<>(); /** Form parameters. */ final ArrayList<RestXqParam> formParams = new ArrayList<>(); /** Header parameters. */ final ArrayList<RestXqParam> headerParams = new ArrayList<>(); /** Returned media types. */ final ArrayList<MediaType> produces = new ArrayList<>(); /** Supported methods. */ final Set<String> methods = new HashSet<>(); /** Serialization parameters. */ final SerializerOptions output; /** Associated function. */ final StaticFunc function; /** Associated module. */ private final RestXqModule module; /** Query parameters. */ private final ArrayList<RestXqParam> errorParams = new ArrayList<>(); /** Cookie parameters. */ private final ArrayList<RestXqParam> cookieParams = new ArrayList<>(); /** Consumed media types. */ private final ArrayList<MediaType> consumes = new ArrayList<>(); /** Path. */ RestXqPath path; /** Singleton id (can be {@code null}). */ String singleton; /** Error. */ private RestXqError error; /** Post/Put variable. */ private QNm requestBody; /** * Constructor. * @param function associated user function * @param qc query context * @param module associated module */ RestXqFunction(final StaticFunc function, final QueryContext qc, final RestXqModule module) { this.function = function; this.module = module; output = qc.serParams(); } /** * Processes the HTTP request. * Parses new modules and discards obsolete ones. * @param conn HTTP connection * @param qe query exception (optional) * @throws Exception exception */ void process(final HTTPConnection conn, final QueryException qe) throws Exception { try { module.process(conn, this, qe); } catch(final QueryException ex) { if(ex.file() == null) ex.info(function.info); throw ex; } } /** * Checks a function for RESTFful annotations. * @param ctx database context * @return {@code true} if module contains relevant annotations * @throws Exception exception */ boolean parse(final Context ctx) throws Exception { // parse all annotations final boolean[] declared = new boolean[function.args.length]; boolean found = false; final MainOptions options = ctx.options; for(final Ann ann : function.anns) { final Annotation sig = ann.sig; if(sig == null) continue; found |= eq(sig.uri, QueryText.REST_URI); final Item[] args = ann.args(); if(sig == _REST_PATH) { try { path = new RestXqPath(toString(args[0]), ann.info); } catch(final IllegalArgumentException ex) { throw error(ann.info, ex.getMessage()); } for(final QNm v : path.vars()) checkVariable(v, AtomType.AAT, declared); } else if(sig == _REST_ERROR) { error(ann); } else if(sig == _REST_CONSUMES) { strings(ann, consumes); } else if(sig == _REST_PRODUCES) { strings(ann, produces); } else if(sig == _REST_QUERY_PARAM) { queryParams.add(param(ann, declared)); } else if(sig == _REST_FORM_PARAM) { formParams.add(param(ann, declared)); } else if(sig == _REST_HEADER_PARAM) { headerParams.add(param(ann, declared)); } else if(sig == _REST_COOKIE_PARAM) { cookieParams.add(param(ann, declared)); } else if(sig == _REST_ERROR_PARAM) { errorParams.add(param(ann, declared)); } else if(sig == _REST_METHOD) { final String mth = toString(args[0]).toUpperCase(Locale.ENGLISH); final Item body = args.length > 1 ? args[1] : null; addMethod(mth, body, declared, ann.info); } else if(sig == _REST_SINGLE) { singleton = '\u0001' + (args.length > 0 ? toString(args[0]) : (function.info.path() + ':' + function.info.line())); } else if(eq(sig.uri, QueryText.REST_URI)) { final Item body = args.length == 0 ? null : args[0]; addMethod(string(sig.local()), body, declared, ann.info); } else if(sig == _INPUT_CSV) { final CsvParserOptions opts = new CsvParserOptions(options.get(MainOptions.CSVPARSER)); options.set(MainOptions.CSVPARSER, parse(opts, ann)); } else if(sig == _INPUT_JSON) { final JsonParserOptions opts = new JsonParserOptions(options.get(MainOptions.JSONPARSER)); options.set(MainOptions.JSONPARSER, parse(opts, ann)); } else if(sig == _INPUT_HTML) { final HtmlOptions opts = new HtmlOptions(options.get(MainOptions.HTMLPARSER)); options.set(MainOptions.HTMLPARSER, parse(opts, ann)); } else if(sig == _INPUT_TEXT) { final TextOptions opts = new TextOptions(options.get(MainOptions.TEXTPARSER)); options.set(MainOptions.TEXTPARSER, parse(opts, ann)); } else if(eq(sig.uri, QueryText.OUTPUT_URI)) { // serialization parameters try { output.assign(string(sig.local()), toString(args[0])); } catch(final BaseXException ex) { throw error(ann.info, UNKNOWN_SER, sig.local()); } } } if(found) { if(path == null && error == null) throw error(function.info, ANN_MISSING, '%', PATH, '%', ERROR); final int dl = declared.length; for(int d = 0; d < dl; d++) { if(declared[d]) continue; throw error(function.info, VAR_UNDEFINED, function.args[d].name.string()); } } return found; } /** * Assigns annotation values as options. * @param <O> option type * @param opts options instance * @param ann annotation * @return options instance * @throws Exception any exception */ private static <O extends Options> O parse(final O opts, final Ann ann) throws Exception { for(final Item arg : ann.args()) opts.assign(string(arg.string(ann.info))); return opts; } /** * Add a HTTP method to the list of supported methods by this RESTXQ function. * @param method HTTP method as a string * @param body variable to which the HTTP request body to be bound (optional) * @param declared variable declaration flags * @param ii input info * @throws QueryException query exception */ private void addMethod(final String method, final Item body, final boolean[] declared, final InputInfo ii) throws QueryException { if(body != null && !body.isEmpty()) { final HttpMethod m = HttpMethod.get(method); if(m != null && !m.body) throw error(ii, METHOD_VALUE, m); if(requestBody != null) throw error(ii, ANN_BODYVAR); requestBody = checkVariable(toString(body), declared); } if(methods.contains(method)) throw error(ii, ANN_TWICE, "%", method); methods.add(method); } /** * Checks if an HTTP request matches this function and its constraints. * @param conn HTTP connection * @param err error code * @return result of check */ boolean matches(final HTTPConnection conn, final QNm err) { // check method, consumed and produced media type, and path or error return (methods.isEmpty() || methods.contains(conn.method)) && consumes(conn) && produces(conn) && (err == null ? path != null && path.matches(conn) : error != null && error.matches(err)); } /** * Binds the annotated variables. * @param conn HTTP connection * @param arg argument array * @param err optional query error * @param qc query context * @throws QueryException query exception * @throws IOException I/O exception */ void bind(final HTTPConnection conn, final Expr[] arg, final QueryException err, final QueryContext qc) throws QueryException, IOException { // bind variables from segments if(path != null) { for(final Entry<QNm, String> entry : path.values(conn).entrySet()) { final QNm qnm = new QNm(entry.getKey().string(), function.sc); if(function.sc.elemNS != null && eq(qnm.uri(), function.sc.elemNS)) qnm.uri(EMPTY); bind(qnm, arg, new Atm(entry.getValue()), qc); } } // bind request body in the correct format final MainOptions mo = conn.context.options; if(requestBody != null) { try { bind(requestBody, arg, HttpPayload.value(conn.params.body(), mo, conn.contentType()), qc); } catch(final IOException ex) { throw error(INPUT_CONV, ex); } } // bind query and form parameters for(final RestXqParam rxp : queryParams) bind(rxp, arg, conn.params.query().get(rxp.name), qc); for(final RestXqParam rxp : formParams) bind(rxp, arg, conn.params.form(mo).get(rxp.name), qc); // bind header parameters for(final RestXqParam rxp : headerParams) { final TokenList tl = new TokenList(); final Enumeration<?> en = conn.req.getHeaders(rxp.name); while(en.hasMoreElements()) { for(final String s : en.nextElement().toString().split(", *")) tl.add(s); } bind(rxp, arg, StrSeq.get(tl), qc); } // bind cookie parameters final Cookie[] ck = conn.req.getCookies(); for(final RestXqParam rxp : cookieParams) { Value val = Empty.SEQ; if(ck != null) { for(final Cookie c : ck) { if(rxp.name.equals(c.getName())) val = Str.get(c.getValue()); } } bind(rxp, arg, val, qc); } // bind errors final Map<String, Value> errs = new HashMap<>(); if(err != null) { final Value[] values = Catch.values(err); final QNm[] names = Catch.NAMES; final int nl = names.length; for(int n = 0; n < nl; n++) errs.put(string(names[n].local()), values[n]); } for(final RestXqParam rxp : errorParams) bind(rxp, arg, errs.get(rxp.name), qc); } /** * Creates an exception with the specified message. * @param msg message * @param ext error extension * @return exception */ QueryException error(final String msg, final Object... ext) { return error(function.info, msg, ext); } /** * Creates an exception with the specified message. * @param ii input info * @param msg message * @param ext error extension * @return exception */ private static QueryException error(final InputInfo ii, final String msg, final Object... ext) { return BASX_RESTXQ_X.get(ii, Util.info(msg, ext)); } @Override public int compareTo(final RestXqFunction rxf) { return path == null ? error.compareTo(rxf.error) : 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 Matcher m = TEMPLATE.matcher(tmp); if(!m.find()) throw error(INV_TEMPLATE, tmp); final byte[] vn = token(m.group(1)); if(!XMLToken.isQName(vn)) throw error(INV_VARNAME, vn); final QNm name = new QNm(vn); return checkVariable(name, type, declared); } /** * Checks if the specified variable exists in the current function. * @param name variable * @param type allowed type * @param declared variable declaration flags * @return resulting variable * @throws QueryException query exception */ private QNm checkVariable(final QNm name, final Type type, final boolean[] declared) throws QueryException { if(name.hasPrefix()) name.uri(function.sc.ns.uri(name.prefix())); int a = -1; final Var[] args = function.args; final int al = args.length; while(++a < al && !args[a].name.eq(name)); if(a == args.length) throw error(UNKNOWN_VAR, name.string()); if(declared[a]) throw error(VAR_ASSIGNED, name.string()); final SeqType st = args[a].declaredType(); if(args[a].checksType() && !st.type.instanceOf(type)) throw error(INV_VARTYPE, name.string(), type); declared[a] = true; return name; } /** * Checks if the consumed content type matches. * @param conn HTTP connection * @return result of check */ private boolean consumes(final HTTPConnection conn) { // return true if no type is given if(consumes.isEmpty()) return true; // return true if no content type is specified by the user final MediaType type = conn.contentType(); if(type.type().isEmpty()) return true; // check if any combination matches for(final MediaType consume : consumes) { if(consume.matches(type)) return true; } return false; } /** * Checks if the produced media type matches. * @param conn HTTP connection * @return result of check */ private boolean produces(final HTTPConnection conn) { // return true if no type is given if(produces.isEmpty()) return true; // check if any combination matches for(final MediaType accept : conn.accepts()) { for(final MediaType produce : produces) { if(produce.matches(accept)) return true; } } return false; } /** * Binds the specified parameter to a variable. * @param rxp parameter * @param args argument array * @param value values to be bound; the parameter's default value is assigned * if the argument is {@code null} or empty * @param qc query context * @throws QueryException query exception */ private void bind(final RestXqParam rxp, final Expr[] args, final Value value, final QueryContext qc) throws QueryException { bind(rxp.var, args, value == null || value.isEmpty() ? rxp.value : value, qc); } /** * Binds the specified value to a variable. * @param name variable name * @param args argument array * @param value value to be bound * @param qc query context * @throws QueryException query exception */ private void bind(final QNm name, final Expr[] args, final Value value, final QueryContext qc) throws QueryException { // skip nulled values if(value == null) return; final Var[] fargs = function.args; final int fl = fargs.length; for(int f = 0; f < fl; f++) { final Var var = fargs[f]; if(var.name.eq(name)) { // casts and binds the value final SeqType decl = var.declaredType(); final Value val = value.seqType().instanceOf(decl) ? value : decl.cast(value, qc, function.sc, null); args[f] = var.checkType(val, qc, false); break; } } } /** * Returns the specified item as a string. * @param item item * @return string */ private static String toString(final Item item) { return ((Str) item).toJava(); } /** * Adds items to the specified list. * @param ann annotation * @param list list to add values to */ private static void strings(final Ann ann, final ArrayList<MediaType> list) { for(final Item it : ann.args()) list.add(new MediaType(toString(it))); } /** * Returns a parameter. * @param ann annotation * @param declared variable declaration flags * @return parameter * @throws QueryException HTTP exception */ private RestXqParam param(final Ann ann, final boolean... declared) throws QueryException { // name of parameter final Item[] args = ann.args(); final String name = toString(args[0]); // variable template final QNm var = checkVariable(toString(args[1]), declared); // default value final int al = args.length; final ValueBuilder vb = new ValueBuilder(); for(int a = 2; a < al; a++) vb.add(args[a]); return new RestXqParam(var, name, vb.value()); } /** * Creates an error function. * @param ann annotation * @throws QueryException HTTP exception */ private void error(final Ann ann) throws QueryException { if(error == null) error = new RestXqError(); // name of parameter NameTest last = error.get(0); for(final Item arg : ann.args()) { final String err = toString(arg); final Kind kind; QNm qnm = null; if(err.equals("*")) { kind = Kind.WILDCARD; } else if(err.startsWith("*:")) { final byte[] local = token(err.substring(2)); if(!XMLToken.isNCName(local)) throw error(INV_CODE, err); qnm = new QNm(local); kind = Kind.NAME; } else if(err.endsWith(":*")) { final byte[] prefix = token(err.substring(0, err.length() - 2)); if(!XMLToken.isNCName(prefix)) throw error(INV_CODE, err); qnm = new QNm(concat(prefix, COLON), function.sc); kind = Kind.URI; } else { final Matcher m = EQNAME.matcher(err); if(m.matches()) { final byte[] uri = token(m.group(1)); final byte[] local = token(m.group(2)); if(local.length == 1 && local[0] == '*') { qnm = new QNm(COLON, uri); kind = Kind.URI; } else { if(!XMLToken.isNCName(local) || !Uri.uri(uri).isValid()) throw error(INV_CODE, err); qnm = new QNm(local, uri); kind = Kind.URI_NAME; } } else { final byte[] nm = token(err); if(!XMLToken.isQName(nm)) throw error(INV_CODE, err); qnm = new QNm(nm, function.sc); kind = Kind.URI_NAME; } } // message if(qnm != null && qnm.hasPrefix() && !qnm.hasURI()) throw error(INV_NONS, qnm); final NameTest test = new NameTest(qnm, kind, false, null); if(last != null && last.kind != kind) throw error(INV_PRIORITY, last, test); if(!error.add(test)) throw error(INV_ERR_SAME, last); last = test; } } }