All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.basex.http.restxq.RestXqFunction Maven / Gradle / Ivy

The newest version!
package org.basex.http.restxq;

import static org.basex.http.web.WebText.*;
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.Set;
import java.util.function.Function;
import java.util.regex.*;

import jakarta.servlet.http.*;

import org.basex.build.csv.*;
import org.basex.build.html.*;
import org.basex.build.json.*;
import org.basex.core.*;
import org.basex.http.*;
import org.basex.http.web.*;
import org.basex.query.*;
import org.basex.query.ann.*;
import org.basex.query.expr.*;
import org.basex.query.expr.path.*;
import org.basex.query.func.*;
import org.basex.query.util.hash.*;
import org.basex.query.util.list.*;
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.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-24, BSD License
 * @author Christian Gruen
 */
public final class RestXqFunction extends WebFunction {
  /** EQName pattern. */
  private static final Pattern EQNAME = Pattern.compile("^Q\\{(.*?)}(.*)$");

  /** Returned media types. */
  public final ArrayList produces = new ArrayList<>();
  /** Query parameters. */
  final ArrayList queryParams = new ArrayList<>();
  /** Form parameters. */
  final ArrayList formParams = new ArrayList<>();

  /** Supported methods. */
  final Set methods = new HashSet<>();
  /** Permissions (can be empty). */
  final TokenList allows = new TokenList();

  /** Error parameters. */
  private final ArrayList errorParams = new ArrayList<>();
  /** Cookie parameters. */
  private final ArrayList cookieParams = new ArrayList<>();
  /** Consumed media types. */
  private final ArrayList consumes = new ArrayList<>();

  /** Path (can be {@code null}). */
  public RestXqPath path;
  /** Singleton id (can be {@code null}). */
  String singleton;

  /** Post/Put variable (can be {@code null}). */
  private QNm requestBody;

  /** Error (can be {@code null}). */
  private RestXqError error;
  /** Error (can be {@code null}). */
  private RestXqPerm permission;

  /**
   * Constructor.
   * @param function associated user function
   * @param module web module
   * @param qc query context
   */
  public RestXqFunction(final StaticFunc function, final WebModule module, final QueryContext qc) {
    super(function, module, qc);
  }

