예제 #1
0
/**
 * This class is an entry point for evaluating XQuery strings.
 *
 * @author BaseX Team 2005-15, BSD License
 * @author Christian Gruen
 */
public final class QueryProcessor extends Proc implements AutoCloseable {
  /** Pattern for detecting library modules. */
  private static final Pattern LIBMOD_PATTERN =
      Pattern.compile(
          "^(xquery( version ['\"].*?['\"])?( encoding ['\"].*?['\"])? ?; ?)?module namespace.*");

  /** Static context. */
  public final StaticContext sc;
  /** Expression context. */
  public final QueryContext qc;
  /** Query. */
  private final String query;
  /** Parsed flag. */
  private boolean parsed;

  /**
   * Default constructor.
   *
   * @param query query string
   * @param ctx database context
   */
  public QueryProcessor(final String query, final Context ctx) {
    this.query = query;
    qc = proc(new QueryContext(ctx));
    sc = new StaticContext(ctx);
  }

  /**
   * Parses the query.
   *
   * @throws QueryException query exception
   */
  public void parse() throws QueryException {
    if (parsed) return;
    parsed = true;
    qc.parseMain(query, null, sc);
    updating = qc.updating;
  }

  /**
   * Compiles the query.
   *
   * @throws QueryException query exception
   */
  public void compile() throws QueryException {
    parse();
    qc.compile();
  }

  /**
   * Returns a result iterator.
   *
   * @return result iterator
   * @throws QueryException query exception
   */
  public Iter iter() throws QueryException {
    parse();
    return qc.iter();
  }

  /**
   * Returns a result value.
   *
   * @return result value
   * @throws QueryException query exception
   */
  public Value value() throws QueryException {
    parse();
    return qc.iter().value();
  }

  /**
   * Evaluates the specified query and returns the result.
   *
   * @return result of query
   * @throws QueryException query exception
   */
  public Result execute() throws QueryException {
    parse();
    return qc.execute();
  }

  /**
   * Binds a value with the specified type to a global variable. If the value is an {@link Expr}
   * instance, it is directly assigned. Otherwise, it is first cast to the appropriate XQuery type.
   * If {@code "json"} is specified as type, the value is interpreted according to the rules
   * specified in {@link JsonMapConverter}.
   *
   * @param name name of variable
   * @param value value to be bound
   * @param type type (may be {@code null})
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Object value, final String type)
      throws QueryException {
    qc.bind(name, value, type, sc);
    return this;
  }

  /**
   * Binds a value to a global variable.
   *
   * @param name name of variable
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Object value) throws QueryException {
    return bind(name, value, null);
  }

  /**
   * Binds an XQuery value to a global variable.
   *
   * @param name name of variable
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Value value) throws QueryException {
    qc.bind(name, value, sc);
    return this;
  }

  /**
   * Binds the context value.
   *
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor context(final Object value) throws QueryException {
    return context(value, null);
  }

  /**
   * Binds the context value.
   *
   * @param value XQuery value to be bound
   * @return self reference
   */
  public QueryProcessor context(final Value value) {
    qc.context(value, sc);
    return this;
  }

  /**
   * Binds the HTTP context to the query processor.
   *
   * @param value HTTP context
   * @return self reference
   */
  public QueryProcessor http(final Object value) {
    qc.http(value);
    return this;
  }

  /**
   * Binds the context value with a specified type, using the same rules as for {@link #bind binding
   * variables}.
   *
   * @param value value to be bound
   * @param type type (may be {@code null})
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor context(final Object value, final String type) throws QueryException {
    qc.context(value, type, sc);
    return this;
  }

  /**
   * Declares a namespace. A namespace is undeclared if the {@code uri} is an empty string. The
   * default element namespaces is set if the {@code prefix} is empty.
   *
   * @param prefix namespace prefix
   * @param uri namespace uri
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor namespace(final String prefix, final String uri) throws QueryException {
    sc.namespace(prefix, uri);
    return this;
  }

  /**
   * Returns a serializer for the given output stream. Optional output declarations within the query
   * will be included in the serializer instance.
   *
   * @param os output stream
   * @return serializer instance
   * @throws IOException query exception
   * @throws QueryException query exception
   */
  public Serializer getSerializer(final OutputStream os) throws IOException, QueryException {
    compile();
    try {
      return Serializer.get(os, qc.serParams());
    } catch (final QueryIOException ex) {
      throw ex.getCause();
    }
  }

