package org.basex.query;
import static org.basex.core.Text.*;
import static org.basex.query.QueryError.*;
import static org.basex.util.Token.*;
import java.io.*;
import java.util.*;
import java.util.Map.*;
import org.basex.build.json.*;
import org.basex.build.json.JsonOptions.*;
import org.basex.core.*;
import org.basex.core.MainOptions.MainParser;
import org.basex.core.jobs.*;
import org.basex.core.locks.*;
import org.basex.core.users.*;
import org.basex.data.*;
import org.basex.io.parse.json.*;
import org.basex.io.serial.*;
import org.basex.query.expr.*;
import org.basex.query.expr.Expr.*;
import org.basex.query.func.*;
import org.basex.query.iter.*;
import org.basex.query.scope.*;
import org.basex.query.up.*;
import org.basex.query.util.collation.*;
import org.basex.query.util.ft.*;
import org.basex.query.util.list.*;
import org.basex.query.value.*;
import org.basex.query.value.item.*;
import org.basex.query.value.node.*;
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.ft.*;
import org.basex.util.hash.*;
import org.basex.util.list.*;
import org.basex.util.options.*;
/**
* This class organizes both static and dynamic properties that are specific to a
* single query.
*
* @author BaseX Team 2005-17, BSD License
* @author Christian Gruen
*/
public final class QueryContext extends Job implements Closeable {
/** The evaluation stack. */
public final QueryStack stack = new QueryStack();
/** Static variables. */
public final Variables vars = new Variables();
/** Functions. */
public final StaticFuncs funcs = new StaticFuncs();
/** Externally bound variables. */
private final HashMap<QNm, Value> bindings = new HashMap<>();
/** Parent query context. */
public final QueryContext parent;
/** Query info. */
public final QueryInfo info;
/** Database context. */
public final Context context;
/** Query resources. */
public QueryResources resources;
/** HTTP connection. */
public Object http;
/** Update container. */
public Updates updates;
/** Global database options (will be reassigned after query execution). */
final HashMap<Option<?>, Object> staticOpts = new HashMap<>();
/** Temporary query options (key/value pairs), supplied by option declarations. */
final StringList tempOpts = new StringList();
/** Current context value. */
public QueryFocus focus = new QueryFocus();
/** Full-text position data (needed for highlighting full-text results). */
public FTPosData ftPosData = Prop.gui ? new FTPosData() : null;
/** Current full-text lexer. */
public FTLexer ftLexer;
/** Current full-text options. */
private FTOpt ftOpt;
/** Full-text token positions (needed for highlighting full-text results). */
public int ftPos;
/** Scoring flag. */
public boolean scoring;
/** Current Date. */
public Dat date;
/** Current DateTime. */
public Dtm datm;
/** Current Time. */
public Tim time;
/** Current timezone. */
public DTDur zone;
/** Current nanoseconds. */
public long nano;
/** Available collations. */
public TokenObjMap<Collation> collations;
/** Strings to lock defined by read-lock option. */
public final LockList readLocks = new LockList();
/** Strings to lock defined by write-lock option. */
public final LockList writeLocks = new LockList();
/** Number of successive tail calls. */
public int tailCalls;
/** Maximum number of successive tail calls (will be set before compilation). */
public int maxCalls;
/** Function for the next tail call. */
private XQFunction tailFunc;
/** Arguments for the next tail call. */
private Value[] args;
/** Counter for variable IDs. */
public int varIDs;
/** Parsed modules, containing the file path and module uri. */
public final TokenMap modParsed = new TokenMap();
/** Pre-declared modules, containing module uri and their file paths (required for test APIs). */
final TokenMap modDeclared = new TokenMap();
/** Stack of module files that are currently parsed. */
final TokenList modStack = new TokenList();
/** Initial context value. */
public MainModule ctxItem;
/** Root expression of the query. */
public MainModule root;
/** Serialization parameters. */
private SerializerOptions serParams;
/** Indicates if the default serialization parameters are used. */
private boolean defaultOutput;
/** Indicates if the query has been compiled. */
private boolean compiled;
/** Indicates if the query context has been closed. */
private boolean closed;
/**
* Constructor.
* @param parent parent context
*/
public QueryContext(final QueryContext parent) {
this(parent.context, parent, parent.info);
parent.pushJob(this);
resources = parent.resources;
http = parent.http;
updates = parent.updates;
}
/**
* Constructor.
* @param context database context
*/
public QueryContext(final Context context) {
this(context, null, null);
resources = new QueryResources(this);
}
/**
* Constructor.
* @param context database context
* @param parent parent context (can be {@code null})
* @param info query info
*/
private QueryContext(final Context context, final QueryContext parent, final QueryInfo info) {
this.context = context;
this.parent = parent;
this.info = info != null ? info : new QueryInfo(this);
}
/**
* Parses the specified query.
* @param query query string
* @param uri base URI (may be {@code null})
* @param sc static context (may be {@code null})
* @return main module
* @throws QueryException query exception
*/
public Module parse(final String query, final String uri, final StaticContext sc)
throws QueryException {
return parse(query, QueryProcessor.isLibrary(query), uri, sc);
}
/**
* Parses the specified query.
* @param query query string
* @param library library/main module
* @param uri base URI (may be {@code null})
* @param sc static context (may be {@code null})
* @return main module
* @throws QueryException query exception
*/
public Module parse(final String query, final boolean library, final String uri,
final StaticContext sc) throws QueryException {
return library ? parseLibrary(query, uri, sc) : parseMain(query, uri, sc);
}
/**
* Parses the specified query.
* @param query query string
* @param uri base URI (may be {@code null})
* @param sc static context (may be {@code null})
* @return main module
* @throws QueryException query exception
*/
public MainModule parseMain(final String query, final String uri, final StaticContext sc)
throws QueryException {
info.query = query;
final QueryParser qp = new QueryParser(query, uri, this, sc);
root = qp.parseMain();
if(updating) updating = (qp.sc.mixUpdates && qp.sc.dynFuncCall) || root.expr.has(Flag.UPD);
return root;
}
/**
* Parses the specified module.
* @param query query string
* @param uri base URI (may be {@code null})
* @param sc static context (may be {@code null})
* @return name of module
* @throws QueryException query exception
*/
public LibraryModule parseLibrary(final String query, final String uri, final StaticContext sc)
throws QueryException {
info.query = query;
try {
return new QueryParser(query, uri, this, sc).parseLibrary(true);
} finally {
// library module itself is not updating
updating = false;
}
}
/**
* Sets the main module (root expression).
* @param rt main module
*/
public void mainModule(final MainModule rt) {
root = rt;
updating = rt.expr.has(Flag.UPD);
}
/**
* Compiles and optimizes the expression.
* @throws QueryException query exception
*/
public void compile() throws QueryException {
checkStop();
if(compiled) return;
final CompileContext cc = new CompileContext(this);
try {
// set database options
final StringList opts = tempOpts;
final int os = opts.size();
for(int o = 0; o < os; o += 2) {
final String key = opts.get(o), val = opts.get(o + 1);
try {
context.options.assign(key.toUpperCase(Locale.ENGLISH), val);
} catch(final BaseXException ex) {
throw BASX_VALUE_X_X.get(null, key, val);
}
}
// set tail call option after assignment database option
maxCalls = context.options.get(MainOptions.TAILCALLS);
// bind external variables
vars.bindExternal(this, bindings);
if(ctxItem != null) {
// evaluate initial expression
try {
ctxItem.comp(cc);
focus.value = ctxItem.cache(this).value();
} catch(final QueryException ex) {
// only {@link ParseExpr} instances may lead to a missing context
throw ex.error() == NOCTX_X ? CIRCCTX.get(ctxItem.info) : ex;
}
} else {
// cache the initial context nodes
final DBNodes nodes = context.current();
if(nodes != null) {
if(!context.perm(Perm.READ, nodes.data().meta.name))
throw BASX_PERM_X.get(null, Perm.READ);
focus.value = resources.compile(nodes);
}
}
// if specified, convert context value to specified type
// [LW] should not be necessary
if(focus.value != null && root.sc.contextType != null) {
focus.value = root.sc.contextType.promote(focus.value, null, this, root.sc, null, true);
}
try {
// compile the expression
if(root != null) QueryCompiler.compile(cc, root);
// compile global functions.
else funcs.compile(cc);
} catch(final StackOverflowError ex) {
Util.debug(ex);
throw BASX_STACKOVERFLOW.get(null, ex);
}
info.runtime = true;
} finally {
compiled = true;
}
}
/**
* Returns a result iterator.
* @return result iterator
* @throws QueryException query exception
*/
public Iter iter() throws QueryException {
compile();
try {
// no updates: iterate through results
if(!updating) return root.iter(this);
// cache results
ItemList results = root.cache(this);
// only perform updates if no parent context exists
if(updates != null && parent == null) {
// create copies of results that will be modified by an update operation
final ItemList cache = updates.cache;
final HashSet<Data> datas = updates.prepare(this);
final StringList dbs = updates.databases();
check(results, datas, dbs);
check(cache, datas, dbs);
// invalidate current node set in context, apply updates
if(context.data() != null) context.invalidate();
updates.apply(this);
// append cached outputs
if(!cache.isEmpty()) {
if(results.isEmpty()) results = cache;
else results.add(cache.value());
}
}
return results.iter();
} catch(final StackOverflowError ex) {
Util.debug(ex);
throw BASX_STACKOVERFLOW.get(null);
}
}
/**
* Checks the specified results, and replaces nodes with their copies if they will be
* affected by update operations.
* @param results node cache
* @param datas data references
* @param dbs database names
* @throws QueryException query exception
*/
private void check(final ItemList results, final HashSet<Data> datas, final StringList dbs)
throws QueryException {
final long cs = results.size();
for(int c = 0; c < cs; c++) {
final Item it = results.get(c);
// all updates are performed on database nodes
if(it instanceof FItem) throw BASX_FITEM_X.get(null, it);
final Data data = it.data();
if(data != null && (datas.contains(data) ||
!data.inMemory() && dbs.contains(data.meta.name))) {
results.set(c, ((DBNode) it).dbNodeCopy(context.options));
}
}
}
/**
* Evaluates the specified expression and returns an iterator.
* @param expr expression to be evaluated
* @return iterator
* @throws QueryException query exception
*/
public Iter iter(final Expr expr) throws QueryException {
checkStop();
return expr.iter(this);
}
/**
* Evaluates the specified expression and returns a value.
* @param expr expression to be evaluated
* @return value
* @throws QueryException query exception
*/
public Value value(final Expr expr) throws QueryException {
checkStop();
return expr.value(this);
}
/**
* Returns a reference to the updates container.
* @return updates container
*/
public synchronized Updates updates() {
if(updates == null) updates = new Updates(false);
return updates;
}
/**
* Returns the current data reference of the context value or {@code null}.
* @return data reference
*/
public Data data() {
return focus.value != null ? focus.value.data() : null;
}
@Override
public void addLocks() {
final Locks locks = jc().locks;
final LockList read = locks.reads, write = locks.writes;
read.add(readLocks);
write.add(writeLocks);
// use global locking if referenced databases cannot be statically determined
if(root == null || !root.databases(locks, this) ||
ctxItem != null && !ctxItem.databases(locks, this)) {
(updating ? write : read).addGlobal();
}
}
/**
* Binds the HTTP connection.
* @param val HTTP connection
*/
public void http(final Object val) {
http = val;
}
/**
* Binds the context value, using the same rules as for
* {@link #bind(String, Object, String, StaticContext) binding variables}.
* @param val value to be bound
* @param type type (may be {@code null})
* @param sc static context
* @throws QueryException query exception
*/
public void context(final Object val, final String type, final StaticContext sc)
throws QueryException {
context(cast(val, type), sc);
}
/**
* Binds the context value.
* @param val value to be bound
* @param sc static context
*/
public void context(final Value val, final StaticContext sc) {
ctxItem = MainModule.get(new VarScope(sc), val, null, null, null);
}
/**
* Binds a value to a global variable. The specified type is interpreted as follows:
* <ul>
* <li> If {@code "json"} is specified, the value is converted according to the rules
* specified in {@link JsonMapConverter}.</li>
* <li> Otherwise, the type is cast to the specified XDM type.</li>
* </ul>
* If the value is an XQuery {@link Value}, it is directly assigned.
* Otherwise, it is cast to the XQuery data model, using a Java/XQuery mapping.
* @param name name of variable
* @param val value to be bound
* @param type type (may be {@code null})
* @param sc static context
* @throws QueryException query exception
*/
public void bind(final String name, final Object val, final String type, final StaticContext sc)
throws QueryException {
bind(name, cast(val, type), sc);
}
/**
* Binds a value to a global variable.
* @param name name of variable
* @param val value to be bound
* @param sc static context
* @throws QueryException query exception
*/
public void bind(final String name, final Value val, final StaticContext sc)
throws QueryException {
final byte[] n = token(name);
bindings.put(QNm.resolve(indexOf(n, '$') == 0 ? substring(n, 1) : n, sc), val);
}
/**
* Adds some evaluation info.
* @param string evaluation info
*/
public void evalInfo(final String string) {
QueryContext qc = this;
while(qc.parent != null) qc = qc.parent;
qc.info.evalInfo(string);
}
/**
* Returns info on query compilation and evaluation.
* @return query info
*/
public String info() {
return info.toString(this);
}
/**
* Returns query-specific or default serialization parameters.
* @return serialization parameters
*/
public SerializerOptions serParams() {
if(serParams == null) {
serParams = new SerializerOptions(context.options.get(MainOptions.SERIALIZER));
defaultOutput = root != null;
}
return serParams;
}
/**
* Returns the current full-text options. Creates a new instance if called for the first time.
* @return full-text options
*/
public FTOpt ftOpt() {
if(ftOpt == null) ftOpt = new FTOpt();
return ftOpt;
}
/**
* Assigns full-text options.
* @param opt full-text options
*/
public void ftOpt(final FTOpt opt) {
ftOpt = opt;
}
/**
* Creates and returns an XML query plan (expression tree) for this query.
* @return query plan
*/
public FElem plan() {
// only show root node if functions or variables exist
final FElem e = new FElem(QueryText.QUERY_PLAN);
e.add(QueryText.COMPILED, token(compiled));
if(root != null) {
for(final StaticScope ss : QueryCompiler.usedDecls(root)) ss.plan(e);
root.plan(e);
} else {
funcs.plan(e);
vars.plan(e);
}
return e;
}
/**
* Indicates that the query contains updating expressions.
*/
public void updating() {
updating = true;
}
@Override
public void close() {
if(closed) return;
closed = true;
if(parent == null) {
// topmost query: close resources (opened by compile step)
resources.close();
} else {
// otherwise, adopt update reference (may have been initialized by sub query)
parent.updates = updates;
parent.popJob();
}
// reassign original database options (changed by compile step)
for(final Entry<Option<?>, Object> e : staticOpts.entrySet()) {
context.options.put(e.getKey(), e.getValue());
}
}
@Override
public String shortInfo() {
return SAVE;
}
// CLASS METHODS ======================================================================
/**
* 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 resulting value
* @throws QueryException query exception
*/
Value cache(final int max) throws QueryException {
final int mx = max >= 0 ? max : Integer.MAX_VALUE;
// evaluates the query
final Iter iter = iter();
final ItemList cache;
Item it;
// check if all results belong to the database of the input context
final Data data = resources.globalData();
if(defaultOutput && data != null) {
final IntList pres = new IntList();
while((it = iter.next()) != null && it.data() == data && pres.size() < mx) {
checkStop();
pres.add(((DBNode) it).pre());
}
// all results processed: return compact node sequence
final int ps = pres.size();
if(it == null || ps == mx) return new DBNodes(data, pres.finish()).ftpos(ftPosData);
// otherwise, add nodes to standard iterator
cache = new ItemList();
for(int p = 0; p < ps; p++) cache.add(new DBNode(data, pres.get(p)));
cache.add(it);
} else {
cache = new ItemList();
}
// use standard iterator
while((it = iter.next()) != null && cache.size() < mx) {
checkStop();
it.materialize(null);
cache.add(it);
}
return cache.value();
}
// PRIVATE METHODS ====================================================================
/**
* Casts a value to the specified type.
* See {@link #bind(String, Object, String, StaticContext)} for more infos.
* @param val value to be cast
* @param type type (may be {@code null})
* @return cast value
* @throws QueryException query exception
*/
private Value cast(final Object val, final String type) throws QueryException {
final StaticContext sc = root != null ? root.sc : new StaticContext(this);
// String input
Object vl = val;
if(vl instanceof String) {
final String string = (String) vl;
final StringList strings = new StringList(1);
// strings containing multiple items (value \1 ...)
if(string.indexOf('\1') == -1) {
strings.add(string);
} else {
strings.add(string.split("\1"));
vl = strings.toArray();
}
// sub types overriding the global value (value \2 type)
if(string.indexOf('\2') != -1) {
final ValueBuilder vb = new ValueBuilder();
for(final String str : strings) {
final int i = str.indexOf('\2');
final String s = i == -1 ? str : str.substring(0, i);
final String t = i == -1 ? type : str.substring(i + 1);
vb.add(cast(s, t));
}
return vb.value();
}
}
// no type specified: return original value or convert Java object
if(type == null || type.isEmpty()) {
return vl instanceof Value ? (Value) vl : JavaFunction.toValue(vl, this, sc);
}
// convert to json
if(type.equalsIgnoreCase(MainParser.JSON.name())) {
try {
final JsonParserOptions jp = new JsonParserOptions();
jp.set(JsonOptions.FORMAT, JsonFormat.MAP);
return JsonConverter.get(jp).convert(token(vl.toString()), null);
} catch(final QueryIOException ex) {
throw ex.getCause();
}
}
// test for empty sequence
if(type.equals(QueryText.EMPTY_SEQUENCE + "()")) return Empty.SEQ;
// convert to the specified type
// [LW] type should be parsed properly
final QNm nm = new QNm(token(type.replaceAll("\\(.*?\\)$", "")), sc);
if(!nm.hasURI() && nm.hasPrefix()) throw NOURI_X.get(null, nm.string());
Type tp;
if(type.endsWith(")")) {
if(nm.eq(AtomType.ITEM.name)) tp = AtomType.ITEM;
else tp = NodeType.find(nm);
if(tp == null) tp = FuncType.find(nm);
} else {
tp = ListType.find(nm);
if(tp == null) tp = AtomType.find(nm, false);
}
if(tp == null) throw WHICHTYPE_X.get(null, type);
// cast XDM values
if(vl instanceof Value) {
// cast single item
if(vl instanceof Item) return tp.cast((Item) vl, this, sc, null);
// cast sequence
final Value v = (Value) vl;
final ValueBuilder seq = new ValueBuilder();
for(final Item i : v) seq.add(tp.cast(i, this, sc, null));
return seq.value();
}
if(vl instanceof String[]) {
// cast string array
final String[] strings = (String[]) vl;
final ValueBuilder seq = new ValueBuilder();
for(final String s : strings) seq.add(tp.cast(s, this, sc, null));
return seq.value();
}
// cast any other object to XDM
return tp.cast(vl, this, sc, null);
}
/**
* Gets the value currently bound to the given variable.
* @param var variable
* @return bound value
*/
public Value get(final Var var) {
return stack.get(var);
}
/**
* Binds an expression to a local variable.
* @param var variable
* @param val expression to be bound
* @throws QueryException exception
*/
public void set(final Var var, final Value val) throws QueryException {
stack.set(var, val, this);
}
/**
* Registers a tail-called function and its arguments to this query context.
* @param fn function to call
* @param arg arguments to pass to {@code fn}
*/
public void registerTailCall(final XQFunction fn, final Value[] arg) {
tailFunc = fn;
args = arg;
}
/**
* Returns and clears the currently registered tail-call function.
* @return function to call if present, {@code null} otherwise
*/
public XQFunction pollTailCall() {
final XQFunction fn = tailFunc;
tailFunc = null;
return fn;
}
/**
* Returns and clears registered arguments of a tail-called function.
* @return argument values if a tail call was registered, {@code null} otherwise
*/
public Value[] pollTailArgs() {
final Value[] as = args;
args = null;
return as;
}
/**
* Initializes the static date and time context of a query if not done yet.
* @return self reference
* @throws QueryException query exception
*/
public QueryContext initDateTime() throws QueryException {
if(time == null) {
final Date dt = Calendar.getInstance().getTime();
final String ymd = DateTime.format(dt, DateTime.DATE);
final String hms = DateTime.format(dt, DateTime.TIME);
final String zon = DateTime.format(dt, DateTime.ZONE);
final String znm = zon.substring(0, 3), zns = zon.substring(3);
time = new Tim(token(hms + znm + ':' + zns), null);
date = new Dat(token(ymd + znm + ':' + zns), null);
datm = new Dtm(token(ymd + 'T' + hms + znm + ':' + zns), null);
zone = new DTDur(Strings.toInt(znm), Strings.toInt(zns));
nano = System.nanoTime();
}
return this;
}
@Override
public String toString() {
return root != null ? root.toString() : info.query;
}
}