package org.basex.query.func; import static org.basex.data.DataText.*; import static org.basex.query.util.Err.*; import static org.basex.util.Token.*; import java.io.IOException; import java.util.Date; import org.basex.core.Prop; import org.basex.core.User; import org.basex.core.cmd.Info; import org.basex.core.cmd.InfoDB; import org.basex.core.cmd.List; import org.basex.core.cmd.Rename; import org.basex.data.Data; import org.basex.data.MetaData; import org.basex.index.IndexToken.IndexType; import org.basex.index.Resources; import org.basex.io.IO; import org.basex.io.IOFile; import org.basex.io.MimeTypes; import org.basex.io.in.DataInput; import org.basex.io.out.ArrayOutput; import org.basex.io.serial.Serializer; import org.basex.io.serial.SerializerException; import org.basex.query.QueryContext; import org.basex.query.QueryException; import org.basex.query.expr.Expr; import org.basex.query.expr.IndexAccess; import org.basex.query.item.ANode; import org.basex.query.item.B64Stream; import org.basex.query.item.Bln; import org.basex.query.item.DBNode; import org.basex.query.item.DBNodeSeq; import org.basex.query.item.Empty; import org.basex.query.item.FAttr; import org.basex.query.item.FElem; import org.basex.query.item.FNode; import org.basex.query.item.FTxt; import org.basex.query.item.Int; import org.basex.query.item.Item; import org.basex.query.item.QNm; import org.basex.query.item.Str; import org.basex.query.item.Value; import org.basex.query.iter.Iter; import org.basex.query.iter.NodeIter; import org.basex.query.iter.ValueIter; import org.basex.query.path.NameTest; import org.basex.query.up.primitives.DBAdd; import org.basex.query.up.primitives.DBDelete; import org.basex.query.up.primitives.DBOptimize; import org.basex.query.up.primitives.DBRename; import org.basex.query.up.primitives.DBStore; import org.basex.query.up.primitives.DeleteNode; import org.basex.query.up.primitives.ReplaceValue; import org.basex.query.util.IndexContext; import org.basex.util.InputInfo; import org.basex.util.list.IntList; import org.basex.util.list.StringList; import org.basex.util.list.TokenList; /** * Database functions. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen * @author Dimitar Popov */ public final class FNDb extends StandardFunc { /** Resource element name. */ static final QNm SYSTEM = new QNm(token("system")); /** Resource element name. */ static final QNm DATABASE = new QNm(token("database")); /** Resource element name. */ static final QNm RESOURCE = new QNm(token("resource")); /** Resource element name. */ static final QNm RESOURCES = new QNm(token("resources")); /** Path element name. */ static final QNm PATH = new QNm(token("path")); /** Raw element name. */ static final QNm RAW = new QNm(token("raw")); /** Size element name. */ static final QNm SIZE = new QNm(token("size")); /** Content type element name. */ static final QNm CTYPE = new QNm(token("content-type")); /** Modified date element name. */ static final QNm MDATE = new QNm(token("modified-date")); /** MIME type application/xml. */ static final byte[] APP_XML = token(MimeTypes.APP_XML); /** * Constructor. * @param ii input info * @param f function definition * @param e arguments */ public FNDb(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 _DB_OPEN: return open(ctx).iter(); case _DB_TEXT: return text(ctx); case _DB_ATTRIBUTE: return attribute(ctx); case _DB_FULLTEXT: return fulltext(ctx); case _DB_LIST: return list(ctx); case _DB_LIST_DETAILS: return listDetails(ctx); case _DB_NODE_ID: return node(ctx, true); case _DB_NODE_PRE: return node(ctx, false); default: return super.iter(ctx); } } @Override public Value value(final QueryContext ctx) throws QueryException { switch(sig) { case _DB_OPEN: return open(ctx); default: return super.value(ctx); } } @Override public Item item(final QueryContext ctx, final InputInfo ii) throws QueryException { switch(sig) { case _DB_EVENT: return event(ctx); case _DB_OPEN_ID: return open(ctx, true); case _DB_OPEN_PRE: return open(ctx, false); case _DB_SYSTEM: return system(ctx); case _DB_INFO: return info(ctx); case _DB_ADD: return add(ctx); case _DB_DELETE: return delete(ctx); case _DB_RENAME: return rename(ctx); case _DB_REPLACE: return replace(ctx); case _DB_OPTIMIZE: return optimize(ctx); case _DB_STORE: return store(ctx); case _DB_RETRIEVE: return retrieve(ctx); case _DB_IS_RAW: return isRaw(ctx); case _DB_EXISTS: return exists(ctx); case _DB_IS_XML: return isXML(ctx); case _DB_CONTENT_TYPE: return contentType(ctx); default: return super.item(ctx, ii); } } /** * Performs the open function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Value open(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final String path = expr.length < 2 ? "" : path(1, ctx); return DBNodeSeq.get(data.resources.docs(path), data, true, path.isEmpty()); } /** * Performs the open-id and open-pre function. * @param ctx query context * @param id id flag * @return result * @throws QueryException query exception */ private DBNode open(final QueryContext ctx, final boolean id) throws QueryException { final Data data = data(0, ctx); final int v = (int) checkItr(expr[1], ctx); final int pre = id ? data.pre(v) : v; if(pre < 0 || pre >= data.meta.size) IDINVALID.thrw(input, this, v); return new DBNode(data, pre); } /** * Performs the text function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Iter text(final QueryContext ctx) throws QueryException { final IndexContext ic = new IndexContext(ctx, data(0, ctx), null, true); return new IndexAccess(input, expr[1], IndexType.TEXT, ic).iter(ctx); } /** * Performs the attribute function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Iter attribute(final QueryContext ctx) throws QueryException { final IndexContext ic = new IndexContext(ctx, data(0, ctx), null, true); final IndexAccess ia = new IndexAccess( input, expr[1], IndexType.ATTRIBUTE, ic); // return iterator if no name test is specified if(expr.length < 3) return ia.iter(ctx); // parse and compile the name test final Item name = checkNoEmpty(expr[2].item(ctx, input)); final QNm nm = new QNm(checkStr(name, ctx), ctx); if(!nm.hasPrefix()) nm.uri(ctx.sc.ns.uri(EMPTY)); final NameTest nt = new NameTest(nm, NameTest.Name.STD, true); // no results expected: return empty sequence if(!nt.comp(ctx)) return Empty.ITER; // wrap iterator with name test return new Iter() { final NodeIter ir = ia.iter(ctx); @Override public ANode next() throws QueryException { ANode n; while((n = ir.next()) != null && !nt.eval(n)); return n; } }; } /** * Performs the fulltext function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Iter fulltext(final QueryContext ctx) throws QueryException { return FNFt.search(data(0, ctx), checkStr(expr[1], ctx), this, ctx); } /** * Performs the list function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Iter list(final QueryContext ctx) throws QueryException { final TokenList tl = new TokenList(); final int el = expr.length; if(el == 0) { for(final String s : List.list(ctx.context)) tl.add(s); } else { final Data data = data(0, ctx); final String path = string(el == 1 ? EMPTY : checkStr(expr[1], ctx)); // add xml resources final Resources res = data.resources; final IntList il = res.docs(path); final int is = il.size(); for(int i = 0; i < is; i++) tl.add(data.text(il.get(i), true)); // add binary resources for(final byte[] file : res.binaries(path)) tl.add(file); } tl.sort(!Prop.WIN); return new Iter() { int pos; @Override public Str get(final long i) { return Str.get(tl.get((int) i)); } @Override public Str next() { return pos < size() ? get(pos++) : null; } @Override public boolean reset() { pos = 0; return true; } @Override public long size() { return tl.size(); } }; } /** * Performs the list-details function. * @param ctx query context * @return iterator * @throws QueryException query exception */ private Iter listDetails(final QueryContext ctx) throws QueryException { if(expr.length == 0) return listDBs(ctx); final Data data = data(0, ctx); final String path = string(expr.length == 1 ? EMPTY : checkStr(expr[1], ctx)); final IntList il = data.resources.docs(path); final TokenList tl = data.resources.binaries(path); return new Iter() { final int is = il.size(), ts = tl.size(); int ip, tp; @Override public ANode get(final long i) throws QueryException { if(i < is) { final byte[] pt = data.text(il.get((int) i), true); return resource(pt, false, 0, APP_XML, data.meta.time); } if(i < is + ts) { final byte[] pt = tl.get((int) i - is); final IO io = data.meta.binary(string(pt)); return resource(pt, true, io.length(), token(MimeTypes.get(io.path())), io.timeStamp()); } return null; } @Override public ANode next() throws QueryException { return ip < is ? get(ip++) : tp < ts ? get(ip + tp++) : null; } @Override public boolean reset() { ip = 0; tp = 0; return true; } @Override public long size() { return ip + is; } }; } /** * Performs the list-details for databases function. * @param ctx query context * @return iterator */ private Iter listDBs(final QueryContext ctx) { final StringList sl = List.list(ctx.context); return new Iter() { int pos; @Override public ANode get(final long i) throws QueryException { final FElem res = new FElem(DATABASE); final String name = sl.get((int) i); final MetaData meta = new MetaData(name, ctx.context); DataInput di = null; try { di = new DataInput(meta.dbfile(DATAINF)); meta.read(di); res.add(new FAttr(RESOURCES, token(meta.ndocs))); final String tstamp = InfoDB.DATE.format(new Date(meta.dbtime())); res.add(new FAttr(MDATE, token(tstamp))); if(ctx.context.perm(User.CREATE, meta)) res.add(new FAttr(PATH, token(meta.original))); res.add(new FTxt(token(name))); } catch(final IOException ex) { NODB.thrw(input, name); } finally { if(di != null) try { di.close(); } catch(final IOException ex) { } } return res; } @Override public ANode next() throws QueryException { return pos < size() ? get(pos++) : null; } @Override public boolean reset() { pos = 0; return true; } @Override public long size() { return sl.size(); } }; } /** * Performs the is-raw function. * @param ctx query context * @return result * @throws QueryException query exception */ private Bln isRaw(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final String path = path(1, ctx); final IOFile io = data.meta.binary(path); return Bln.get(io.exists() && !io.isDir()); } /** * Performs the exists function. * @param ctx query context * @return result * @throws QueryException query exception */ private Bln exists(final QueryContext ctx) throws QueryException { try { final Data data = data(0, ctx); if(expr.length == 1) return Bln.TRUE; // check if raw file or XML document exists final String path = path(1, ctx); final IOFile io = data.meta.binary(path); return Bln.get(io.exists() && !io.isDir() || data.resources.doc(path) != -1); } catch(final QueryException ex) { if(ex.err() == NODB) return Bln.FALSE; throw ex; } } /** * Performs the is-xml function. * @param ctx query context * @return result * @throws QueryException query exception */ private Bln isXML(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final String path = path(1, ctx); return Bln.get(data.resources.doc(path) != -1); } /** * Performs the content-type function. * @param ctx query context * @return result * @throws QueryException query exception */ private Str contentType(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final String path = path(1, ctx); if(data.resources.doc(path) != -1) return Str.get(MimeTypes.APP_XML); final IOFile io = data.meta.binary(path); if(!io.exists() || io.isDir()) RESFNF.thrw(input, path); return Str.get(MimeTypes.get(path)); } /** * Create a <code><resource/></code> node. * @param path path * @param raw is the resource a raw file * @param size size * @param ctype content type * @param mdate modified date * @return <code><resource/></code> node */ static FNode resource(final byte[] path, final boolean raw, final long size, final byte[] ctype, final long mdate) { final FElem res = new FElem(RESOURCE). add(new FTxt(path)). add(new FAttr(RAW, token(raw))). add(new FAttr(CTYPE, ctype)). add(new FAttr(MDATE, token(mdate))); return raw ? res.add(new FAttr(SIZE, token(size))) : res; } /** * Performs the system function. * @param ctx query context * @return node */ private static ANode system(final QueryContext ctx) { return toNode(Info.info(ctx.context), SYSTEM); } /** * Performs the info function. * @param ctx query context * @return node * @throws QueryException query exception */ private ANode info(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final boolean create = ctx.context.user.perm(User.CREATE); return toNode(InfoDB.db(data.meta, false, true, create), DATABASE); } /** * Converts the specified info string to a node fragment. * @param root name of the root node * @param str string to be converted * @return node */ private static ANode toNode(final String str, final QNm root) { final FElem top = new FElem(root); FElem node = null; for(final String l : str.split("\r\n?|\n")) { final String[] cols = l.split(": ", 2); if(cols[0].isEmpty()) continue; final String name = cols[0].replaceAll(" |-", ""); final FElem n = new FElem(new QNm(lc(token(name)))); if(cols[0].startsWith(" ")) { if(node != null) node.add(n); if(!cols[1].isEmpty()) n.add(new FTxt(token(cols[1]))); } else { node = n; top.add(n); } } return top; } /** * Performs the add function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item add(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final Item it = checkItem(expr[1], ctx); final String path = expr.length < 3 ? "" : path(2, ctx); ctx.updates.add(new DBAdd(data, input, it, path, ctx.context), ctx); return null; } /** * Performs the replace function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item replace(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final String path = path(1, ctx); final Item doc = checkItem(expr[2], ctx); // collect all old documents final Resources res = data.resources; final int pre = res.doc(path); if(pre != -1) { if(res.docs(path).size() != 1) DOCTRGMULT.thrw(input); ctx.updates.add(new DeleteNode(pre, data, input), ctx); } // delete binary resources final IOFile bin = data.meta.binary(path); if(bin != null) ctx.updates.add(new DBDelete(data, path, input), ctx); ctx.updates.add(new DBAdd(data, input, doc, path, ctx.context), ctx); final IOFile file = data.meta.binary(path); if(file != null && file.exists() && !file.isDir()) { final Item it = checkItem(doc, ctx); ctx.updates.add(new DBStore(data, token(path), it, input), ctx); } return null; } /** * Performs the delete function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item delete(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final String path = path(1, ctx); // delete XML resources final IntList docs = data.resources.docs(path); for(int i = 0, is = docs.size(); i < is; i++) { ctx.updates.add(new DeleteNode(docs.get(i), data, input), ctx); } // delete raw resources final IOFile bin = data.meta.binary(path); if(bin == null) UPDBDELERR.thrw(input, path); ctx.updates.add(new DBDelete(data, path, input), ctx); return null; } /** * Performs the rename function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item rename(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final String source = path(1, ctx); final String target = path(2, ctx); // the first step of the path should be the database name final IntList il = data.resources.docs(source); for(int i = 0, is = il.size(); i < is; i++) { final int pre = il.get(i); final String trg = Rename.target(data, pre, source, target); if(trg.isEmpty()) EMPTYPATH.thrw(input, this); ctx.updates.add(new ReplaceValue(pre, data, input, token(trg)), ctx); } // rename files final IOFile src = data.meta.binary(source); final IOFile trg = data.meta.binary(target); if(src == null || trg == null) UPDBRENAMEERR.thrw(input, src); ctx.updates.add(new DBRename(data, src.path(), trg.path(), input), ctx); return null; } /** * Performs the optimize function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item optimize(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final boolean all = expr.length == 2 && checkBln(expr[1], ctx); ctx.updates.add(new DBOptimize(data, ctx.context, all, input), ctx); return null; } /** * Performs the store function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private Item store(final QueryContext ctx) throws QueryException { checkWrite(ctx); final Data data = data(0, ctx); final String path = path(1, ctx); final IOFile file = data.meta.binary(path); if(file == null || file.isDir()) RESINV.thrw(input, path); final Item it = checkItem(expr[2], ctx); ctx.updates.add(new DBStore(data, token(path), it, input), ctx); return null; } /** * Performs the retrieve function. * @param ctx query context * @return {@code null} * @throws QueryException query exception */ private B64Stream retrieve(final QueryContext ctx) throws QueryException { final Data data = data(0, ctx); final String path = path(1, ctx); final IOFile file = data.meta.binary(path); if(file == null || !file.exists() || file.isDir()) RESFNF.thrw(input, path); return new B64Stream(file, DBERR); } /** * Performs the node-pre and node-id function. * @param ctx query context * @param id id flag * @return iterator * @throws QueryException query exception */ private Iter node(final QueryContext ctx, final boolean id) throws QueryException { return new Iter() { final Iter ir = ctx.iter(expr[0]); @Override public Int next() throws QueryException { final Item it = ir.next(); if(it == null) return null; final DBNode node = checkDBNode(it); return Int.get(id ? node.data.id(node.pre) : node.pre); } }; } /** * Sends an event to the registered sessions. * @param ctx query context * @return event result * @throws QueryException query exception */ private Item event(final QueryContext ctx) throws QueryException { final byte[] name = checkStr(expr[0], ctx); final ArrayOutput ao = new ArrayOutput(); try { // run serialization final Serializer ser = Serializer.get(ao, ctx.serProp(true)); final ValueIter ir = ctx.value(expr[1]).iter(); for(Item it; (it = ir.next()) != null;) it.serialize(ser); ser.close(); } catch(final SerializerException ex) { throw ex.getCause(input); } catch(final IOException ex) { SERANY.thrw(input, ex); } // throw exception if event is unknown if(!ctx.context.events.notify(ctx.context, name, ao.toArray())) { NOEVENT.thrw(input, name); } return null; } @Override public boolean isVacuous() { return sig == Function._DB_EVENT; } @Override public boolean uses(final Use u) { final boolean up = sig == Function._DB_ADD || sig == Function._DB_DELETE || sig == Function._DB_RENAME || sig == Function._DB_REPLACE || sig == Function._DB_OPTIMIZE || sig == Function._DB_STORE; return // skip evaluation at compile time u == Use.CTX && ( sig == Function._DB_TEXT || sig == Function._DB_ATTRIBUTE || sig == Function._DB_FULLTEXT || sig == Function._DB_EVENT || up) || u == Use.UPD && up || super.uses(u); } @Override public boolean iterable() { // index functions will always yield ordered and duplicate-free results return sig == Function._DB_OPEN || sig == Function._DB_TEXT || sig == Function._DB_ATTRIBUTE || sig == Function._DB_FULLTEXT || super.iterable(); } /** * Returns the specified expression as normalized database path. * Throws an exception if the path is invalid. * @param i index of argument * @param ctx query context * @return normalized path * @throws QueryException query exception */ private String path(final int i, final QueryContext ctx) throws QueryException { final String path = string(checkStr(expr[i], ctx)); final String norm = MetaData.normPath(path); if(norm == null) RESINV.thrw(input, path); return norm; } }