  @Override
  public boolean parseAnnotations(final Context ctx) throws QueryException, IOException {
    // parse all annotations
    final boolean[] declared = new boolean[function.arity()];
    boolean found = false;
    final MainOptions options = ctx.options;

    AnnList starts = AnnList.EMPTY;
    for(final Ann ann : function.anns) {
      final Annotation def = ann.definition;
      if(def == null) continue;

      found |= eq(def.uri, QueryText.REST_URI, QueryText.PERM_URI);
      final Value value = ann.value();
      if(def == _REST_PATH) {
        try {
          path = new RestXqPath(toString(value.itemAt(0)), ann.info);
          starts = starts.attach(ann);
        } catch(final IllegalArgumentException ex) {
          throw error(ann.info, ex.getMessage());
        }
        for(final QNm name : path.varNames()) {
          checkVariable(name, declared);
        }
      } else if(def == _REST_ERROR) {
        error(ann);
        // function can have multiple error annotations
        if(!starts.contains(def)) starts = starts.attach(ann);
      } else if(def == _REST_CONSUMES) {
        strings(ann, consumes);
      } else if(def == _REST_PRODUCES) {
        strings(ann, produces);
      } else if(def == _REST_QUERY_PARAM) {
        queryParams.add(param(ann, declared));
      } else if(def == _REST_FORM_PARAM) {
        formParams.add(param(ann, declared));
      } else if(def == _REST_HEADER_PARAM) {
        headerParams.add(param(ann, declared));
      } else if(def == _REST_COOKIE_PARAM) {
        cookieParams.add(param(ann, declared));
      } else if(def == _REST_ERROR_PARAM) {
        errorParams.add(param(ann, declared));
      } else if(def == _REST_METHOD) {
        final String mth = toString(value.itemAt(0)).toUpperCase(Locale.ENGLISH);
        final Item body = value.size() > 1 ? value.itemAt(1) : null;
        addMethod(mth, body, declared, ann.info);
      } else if(def == _REST_SINGLE) {
        singleton = '\u0001' + (!value.isEmpty() ? toString(value.itemAt(0)) :
          function.info.path() + ':' + function.info.line());
      } else if(eq(def.uri, QueryText.REST_URI)) {
        final Item body = value.isEmpty() ? null : value.itemAt(0);
        addMethod(string(def.local()), body, declared, ann.info);
      } else if(def == _INPUT_CSV) {
        final CsvParserOptions opts = new CsvParserOptions(options.get(MainOptions.CSVPARSER));
        options.set(MainOptions.CSVPARSER, parse(opts, ann));
      } else if(def == _INPUT_JSON) {
        final JsonParserOptions opts = new JsonParserOptions(options.get(MainOptions.JSONPARSER));
        options.set(MainOptions.JSONPARSER, parse(opts, ann));
      } else if(def == _INPUT_HTML) {
        final HtmlOptions opts = new HtmlOptions(options.get(MainOptions.HTMLPARSER));
        options.set(MainOptions.HTMLPARSER, parse(opts, ann));
      } else if(eq(def.uri, QueryText.OUTPUT_URI)) {
        // serialization parameters
        final String name = string(def.local()), val = toString(value.itemAt(0));
        try {
          sopts.assign(name, val);
        } catch(final BaseXException ex) {
          throw error(ann.info, UNKNOWN_PARAMETER_X, ex);
        }
      } else if(def == _PERM_ALLOW) {
        for(final Item arg : value) allows.add(toString(arg));
      } else if(def == _PERM_CHECK) {
        final String p = value.isEmpty() ? "" : toString(value.itemAt(0));
        final QNm v = value.size() > 1 ? checkVariable(toString(value.itemAt(1)), declared) : null;
        permission = new RestXqPerm(p, v);
        starts = starts.attach(ann);
      }
    }

    // check validity of quality factors
    for(final MediaType produce : produces) {
      final String qs = produce.parameter("qs");
      if(qs != null) {
        final double d = toDouble(token(qs));
        // NaN will be included if negated condition is used...
        if(d < 0 || d > 1) throw error(ERROR_QS_X, qs);
      }
    }
    return checkParsed(found, starts, declared);
  }

  /**
   * Binds the annotated variables.
   * @param ext extended processing information (can be {@code null})
   * @param conn HTTP connection
   * @param qc query context
   * @return arguments
   * @throws QueryException exception
   * @throws IOException I/O exception
   */
  Expr[] bind(final Object ext, final HTTPConnection conn, final QueryContext qc)
      throws QueryException, IOException {

    // bind variables from segments
    final Expr[] args = new Expr[function.arity()];
    if(path != null) {
      final QNmMap qnames = path.values(conn);
      for(final QNm qname : qnames) {
        final QNm qnm = new QNm(qname.string(), function.sc);
        if(function.sc.elemNS != null && eq(qnm.uri(), function.sc.elemNS)) qnm.uri(EMPTY);
        bind(qnm, args, Atm.get(qnames.get(qname)), qc, "Path segment");
      }
    }

    // bind request body in the correct format
    final MainOptions mopts = conn.context.options;
    if(requestBody != null) {
      final MediaType type = conn.mediaType();
      final byte[] body = conn.requestCtx.body().read();
      final Value value;
      try {
        value = Payload.value(body, type, mopts);
      } catch(final IOException ex) {
        throw error(BODY_TYPE_X_X, type, ex);
      }
      bind(requestBody, args, value, qc, "Request body");
    }

    // bind query and form parameters
    for(final WebParam rxp : queryParams) {
      bind(rxp, args, conn.requestCtx.queryValues().get(rxp.name), qc);
    }
    for(final WebParam rxp : formParams) {
      bind(rxp, args, conn.requestCtx.formValues(mopts).get(rxp.name), qc);
    }

    // bind header parameters
    for(final WebParam rxp : headerParams) {
      final TokenList tl = new TokenList();
      final Enumeration en = conn.request.getHeaders(rxp.name);
      while(en.hasMoreElements()) {
        for(final String s : en.nextElement().toString().split(", *")) tl.add(s);
      }
      bind(rxp, args, StrSeq.get(tl), qc);
    }

    // bind cookie parameters
    final Cookie[] ck = conn.request.getCookies();
    for(final WebParam rxp : cookieParams) {
      Value value = Empty.VALUE;
      if(ck != null) {
        for(final Cookie c : ck) {
          if(rxp.name.equals(c.getName())) value = Str.get(c.getValue());
        }
      }
      bind(rxp, args, value, qc);
    }

    // bind errors
    final Map errors = new HashMap<>();
    if(ext instanceof QueryException) {
      final Value[] values = Catch.values((QueryException) ext);
      final int vl = values.length;
      for(int v = 0; v < vl; v++) errors.put(Catch.NAMES[v], values[v]);
    }
    for(final WebParam rxp : errorParams) bind(rxp, args, errors.get(rxp.name), qc);

    // bind permission information
    if(ext instanceof RestXqFunction && permission.var != null) {
      bind(permission.var, args, RestXqPerm.map((RestXqFunction) ext, conn), qc, "Error info");
    }
    return args;
  }