  /**
   * Adds a module reference. Only called from the test APIs.
   *
   * @param uri module uri
   * @param file file name
   */
  public void module(final String uri, final String file) {
    qc.modDeclared.put(uri, file);
  }

  /**
   * Returns the query string.
   *
   * @return query
   */
  public String query() {
    return query;
  }

  @Override
  public void close() {
    qc.close();
  }

  @Override
  public void databases(final LockResult lr) {
    qc.databases(lr);
  }

  /**
   * Returns the number of performed updates after query execution, or {@code 0}.
   *
   * @return number of updates
   */
  public int updates() {
    return updating ? qc.resources.updates().size() : 0;
  }

  /**
   * Returns query information.
   *
   * @return query information
   */
  public String info() {
    return qc.info();
  }

  /**
   * Checks if the specified XQuery string is a library module.
   *
   * @param qu query string
   * @return result of check
   */
  public static boolean isLibrary(final String qu) {
    return LIBMOD_PATTERN.matcher(removeComments(qu, 80)).matches();
  }

  /**
   * Removes comments from the specified string and returns the first characters of a query.
   *
   * @param qu query string
   * @param max maximum length of string to return
   * @return result
   */
  public static String removeComments(final String qu, final int max) {
    final StringBuilder sb = new StringBuilder();
    int m = 0;
    boolean s = false;
    final int cl = qu.length();
    for (int c = 0; c < cl && sb.length() < max; ++c) {
      final char ch = qu.charAt(c);
      if (ch == 0x0d) continue;
      if (ch == '(' && c + 1 < cl && qu.charAt(c + 1) == ':') {
        if (m == 0 && !s) {
          sb.append(' ');
          s = true;
        }
        ++m;
        ++c;
      } else if (m != 0 && ch == ':' && c + 1 < cl && qu.charAt(c + 1) == ')') {
        --m;
        ++c;
      } else if (m == 0) {
        if (ch > ' ') sb.append(ch);
        else if (!s) sb.append(' ');
        s = ch <= ' ';
      }
    }
    if (sb.length() >= max) sb.append("...");
    return sb.toString().trim();
  }

  /**
   * Returns a map with variable bindings.
   *
   * @param opts main options
   * @return bindings
   */
  public static HashMap<String, String> bindings(final MainOptions opts) {
    final HashMap<String, String> bindings = new HashMap<>();
    final String bind = opts.get(MainOptions.BINDINGS).trim();
    final StringBuilder key = new StringBuilder();
    final StringBuilder val = new StringBuilder();
    boolean first = true;
    final int sl = bind.length();
    for (int s = 0; s < sl; s++) {
      final char ch = bind.charAt(s);
      if (first) {
        if (ch == '=') {
          first = false;
        } else {
          key.append(ch);
        }
      } else {
        if (ch == ',') {
          if (s + 1 == sl || bind.charAt(s + 1) != ',') {
            bindings.put(key.toString().trim(), val.toString());
            key.setLength(0);
            val.setLength(0);
            first = true;
            continue;
          }
          // literal commas are escaped by a second comma
          s++;
        }
        val.append(ch);
      }
    }
    if (key.length() != 0) bindings.put(key.toString().trim(), val.toString());
    return bindings;
  }

  /**
   * Returns a tree representation of the query plan.
   *
   * @return root node
   */
  public FDoc plan() {
    return new FDoc().add(qc.plan());
  }

  @Override
  public String tit() {
    return PLEASE_WAIT_D;
  }

  @Override
  public String det() {
    return PLEASE_WAIT_D;
  }

