@Override public final Expr compile(final QueryContext qc, final VarScope scp) throws QueryException { if (root != null) root = root.compile(qc, scp); // no steps if (steps.length == 0) return root == null ? new Context(info) : root; final Value init = qc.value, cv = initial(qc); final boolean doc = cv != null && cv.type == NodeType.DOC; qc.value = cv; try { final int sl = steps.length; for (int s = 0; s < sl; s++) { Expr e = steps[s]; // axis step: if input is a document, its type is temporarily generalized final boolean as = e instanceof Step; if (as && s == 0 && doc) cv.type = NodeType.NOD; e = e.compile(qc, scp); if (e.isEmpty()) return optPre(qc); steps[s] = e; // no axis step: invalidate context value if (!as) qc.value = null; } } finally { if (doc) cv.type = NodeType.DOC; qc.value = init; } // optimize path return optimize(qc, scp); }
@Override public final Expr optimize(final QueryContext qc, final VarScope scp) throws QueryException { final Value v = initial(qc); if (v != null && v.isEmpty() || emptyPath(v)) return optPre(qc); // merge descendant steps Expr e = mergeSteps(qc); if (e == this && v != null && v.type == NodeType.DOC) { // check index access e = index(qc, v); // rewrite descendant to child steps if (e == this) e = children(qc, v); } // recompile path if it has changed if (e != this) return e.compile(qc, scp); // set atomic type for single attribute steps to speed up predicate tests if (root == null && steps.length == 1 && steps[0] instanceof Step) { final Step curr = (Step) steps[0]; if (curr.axis == ATTR && curr.test.kind == Kind.URI_NAME) curr.seqType(SeqType.NOD_ZO); } // choose best path implementation and set type information final Path path = get(info, root, steps); path.size = path.size(qc); path.seqType = SeqType.get(steps[steps.length - 1].seqType().type, size); return path; }
/** * Computes the number of results. * * @param qc query context (may be @code null) * @return number of results */ private long size(final QueryContext qc) { final Value rt = initial(qc); // skip computation if value is not a document node if (rt == null || rt.type != NodeType.DOC) return -1; final Data data = rt.data(); // skip computation if no database instance is available, is out-of-date or // if context does not contain all database nodes if (data == null || !data.meta.uptodate || data.resources.docs().size() != rt.size()) return -1; ArrayList<PathNode> nodes = data.paths.root(); long m = 1; final int sl = steps.length; for (int s = 0; s < sl; s++) { final Step curr = axisStep(s); if (curr != null) { nodes = curr.nodes(nodes, data); if (nodes == null) return -1; } else if (s + 1 == sl) { m = steps[s].size(); } else { // stop if a non-axis step is not placed last return -1; } } long sz = 0; for (final PathNode pn : nodes) sz += pn.stats.count; return sz * m; }
/** * Test sending of HTTP GET requests. * * @throws Exception exception */ @Test public void postGet() throws Exception { // GET1 - just send a GET request try (final QueryProcessor qp = new QueryProcessor( _HTTP_SEND_REQUEST.args("<http:request method='get' href='" + REST_ROOT + "'/>"), ctx)) { final Value v = qp.value(); checkResponse(v, 2, HttpURLConnection.HTTP_OK); assertEquals(NodeType.DOC, v.itemAt(1).type); } // GET2 - with override-media-type='text/plain' try (final QueryProcessor qp = new QueryProcessor( _HTTP_SEND_REQUEST.args( "<http:request method='get' override-media-type='text/plain'/>", REST_ROOT), ctx)) { final Value v = qp.value(); checkResponse(v, 2, HttpURLConnection.HTTP_OK); assertEquals(AtomType.STR, v.itemAt(1).type); } // Get3 - with status-only='true' try (final QueryProcessor qp = new QueryProcessor( _HTTP_SEND_REQUEST.args("<http:request method='get' status-only='true'/>", REST_ROOT), ctx)) { checkResponse(qp.value(), 1, HttpURLConnection.HTTP_OK); } }
/** * Checks the response to an HTTP request. * * @param v query result * @param itemsCount expected number of items * @param expStatus expected status */ private static void checkResponse(final Value v, final int itemsCount, final int expStatus) { assertEquals(itemsCount, v.size()); assertTrue(v.itemAt(0) instanceof FElem); final FElem response = (FElem) v.itemAt(0); assertNotNull(response.attributes()); if (!eq(response.attribute(STATUS), token(expStatus))) { fail("Expected: " + expStatus + "\nFound: " + response); } }
/** * Returns a string representation of all found arguments. * * @param args array with arguments * @return string representation */ static String foundArgs(final Value[] args) { // compose found arguments final StringBuilder sb = new StringBuilder(); for (final Value v : args) { if (sb.length() != 0) sb.append(", "); sb.append(v instanceof Jav ? Util.className(((Jav) v).toJava()) : v.seqType()); } return sb.toString(); }
/** * Returns the root of the current context or {@code null}. * * @param ctx query context * @return root */ final Value root(final QueryContext ctx) { final Value v = ctx != null ? ctx.value : null; // no root specified: return context, if it does not reference a document // as e.g. happens in //a(b|c) if (root == null) return v == null || v.type != NodeType.DOC ? v : null; // root is value: return root if (root.isValue()) return (Value) root; // no root reference, no context: return null if (!(root instanceof Root) || v == null) return null; // return context sequence or root of current context return v.size() == 1 ? Root.root(v) : v; }
@Override public Expr compile(final QueryContext qc, final VarScope scp) throws QueryException { final Value init = qc.value; // never compile predicates with empty sequence as context value (#1016) if (init != null && init.isEmpty()) qc.value = null; try { final int pl = preds.length; for (int p = 0; p < pl; ++p) preds[p] = preds[p].compile(qc, scp).optimizeEbv(qc, scp); return this; } finally { qc.value = init; } }
/** Tests the specified instance. */ @Test public void test() { final StringBuilder sb = new StringBuilder(); int fail = 0; for (final Object[] qu : queries) { final boolean correct = qu.length == 3; final String query = qu[correct ? 2 : 1].toString(); final Value cmp = correct ? (Value) qu[1] : null; final QueryProcessor qp = new QueryProcessor(query, context); try { final Value val = qp.value(); if (!correct || !new DeepCompare().equal(val, cmp)) { sb.append("[" + qu[0] + "] " + query); String s = correct && cmp.size() != 1 ? "#" + cmp.size() : ""; sb.append("\n[E" + s + "] "); if (correct) { final String cp = cmp.toString(); sb.append('\''); sb.append(cp.length() > 1000 ? cp.substring(0, 1000) + "..." : cp); sb.append('\''); } else { sb.append("error"); } final TokenBuilder types = new TokenBuilder(); for (final Item it : val) types.add(it.type.toString()).add(" "); s = val.size() == 1 ? "" : "#" + val.size(); sb.append("\n[F" + s + "] '" + val + "', " + types + details() + '\n'); ++fail; } } catch (final Exception ex) { final String msg = ex.getMessage(); if (correct || msg == null || msg.contains("mailman")) { final String cp = correct && cmp.data() != null ? cmp.toString() : "()"; sb.append( "[" + qu[0] + "] " + query + "\n[E] " + cp + "\n[F] " + (msg == null ? Util.className(ex) : msg.replaceAll("\r\n?|\n", " ")) + ' ' + details() + '\n'); ex.printStackTrace(); ++fail; } } finally { qp.close(); } } if (fail != 0) fail(fail + " Errors. [E] = expected, [F] = found:\n" + sb.toString().trim()); }
/** * 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()); }
/** * Returns the path nodes that will result from this path. * * @param qc query context * @return path nodes, or {@code null} if nodes cannot be evaluated */ public final ArrayList<PathNode> pathNodes(final QueryContext qc) { final Value init = initial(qc); final Data data = init != null && init.type == NodeType.DOC ? init.data() : null; if (data == null || !data.meta.uptodate) return null; ArrayList<PathNode> nodes = data.paths.root(); final int sl = steps.length; for (int s = 0; s < sl; s++) { final Step curr = axisStep(s); if (curr == null) return null; nodes = curr.nodes(nodes, data); if (nodes == null) return null; } return nodes; }
/** * Returns a document test. This test will be called by {@link AxisPath#index} if the context * value only consists of database nodes. * * @param rt root value * @return document test */ static Test get(final Value rt) { // use simple test if database contains only one document final Data data = rt.data(); if (data.meta.ndocs == 1) return Test.DOC; // adopt nodes from existing sequence if (rt instanceof DBNodeSeq) { final DBNodeSeq seq = (DBNodeSeq) rt; return seq.all() ? Test.DOC : new InvDocTest(new IntList(seq.pres()), data); } // loop through all documents and add pre values of documents // not more than 2^31 documents supported final IntList il = new IntList((int) rt.size()); for (final Item it : rt) il.add(((DBNode) it).pre()); return new InvDocTest(il, data); }
/** * Converts descendant to child steps. * * @param qc query context * @param rt root value * @return original or new expression */ private Expr children(final QueryContext qc, final Value rt) { // skip if index does not exist or is out-dated, or if several namespaces occur in the input final Data data = rt.data(); if (data == null || !data.meta.uptodate || data.nspaces.globalNS() == null) return this; Path path = this; final int sl = steps.length; for (int s = 0; s < sl; s++) { // don't allow predicates in preceding location steps final Step prev = s > 0 ? axisStep(s - 1) : null; if (prev != null && prev.preds.length != 0) break; // ignore axes other than descendant, or numeric predicates final Step curr = axisStep(s); if (curr == null || curr.axis != DESC || curr.has(Flag.FCS)) continue; // check if child steps can be retrieved for current step ArrayList<PathNode> nodes = pathNodes(data, s); if (nodes == null) continue; // cache child steps final ArrayList<QNm> qnm = new ArrayList<>(); while (nodes.get(0).parent != null) { QNm nm = new QNm(data.elemNames.key(nodes.get(0).name)); // skip children with prefixes if (nm.hasPrefix()) return this; for (final PathNode p : nodes) { if (nodes.get(0).name != p.name) nm = null; } qnm.add(nm); nodes = PathSummary.parent(nodes); } qc.compInfo(OPTCHILD, steps[s]); // build new steps int ts = qnm.size(); final Expr[] stps = new Expr[ts + sl - s - 1]; for (int t = 0; t < ts; t++) { final Expr[] preds = t == ts - 1 ? ((Preds) steps[s]).preds : new Expr[0]; final QNm nm = qnm.get(ts - t - 1); final NameTest nt = nm == null ? new NameTest(false) : new NameTest(nm, Kind.NAME, false, null); stps[t] = Step.get(info, CHILD, nt, preds); } while (++s < sl) stps[ts++] = steps[s]; path = get(info, root, stps); break; } // check if all steps yield results; if not, return empty sequence final ArrayList<PathNode> nodes = pathNodes(qc); if (nodes != null && nodes.isEmpty()) { qc.compInfo(OPTPATH, path); return Empty.SEQ; } return path; }
@Override public Value value(final QueryContext qc) throws QueryException { final Value val = qc.value(exprs[0]); if (val.isEmpty()) throw ONEORMORE.get(info); return val; }
/** * 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; }
/** * Returns an equivalent expression which accesses an index. If the expression cannot be * rewritten, the original expression is returned. * * <p>The following types of queries can be rewritten (in the examples, the equality comparison is * used, which will be rewritten to {@link ValueAccess} instances): * * <pre> * 1. A[text() = '...'] -> IA('...') * 2. A[. = '...'] -> IA('...', A) * 3. text()[. = '...'] -> IA('...') * 4. A[B = '...'] -> IA('...', B)/parent::A * 1. A[B/text() = '...'] -> IA('...')/parent::B/parent::A * 2. A[B/C = '...'] -> IA('...', C)/parent::B/parent::A * 7. A[@a = '...'] -> IA('...', @a)/parent::A * 8. @a[. = '...'] -> IA('...', @a)</pre> * * Queries of type 1, 3, 5 will not yield any results if the string to be compared is empty. * * @param qc query context * @param rt root value * @return original or new expression * @throws QueryException query exception */ private Expr index(final QueryContext qc, final Value rt) throws QueryException { // only rewrite paths with data reference final Data data = rt.data(); if (data == null) return this; // cache index access costs IndexInfo index = null; // cheapest predicate and step int iPred = 0, iStep = 0; // check if path can be converted to an index access final int sl = steps.length; for (int s = 0; s < sl; s++) { // only accept descendant steps without positional predicates final Step step = axisStep(s); if (step == null || !step.axis.down || step.has(Flag.FCS)) break; // check if path is iterable (i.e., will be duplicate-free) final boolean iter = pathNodes(data, s) != null; final IndexContext ictx = new IndexContext(data, iter); // choose cheapest index access final int pl = step.preds.length; for (int p = 0; p < pl; p++) { final IndexInfo ii = new IndexInfo(ictx, qc, step); if (!step.preds[p].indexAccessible(ii)) continue; if (ii.costs == 0) { // no results... qc.compInfo(OPTNOINDEX, this); return Empty.SEQ; } if (index == null || index.costs > ii.costs) { index = ii; iPred = p; iStep = s; } } } // skip rewriting if no index access is possible, or if it is too expensive if (index == null || index.costs > data.meta.size) return this; // rewrite for index access qc.compInfo(index.info); // replace expressions for index access final Step indexStep = index.step; // collect remaining predicates final int pl = indexStep.preds.length; final ExprList newPreds = new ExprList(pl - 1); for (int p = 0; p < pl; p++) { if (p != iPred) newPreds.add(indexStep.preds[p]); } // invert steps that occur before index step and add them as predicate final Test test = InvDocTest.get(rt); final ExprList invSteps = new ExprList(); if (test != Test.DOC || !data.meta.uptodate || predSteps(data, iStep)) { for (int s = iStep; s >= 0; s--) { final Axis ax = axisStep(s).axis.invert(); if (s == 0) { // add document test for collections and axes other than ancestors if (test != Test.DOC || ax != Axis.ANC && ax != Axis.ANCORSELF) invSteps.add(Step.get(info, ax, test)); } else { final Step prev = axisStep(s - 1); invSteps.add(Step.get(info, ax, prev.test, prev.preds)); } } } if (!invSteps.isEmpty()) newPreds.add(get(info, null, invSteps.finish())); // create resulting expression final ExprList resultSteps = new ExprList(); final Expr resultRoot; if (index.expr instanceof Path) { final Path p = (Path) index.expr; resultRoot = p.root; resultSteps.add(p.steps); } else { resultRoot = index.expr; } if (!newPreds.isEmpty()) { int ls = resultSteps.size() - 1; Step step; if (ls < 0 || !(resultSteps.get(ls) instanceof Step)) { // add at least one self axis step step = Step.get(info, Axis.SELF, Test.NOD); ls++; } else { step = (Step) resultSteps.get(ls); } // add remaining predicates to last step resultSteps.set(ls, step.addPreds(newPreds.finish())); } // add remaining steps for (int s = iStep + 1; s < sl; s++) resultSteps.add(steps[s]); return get(info, resultRoot, resultSteps.finish()); }
/** * 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)); }