  /**
   * Checks if an HTTP request matches this function and its constraints.
   * @param conn HTTP connection
   * @param err error code (assigned if error function is to be called)
   * @param perm permission flag
   * @return result of check
   */
  public boolean matches(final HTTPConnection conn, final QNm err, final boolean perm) {
    // check method, consumed and produced media type, and path or error
    if(!methods.isEmpty() && !methods.contains(conn.method) || !consumes(conn) || !produces(conn))
      return false;

    if(perm) return permission != null && permission.matches(conn);
    if(err != null) return error != null && error.matches(err);
    return path != null && path.matches(conn);
  }

  /**
   * Returns the most specific consume type for the specified type.
   * @param type media type
   * @return most specific type
   */
  public MediaType consumedType(final MediaType type) {
    MediaType mt = null;
    for(final MediaType consume : consumes) {
      if(type.matches(consume) && (mt == null || mt.compareTo(consume) > 0)) mt = consume;
    }
    return mt == null ? MediaType.ALL_ALL : mt;
  }

  @Override
  public QueryException error(final String msg, final Object... ext) {
    return error(function.info, msg, ext);
  }

  /**
   * Creates an exception with the specified message.
   * @param info input info (can be {@code null})
   * @param msg error message
   * @param ext error extension
   * @return QueryException query exception
   */
  static QueryException error(final InputInfo info, final String msg, final Object... ext) {
    return BASEX_RESTXQ_X.get(info, Util.info(msg, ext));
  }

  @Override
  public int compareTo(final WebFunction func) {
    if(!(func instanceof RestXqFunction)) return -1;

    final RestXqFunction rxf = (RestXqFunction) func;
    if(path != null) return path.compareTo(rxf.path);
    if(error != null) return error.compareTo(rxf.error);
    return permission.compareTo(rxf.permission);
  }

  @Override
  public String toString() {
    final StringBuilder sb = new StringBuilder().append(super.toString());
    if(!produces.isEmpty()) sb.append(' ').append(produces);
    return sb.toString();
  }

  // PRIVATE METHODS ==============================================================================

  /**
   * Assigns annotation values as options.
   * @param  option type
   * @param opts options instance
   * @param ann annotation
   * @return options instance
   * @throws QueryException query exception
   * @throws IOException I/O exception
   */
  private static  O parse(final O opts, final Ann ann)
      throws QueryException, IOException {
    for(final Item arg : ann.value()) opts.assign(string(arg.string(ann.info)));
    return opts;
  }