  @Override
  public String toString() {
    return query;
  }
}
예제 #2
0
/**
 * This class is an entry point for evaluating XQuery strings.
 *
 * @author BaseX Team 2005-16, BSD License
 * @author Christian Gruen
 */
public final class QueryProcessor extends Job implements Closeable {
  /** Pattern for detecting library modules. */
  private static final Pattern LIBMOD_PATTERN =
      Pattern.compile(
          "^(xquery( version ['\"].*?['\"])?( encoding ['\"].*?['\"])? ?; ?)?module namespace.*");

  /** Static context. */
  public final StaticContext sc;
  /** Expression context. */
  public final QueryContext qc;
  /** Query. */
  private final String query;
  /** Parsed flag. */
  private boolean parsed;

  /**
   * Default constructor.
   *
   * @param query query string
   * @param ctx database context
   */
  public QueryProcessor(final String query, final Context ctx) {
    this(query, null, ctx);
  }

  /**
   * Default constructor.
   *
   * @param query query string
   * @param uri base uri (can be {@code null})
   * @param ctx database context
   */
  public QueryProcessor(final String query, final String uri, final Context ctx) {
    this.query = query;
    qc = pushJob(new QueryContext(ctx));
    sc = new StaticContext(qc);
    sc.baseURI(uri);
  }

  /**
   * Parses the query.
   *
   * @throws QueryException query exception
   */
  public void parse() throws QueryException {
    if (parsed) return;
    try {
      qc.parseMain(query, null, sc);
    } finally {
      parsed = true;
      updating = qc.updating;
    }
  }

  /**
   * Compiles the query.
   *
   * @throws QueryException query exception
   */
  public void compile() throws QueryException {
    parse();
    qc.compile();
  }

  /**
   * Returns a memory-efficient result iterator. In most cases, the query will only be fully
   * evaluated if all items of this iterator are requested.
   *
   * @return result iterator
   * @throws QueryException query exception
   */
  public Iter iter() throws QueryException {
    parse();
    return qc.iter();
  }

  /**
   * Evaluates the query and returns the resulting value.
   *
   * @return result value
   * @throws QueryException query exception
   */
  public Value value() throws QueryException {
    parse();
    return qc.iter().value();
  }

  /**
   * This function is called by the GUI; use {@link #iter()} or {@link #value()} instead. Caches and
   * returns the result of the specified query. If all nodes are of the same database instance, the
   * returned value will be of type {@link DBNodes}.
   *
   * @param max maximum number of results to cache (negative: return all values)
   * @return result of query
   * @throws QueryException query exception
   */
  public Value cache(final int max) throws QueryException {
    parse();
    return qc.cache(max);
  }

  /**
   * Binds a value with the specified type to a global variable. If the value is an {@link Expr}
   * instance, it is directly assigned. Otherwise, it is first cast to the appropriate XQuery type.
   * If {@code "json"} is specified as type, the value is interpreted according to the rules
   * specified in {@link JsonMapConverter}.
   *
   * @param name name of variable
   * @param value value to be bound
   * @param type type (may be {@code null})
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Object value, final String type)
      throws QueryException {
    qc.bind(name, value, type, sc);
    return this;
  }

  /**
   * Binds a value to a global variable.
   *
   * @param name name of variable
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Object value) throws QueryException {
    return bind(name, value, null);
  }

  /**
   * Binds an XQuery value to a global variable.
   *
   * @param name name of variable
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor bind(final String name, final Value value) throws QueryException {
    qc.bind(name, value, sc);
    return this;
  }

  /**
   * Binds the context value.
   *
   * @param value value to be bound
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor context(final Object value) throws QueryException {
    return context(value, null);
  }

  /**
   * Binds the context value.
   *
   * @param value XQuery value to be bound
   * @return self reference
   */
  public QueryProcessor context(final Value value) {
    qc.context(value, sc);
    return this;
  }

  /**
   * Binds the HTTP context to the query processor.
   *
   * @param value HTTP context
   * @return self reference
   */
  public QueryProcessor http(final Object value) {
    qc.http(value);
    return this;
  }

  /**
   * Binds the context value with a specified type, using the same rules as for {@link #bind binding
   * variables}.
   *
   * @param value value to be bound
   * @param type type (may be {@code null})
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor context(final Object value, final String type) throws QueryException {
    qc.context(value, type, sc);
    return this;
  }

  /**
   * Declares a namespace. A namespace is undeclared if the {@code uri} is an empty string. The
   * default element namespaces is set if the {@code prefix} is empty.
   *
   * @param prefix namespace prefix
   * @param uri namespace uri
   * @return self reference
   * @throws QueryException query exception
   */
  public QueryProcessor namespace(final String prefix, final String uri) throws QueryException {
    sc.namespace(prefix, uri);
    return this;
  }

  /**
   * Assigns a URI resolver.
   *
   * @param resolver resolver
   * @return self reference
   */
  public QueryProcessor uriResolver(final UriResolver resolver) {
    sc.resolver = resolver;
    return this;
  }

  /**
   * Returns a serializer for the given output stream. Optional output declarations within the query
   * will be included in the serializer instance.
   *
   * @param os output stream
   * @return serializer instance
   * @throws IOException query exception
   * @throws QueryException query exception
   */
  public Serializer getSerializer(final OutputStream os) throws IOException, QueryException {
    compile();
    try {
      return Serializer.get(os, qc.serParams()).sc(sc);
    } catch (final QueryIOException ex) {
      throw ex.getCause();
    }
  }

  /**
   * Adds a module reference. Only called from the test APIs.
   *
   * @param uri module uri
   * @param file file name
   */
  public void module(final String uri, final String file) {
    qc.modDeclared.put(uri, file);
  }

  /**
   * Returns the query string.
   *
   * @return query
   */
  public String query() {
    return query;
  }

  @Override
  public void close() {
    qc.close();
  }

  @Override
  public void databases(final LockResult lr) {
    qc.databases(lr);
  }

  /**
   * Returns the number of performed updates after query execution, or {@code 0}.
   *
   * @return number of updates
   */
  public int updates() {
    return updating ? qc.updates().size() : 0;
  }

  /**
   * Returns query information.
   *
   * @return query information
   */
  public String info() {
    return qc.info();
  }

  /**
   * Checks if the specified XQuery string is a library module.
   *
   * @param qu query string
   * @return result of check
   */
  public static boolean isLibrary(final String qu) {
    return LIBMOD_PATTERN.matcher(removeComments(qu, 80)).matches();
  }

  /**
   * Removes comments from the specified string and returns the first characters of a query.
   *
   * @param qu query string
   * @param max maximum length of string to return
   * @return result
   */
  public static String removeComments(final String qu, final int max) {
    final StringBuilder sb = new StringBuilder();
    int m = 0;
    boolean s = false;
    final int cl = qu.length();
    for (int c = 0; c < cl && sb.length() < max; ++c) {
      final char ch = qu.charAt(c);
      if (ch == 0x0d) continue;
      if (ch == '(' && c + 1 < cl && qu.charAt(c + 1) == ':') {
        if (m == 0 && !s) {
          sb.append(' ');
          s = true;
        }
        ++m;
        ++c;
      } else if (m != 0 && ch == ':' && c + 1 < cl && qu.charAt(c + 1) == ')') {
        --m;
        ++c;
      } else if (m == 0) {
        if (ch > ' ') sb.append(ch);
        else if (!s) sb.append(' ');
        s = ch <= ' ';
      }
    }
    if (sb.length() >= max) sb.append("...");
    return sb.toString().trim();
  }

  /**
   * Returns a tree representation of the query plan.
   *
   * @return root node
   */
  public FDoc plan() {
    return new FDoc().add(qc.plan());
  }

  @Override
  public String shortInfo() {
    return PLEASE_WAIT_D;
  }

  @Override
  public String detailedInfo() {
    return PLEASE_WAIT_D;
  }

  @Override
  public String toString() {
    return query;
  }
}
예제 #3
0
/**
 * 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);
  }
}
예제 #4
0
/**
 * 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);
      }
    }
  }
}