package org.basex.query;
import static org.basex.query.QueryError.*;
import java.io.*;
import java.util.*;
import org.basex.build.*;
import org.basex.core.*;
import org.basex.core.cmd.*;
import org.basex.core.users.*;
import org.basex.data.*;
import org.basex.io.*;
import org.basex.query.util.pkg.*;
import org.basex.query.value.*;
import org.basex.query.value.node.*;
import org.basex.query.value.seq.*;
import org.basex.query.value.type.*;
import org.basex.util.*;
import org.basex.util.list.*;
/**
* This class provides access to all kinds of resources (databases, documents, database connections,
* sessions) used by an XQuery expression.
*
* @author BaseX Team 2005-17, BSD License
* @author Christian Gruen
*/
public final class QueryResources {
/** Database context. */
private final QueryContext qc;
/** Module loader. */
private ModuleLoader modules;
/** Collections: single nodes and sequences. */
private final ArrayList<Value> colls = new ArrayList<>(1);
/** Names of collections. */
private final ArrayList<String> collNames = new ArrayList<>(1);
/** Indicates if the first database in the context is globally opened. */
private boolean globalData;
/** Textual resources. Required for test APIs. */
private Map<String, String[]> texts;
/** Cached stop word files. Required for test APIs. */
private Map<String, IO> stop;
/** Cached thesaurus files. Required for test APIs. */
private Map<String, IO> thes;
/** Opened databases (both temporary and persistent ones). */
private final ArrayList<Data> datas = new ArrayList<>(1);
/** External resources. */
private Map<Class<? extends QueryResource>, QueryResource> external;
/**
* Constructor.
* @param qc query context
*/
QueryResources(final QueryContext qc) {
this.qc = qc;
}
/**
* Compiles the resources.
* @param nodes input node set
* @return context value
*/
Value compile(final DBNodes nodes) {
// add globally opened database
final Data data = nodes.data();
addData(data);
synchronized(qc.context.datas) { qc.context.datas.pin(data); }
globalData = true;
// create context value
final boolean all = nodes.all();
final Value value = DBNodeSeq.get(new IntList(nodes.pres()), data, all, all);
// add default collection. use initial node set if it contains all documents of the database.
// otherwise, create new node set
final Value coll = all ? value : DBNodeSeq.get(data.resources.docs(), data, true, true);
addCollection(coll, data.meta.name);
return value;
}
/**
* Closes all opened data references that have not been added by the global context.
*/
void close() {
for(final Data data : datas) Close.close(data, qc.context);
datas.clear();
// close dynamically loaded JAR files
if(modules != null) modules.close();
// close external resources
if(external != null) {
for(final QueryResource c : external.values()) c.close();
}
}
/**
* Returns the globally opened database.
* @return database or {@code null} if no database is globally opened
*/
Data globalData() {
return globalData ? datas.get(0) : null;
}
/**
* Returns or creates an external resource of the specified class.
* @param <R> resource
* @param resource external resource
* @return resource
*/
@SuppressWarnings("unchecked")
public synchronized <R extends QueryResource> R index(final Class<? extends R> resource) {
if(external == null) external = new HashMap<>();
QueryResource value = external.get(resource);
if(value == null) {
try {
value = resource.newInstance();
external.put(resource, value);
} catch(final Throwable ex) {
throw Util.notExpected(ex);
}
}
return (R) value;
}
/**
* Opens a new database or returns a reference to an already opened database.
* @param name name of database
* @param info input info
* @return database instance
* @throws QueryException query exception
*/
public synchronized Data database(final String name, final InputInfo info) throws QueryException {
final Context ctx = qc.context;
final boolean mainmem = ctx.options.get(MainOptions.MAINMEM);
// check if a database with the same name has already been opened
for(final Data data : datas) {
// default mode: skip main-memory database instances (which may result from fn:doc calls)
if(data.inMemory() && !mainmem) continue;
final String n = data.meta.name;
if(Prop.CASE ? n.equals(name) : n.equalsIgnoreCase(name)) return data;
}
// open and register database
try {
return addData(Open.open(name, ctx, ctx.options));
} catch(final IOException ex) {
throw BXDB_OPEN_X.get(info, ex);
}
}
/**
* Evaluates {@code fn:doc()}: opens an existing database document, or creates a new
* database and node.
* @param qi query input
* @param info input info
* @return document
* @throws QueryException query exception
*/
public synchronized DBNode doc(final QueryInput qi, final InputInfo info) throws QueryException {
// favor default database
Data data = globalData();
if(data != null && qc.context.options.get(MainOptions.DEFAULTDB)) {
final int pre = data.resources.doc(qi.original);
if(pre != -1) return new DBNode(data, pre, Data.DOC);
}
// access open database or create new one
data = data(qi, info, true);
// ensure that database contains a single document
final IntList docs = data.resources.docs(qi.dbPath);
if(docs.size() == 1) return new DBNode(data, docs.get(0), Data.DOC);
throw (docs.isEmpty() ? BXDB_NODOC_X : BXDB_SINGLE_X).get(info, qi.original);
}
/**
* Evaluates {@code fn:collection()}: opens an existing collection,
* or creates a new data reference.
* @param qi query input (set to {@code null} if default collection is requested)
* @param info input info
* @return collection
* @throws QueryException query exception
*/
public synchronized Value collection(final QueryInput qi, final InputInfo info)
throws QueryException {
// return default collection
if(qi == null) {
if(colls.isEmpty()) throw NODEFCOLL.get(info);
return colls.get(0);
}
// favor default database
Data data = globalData();
if(data != null && qc.context.options.get(MainOptions.DEFAULTDB)) {
final IntList pres = data.resources.docs(qi.original);
return DBNodeSeq.get(pres, data, true, qi.original.isEmpty());
}
// check currently opened collections (required for tests)
final int cs = colls.size();
for(int c = 0; c < cs; c++) {
final String name = collNames.get(c), path = qi.io.path();
if(Prop.CASE ? name.equals(path) : name.equalsIgnoreCase(path)) {
return colls.get(c);
}
}
// access open database or create new one
data = data(qi, info, false);
final IntList docs = data.resources.docs(qi.dbPath);
return DBNodeSeq.get(docs, data, true, qi.dbPath.isEmpty());
}
/**
* Returns the module loader. Called during parsing.
* @return module loader
*/
public ModuleLoader modules() {
if(modules == null) modules = new ModuleLoader(qc.context);
return modules;
}
/**
* Removes and closes a database. Called during updates.
* @param name name of database to be removed
*/
public void remove(final String name) {
final int ds = datas.size();
for(int d = globalData ? 1 : 0; d < ds; d++) {
final Data data = datas.get(d);
if(data.meta.name.equals(name)) {
Close.close(data, qc.context);
datas.remove(d);
break;
}
}
}
/**
* Returns the document path of a textual resource and its encoding. Only required for test APIs.
* @param uri resource uri
* @return path and encoding, or {@code null}
*/
public String[] text(final String uri) {
return texts == null ? null : texts.get(uri);
}
/**
* Returns stop words. Called during parsing, and only required for test APIs.
* @param path resource path
* @param sc static context
* @return file reference
*/
public IO stopWords(final String path, final StaticContext sc) {
return stop != null ? stop.get(path) : sc.resolve(path, null);
}
/**
* Returns a thesaurus file. Called during parsing, and only required for Test APIs.
* @param path resource path
* @param sc static context
* @return file reference
*/
public IO thesaurus(final String path, final StaticContext sc) {
return thes != null ? thes.get(path) : sc.resolve(path, null);
}
// TEST APIS ====================================================================================
/**
* Adds a document with the specified path. Only called from the test APIs.
* @param name document identifier (may be {@code null})
* @param path document path
* @param sc static context (can be {@code null})
* @throws QueryException query exception
*/
public void addDoc(final String name, final String path, final StaticContext sc)
throws QueryException {
final QueryInput qi = new QueryInput(path, sc);
final Data data = create(qi, true, null);
if(name != null) data.meta.original = name;
}
/**
* Adds a resource with the specified path. Only called from the test APIs.
* @param uri resource uri
* @param strings resource strings (path, encoding)
*/
public void addResource(final String uri, final String... strings) {
if(texts == null) texts = new HashMap<>();
texts.put(uri, strings);
}
/**
* Adds a collection with the specified paths. Only called from the test APIs.
* @param name name of collection (can be empty string)
* @param paths documents paths
* @param sc static context (can be {@code null})
* @throws QueryException query exception
*/
public void addCollection(final String name, final String[] paths, final StaticContext sc)
throws QueryException {
final int ns = paths.length;
final DBNode[] nodes = new DBNode[ns];
for(int n = 0; n < ns; n++) {
final QueryInput qi = new QueryInput(paths[n], sc);
nodes[n] = new DBNode(create(qi, true, null), 0, Data.DOC);
}
addCollection(ValueBuilder.value(nodes, ns, NodeType.DOC), name);
}
/**
* Attaches full-text maps. Only called from the test APIs.
* @param sw stop words
* @param th thesaurus
*/
public void ftmaps(final HashMap<String, IO> sw, final HashMap<String, IO> th) {
stop = sw;
thes = th;
}
// PRIVATE METHODS ==============================================================================
/**
* Returns an already open database for the specified input or creates a new one.
* @param qi query input
* @param info input info
* @param single single document
* @return document
* @throws QueryException query exception
*/
private Data data(final QueryInput qi, final InputInfo info, final boolean single)
throws QueryException {
// check opened databases
for(final Data data : datas) {
// compare input path
final String orig = data.meta.original;
if(!orig.isEmpty() && IO.get(orig).eq(qi.io)) {
// reset database path: indicates that database includes all files of the original path
qi.dbPath = "";
return data;
}
// compare database name
final String name = data.meta.name, dbName = qi.dbName;
if(Prop.CASE ? name.equals(dbName) : name.equalsIgnoreCase(dbName)) return data;
}
// open new database
Data data = open(qi);
if(data != null) return data;
// otherwise, create new instance
data = create(qi, single, info);
// reset database path: indicates that all documents were parsed
qi.dbPath = "";
return data;
}
/**
* Tries to open the addressed database, or returns {@code null}.
* @param input query input
* @return data reference
*/
private Data open(final QueryInput input) {
final String dbName = input.dbName;
if(dbName != null) {
try {
final Context ctx = qc.context;
return addData(Open.open(dbName, ctx, ctx.options));
} catch(final IOException ex) {
Util.debug(ex);
}
}
return null;
}
/**
* Creates a new database instance.
* @param input query input
* @param single expect single document
* @param ii input info
* @return data reference
* @throws QueryException query exception
*/
private Data create(final QueryInput input, final boolean single, final InputInfo ii)
throws QueryException {
// check if new databases can be created
final Context context = qc.context;
// do not check for existence of input if user has no read permissions
if(!context.user().has(Perm.READ))
throw BXXQ_PERM_X.get(ii, Util.info(Text.PERM_REQUIRED_X, Perm.READ));
// check if input points to a single file
final IO io = input.io;
if(!io.exists()) throw WHICHRES_X.get(ii, io);
if(single && io.isDir()) throw RESDIR_X.get(ii, io);
// overwrite parsing options with default values
final boolean mem = !context.options.get(MainOptions.FORCECREATE);
final MainOptions opts = new MainOptions(context.options, true);
final Parser parser = new DirParser(io, opts);
final Data data;
try {
data = CreateDB.create(io.dbName(), parser, context, opts, mem);
} catch(final IOException ex) {
throw IOERR_X.get(ii, ex);
}
return addData(data);
}
/**
* Adds a data reference.
* @param data data reference to be added
* @return argument
*/
private Data addData(final Data data) {
datas.add(data);
return data;
}
/**
* Adds a collection to the global collection list.
* @param coll documents of collection
* @param name collection name (can be empty string)
*/
private void addCollection(final Value coll, final String name) {
colls.add(coll);
collNames.add(name);
}
}