  /**
   * Adds an 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 info input info (can be {@code null})
   * @throws QueryException query exception
   */
  private void addMethod(final String method, final Item body, final boolean[] declared,
      final InputInfo info) throws QueryException {

    if(body != null) {
      final Method m = Method.get(method);
      if(m != null && !m.body) throw error(info, METHOD_VALUE_X, m);
      if(requestBody != null) throw error(info, ANN_BODYVAR);
      requestBody = checkVariable(toString(body), declared);
    }
    if(methods.contains(method)) throw error(info, ANN_TWICE_X_X, "%", method);
    methods.add(method);
  }

  /**
   * Checks if the consumed content type matches.
   * @param conn HTTP connection
   * @return result of check
   */
  private boolean consumes(final HTTPConnection conn) {
    // check if any combination matches
    final MediaType mt = conn.mediaType();
    for(final MediaType consume : consumes) {
      if(mt.matches(consume)) return true;
    }
    // return true if no type is given
    return consumes.isEmpty();
  }

  /**
   * 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 param 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 WebParam param, final Expr[] args, final Value value,
      final QueryContext qc) throws QueryException {
    bind(param.var, args, value == null || value.isEmpty() ? param.value : value, qc,
      "Value of \"" + param.name + '"');
  }

  /**
   * 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 list) {
    for(final Item arg : ann.value()) list.add(new MediaType(toString(arg)));
  }

  /**
   * Returns a parameter.
   * @param ann annotation
   * @param declared variable declaration flags
   * @return parameter
   * @throws QueryException query exception
   */
  private WebParam param(final Ann ann, final boolean... declared) throws QueryException {
    // name of parameter
    final Value value = ann.value();
    final String name = toString(value.itemAt(0));
    // variable template
    final QNm var = checkVariable(toString(value.itemAt(1)), declared);
    // default value
    final long al = value.size();
    final ItemList items = new ItemList(al - 2);
    for(int a = 2; a < al; a++) items.add(value.itemAt(a));
    return new WebParam(var, name, items.value());
  }

  /**
   * Creates an error function.
   * @param ann annotation
   * @throws QueryException query exception
   */
  private void error(final Ann ann) throws QueryException {
    if(error == null) error = new RestXqError();

    // name of parameter
    for(final Item arg : ann.value()) {
      final String err = toString(arg);
      final QNm name;
      final NamePart part;
      if(err.equals("*")) {
        name = null;
        part = null;
      } else if(err.startsWith("*:")) {
        final byte[] local = token(err.substring(2));
        if(!XMLToken.isNCName(local)) throw error(INV_CODE_X, err);
        name = new QNm(local);
        part = NamePart.LOCAL;
      } else if(err.endsWith(":*")) {
        final byte[] prefix = token(err.substring(0, err.length() - 2));
        if(!XMLToken.isNCName(prefix)) throw error(INV_CODE_X, err);
        name = new QNm(concat(prefix, cpToken(':')), function.sc);
        part = NamePart.URI;
      } else {
        final Matcher m = EQNAME.matcher(err);
        if(m.matches()) {
          final byte[] uri = token(m.group(1)), local = token(m.group(2));
          if(local.length == 1 && local[0] == '*') {
            name = new QNm(cpToken(':'), uri);
            part = NamePart.URI;
          } else {
            if(!XMLToken.isNCName(local) || !Uri.get(uri).isValid()) throw error(INV_CODE_X, err);
            name = new QNm(local, uri);
            part = NamePart.FULL;
          }
        } else {
          final byte[] nm = token(err);
          if(!XMLToken.isQName(nm)) throw error(INV_CODE_X, err);
          name = new QNm(nm, function.sc);
          part = NamePart.FULL;
        }
      }

      // message
      if(name != null && name.hasPrefix() && !name.hasURI()) throw error(INV_NONS_X, name);
      final NameTest test = part != null ? new NameTest(name, part, NodeType.ELEMENT, null) : null;

      final Function toString = t -> t != null ? t.toString() : "*";
      if(!error.isEmpty()) {
        final NameTest first = error.get(0);
        if(first != null ? first.part() != part : part != null) {
          throw error(INV_PRECEDENCE_X_X, toString.apply(first), toString.apply(test));
        }
      }
      if(!error.add(test)) throw error(INV_ERR_TWICE_X, toString.apply(test));
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy