package org.basex.http.webdav; import static org.basex.http.webdav.WebDAVUtils.*; import static org.basex.query.func.Function.*; import java.io.*; import java.util.*; import java.util.Map.Entry; import java.util.List; import org.basex.api.client.*; import org.basex.core.*; import org.basex.core.cmd.*; import org.basex.core.cmd.Set; import org.basex.http.*; import org.basex.io.in.*; import org.basex.io.serial.*; import org.basex.query.func.db.*; import org.basex.util.*; import org.basex.util.http.*; import org.basex.util.list.*; /** * Service handling the various WebDAV operations. * * @author BaseX Team 2005-17, BSD License * @author Dimitar Popov */ final class WebDAVService { /** Name of the database with the WebDAV locks. */ private static final String WEBDAV_DB = "~webdav"; /** Static WebDAV character map. */ private static final String WEBDAV; static { final StringBuilder sb = new StringBuilder(); add(160, sb); for(int cp = 8192; cp <= 8207; cp++) add(cp, sb); for(int cp = 8232; cp <= 8239; cp++) add(cp, sb); for(int cp = 8287; cp <= 8303; cp++) add(cp, sb); WEBDAV = sb.toString(); } /** HTTP connection. */ final HTTPConnection conn; /** Locking service. */ final WebDAVLockService locking; /** Session. */ private LocalSession ls; /** * Constructor. * @param conn HTTP connection */ WebDAVService(final HTTPConnection conn) { this.conn = conn; locking = new WebDAVLockService(conn); } /** * Closes an open session. */ void close() { if(ls != null) ls.close(); } /** * Checks if the user is authorized to perform the given action. * @param db database (can be {@code null}) * @return {@code true} if the user is authorized */ static boolean authorize(final String db) { return !WEBDAV_DB.equals(db); } /** * Checks a folder for a dummy document and delete it. * @param db database * @param path path * @throws IOException I/O exception */ void deleteDummy(final String db, final String path) throws IOException { final String dummy = path + SEP + DUMMY; if(!pathExists(db, dummy)) return; // path contains dummy document final LocalSession session = session(); session.execute(new Open(db)); session.execute(new Delete(dummy)); } /** * Checks if the specified database exists. * @param db database to be found * @return result of check * @throws IOException I/O exception */ boolean dbExists(final String db) throws IOException { final WebDAVQuery query = new WebDAVQuery(_DB_EXISTS.args("$db")).bind("db", db); return execute(query).equals(Text.TRUE); } /** * Retrieves the last modified timestamp of a database. * @param db database * @return timestamp in milliseconds * @throws IOException I/O exception */ long timestamp(final String db) throws IOException { final WebDAVQuery query = new WebDAVQuery(DATA.args(_DB_INFO.args("$db") + "/descendant::" + DbFn.toName(Text.TIMESTAMP) + "[1]")).bind("db", db); return DateTime.parse(execute(query)).getTime(); } /** * Retrieves meta data about the resource at the given path. * @param db database * @param path resource path * @return resource meta data * @throws IOException I/O exception */ private WebDAVMetaData metaData(final String db, final String path) throws IOException { final WebDAVQuery query = new WebDAVQuery( "let $a := " + _DB_LIST_DETAILS.args("$db", "$path") + "[1] " + "return string-join(($a/@raw, $a/@content-type, $a/@modified-date, $a/@size, $a),out:tab())"); query.bind("db", db); query.bind("path", path); final String[] result = results(query); final boolean raw = Boolean.parseBoolean(result[0]); final MediaType type = new MediaType(result[1]); final long mod = DateTime.parse(result[2]).getTime(); final Long size = raw ? Long.valueOf(result[3]) : null; final String pth = stripLeadingSlash(result[4]); return new WebDAVMetaData(db, pth, mod, raw, type, size); } /** * Deletes a document or folder. * @param db database * @param path path * @throws IOException I/O exception */ void delete(final String db, final String path) throws IOException { final LocalSession session = session(); session.execute(new Open(db)); session.execute(new Delete(path)); // create dummy if parent is an empty folder final int ix = path.lastIndexOf(SEP); if(ix > 0) createDummy(db, path.substring(0, ix)); } /** * Renames a document or folder. * @param db database * @param path path * @param npath new path * @throws IOException I/O exception */ void rename(final String db, final String path, final String npath) throws IOException { final LocalSession session = session(); session.execute(new Open(db)); session.execute(new Rename(path, npath)); // create dummy if old parent is an empty folder final int i1 = path.lastIndexOf(SEP); if(i1 > 0) createDummy(db, path.substring(0, i1)); // delete dummy if new parent is an empty folder final int i2 = npath.lastIndexOf(SEP); if(i2 > 0) deleteDummy(db, npath.substring(0, i2)); } /** * Copies a document to the specified target. * @param db source database * @param path source path * @param tdb target database * @param tpath target path * @throws IOException I/O exception */ void copyDoc(final String db, final String path, final String tdb, final String tpath) throws IOException { final WebDAVQuery query = new WebDAVQuery( "declare option db:chop 'false';" + "if(" + _DB_IS_RAW.args("$db", "$path") + ')' + " then " + _DB_STORE.args("$tdb", "$tpath", _DB_RETRIEVE.args("$db", "$path")) + " else " + _DB_ADD.args("$tdb", _DB_OPEN.args("$db", "$path"), "$tpath")); query.bind("db", db); query.bind("path", path); query.bind("tdb", tdb); query.bind("tpath", tpath); execute(query); } /** * Copies all documents in a folder to another folder. * @param db source database * @param path source path * @param tdb target database * @param tpath target folder * @throws IOException I/O exception */ void copyAll(final String db, final String path, final String tdb, final String tpath) throws IOException { final WebDAVQuery query = new WebDAVQuery( "declare option db:chop 'false'; " + "for $d in " + _DB_LIST.args("$db", "$path") + "let $t := $tpath ||'/'|| substring($d, string-length($path) + 1) return " + "if(" + _DB_IS_RAW.args("$db", "$d") + ") " + "then " + _DB_STORE.args("$tdb", "$t", _DB_RETRIEVE.args("$db", "$d")) + " else " + _DB_ADD.args("$tdb", _DB_OPEN.args("$db", "$d"), "$t")); query.bind("db", db); query.bind("path", path); query.bind("tdb", tdb); query.bind("tpath", tpath); execute(query); } /** * Writes a file to the specified output stream. * @param db database * @param path path * @param raw is the file a raw file * @param out output stream * @throws IOException I/O exception */ void retrieve(final String db, final String path, final boolean raw, final OutputStream out) throws IOException { session().setOutputStream(out); final String string = SerializerOptions.USE_CHARACTER_MAPS.arg(WEBDAV) + (raw ? _DB_RETRIEVE : _DB_OPEN).args("$db", "$path") + "[1]"; final WebDAVQuery query = new WebDAVQuery(string); query.bind("db", db); query.bind("path", path); execute(query); } /** * Creates an empty database with the given name. * @param db database name * @return object representing the newly created database * @throws IOException I/O exception */ WebDAVResource createDb(final String db) throws IOException { session().execute(new CreateDB(db)); return WebDAVFactory.database(this, new WebDAVMetaData(db, timestamp(db))); } /** * Drops the database with the given name. * @param db database name * @throws IOException I/O exception */ void dropDb(final String db) throws IOException { session().execute(new DropDB(db)); } /** * Renames the database with the given name. * @param old database name * @param db new name * @throws IOException I/O exception */ void renameDb(final String old, final String db) throws IOException { session().execute(new AlterDB(old, dbName(db))); } /** * Copies the database with the given name. * @param old database name * @param db new database name * @throws IOException I/O exception */ void copyDb(final String old, final String db) throws IOException { session().execute(new Copy(old, dbName(db))); } /** * Lists the direct children of a path. * @param db database * @param path path * @return children * @throws IOException I/O exception */ List<WebDAVResource> list(final String db, final String path) throws IOException { final WebDAVQuery query = new WebDAVQuery(STRING_JOIN.args( _DB_LIST_DETAILS.args("$db", "$path") + " ! (" + "@raw,@content-type,@modified-date,@size," + SUBSTRING_AFTER.args("text()", "$path") + ')', "out:tab()")); query.bind("db", db); query.bind("path", path); final String[] result = results(query); final HashSet<String> paths = new HashSet<>(); final List<WebDAVResource> ch = new ArrayList<>(); final int rs = result.length; for(int r = 0; r < rs; r += 5) { final boolean raw = Boolean.parseBoolean(result[r]); final MediaType ctype = new MediaType(result[r + 1]); final long mod = DateTime.parse(result[r + 2]).getTime(); final Long size = raw ? Long.valueOf(result[r + 3]) : null; final String pth = stripLeadingSlash(result[r + 4]); final int ix = pth.indexOf(SEP); // check if document or folder if(ix < 0) { if(!pth.equals(DUMMY)) ch.add(WebDAVFactory.file(this, new WebDAVMetaData(db, path + SEP + pth, mod, raw, ctype, size))); } else { final String dir = path + SEP + pth.substring(0, ix); if(paths.add(dir)) ch.add(WebDAVFactory.folder(this, new WebDAVMetaData(db, dir, mod))); } } return ch; } /** * Lists all databases. * @return a list of database resources. * @throws IOException I/O exception */ List<WebDAVResource> listDbs() throws IOException { final WebDAVQuery query = new WebDAVQuery(STRING_JOIN.args( _DB_LIST_DETAILS.args() + "[. != $db] ! (text(), @modified-date)", "out:tab()")); query.bind("db", WEBDAV_DB); final String[] result = results(query); final List<WebDAVResource> dbs = new ArrayList<>(); final int rs = result.length; for(int r = 0; r < rs; r += 2) { final String name = result[r]; final long mod = DateTime.parse(result[r + 1]).getTime(); dbs.add(WebDAVFactory.database(this, new WebDAVMetaData(name, mod))); } return dbs; } /** * Creates a folder at the given path. * @param db database * @param path path * @param name new folder name * @return new folder resource * @throws IOException I/O exception */ WebDAVResource createFolder(final String db, final String path, final String name) throws IOException { deleteDummy(db, path); final String newFolder = path + SEP + name; createDummy(db, newFolder); return WebDAVFactory.folder(this, new WebDAVMetaData(db, newFolder, timestamp(db))); } /** * Gets the resource at the given path. * @param db database * @param path path * @return resource * @throws IOException I/O exception */ WebDAVResource resource(final String db, final String path) throws IOException { return exists(db, path) ? WebDAVFactory.file(this, metaData(db, path)) : pathExists(db, path) ? WebDAVFactory.folder(this, new WebDAVMetaData(db, path, timestamp(db))) : null; } /** * Adds the given file to the specified path. * @param db database * @param path path * @param name file name * @param in file content * @return object representing the newly added file * @throws IOException I/O exception */ WebDAVResource createFile(final String db, final String path, final String name, final InputStream in) throws IOException { final LocalSession session = session(); session.execute(new Open(db)); final String dbp = path.isEmpty() ? name : path + SEP + name; // delete old resource if it already exists if(pathExists(db, dbp)) { session.execute(new Open(db)); session.execute(new Delete(dbp)); } else { // otherwise, delete dummy file deleteDummy(db, path); } return addFile(db, dbp, in); } /** * Creates a new database from the given file. * @param n file name * @param in file content * @return object representing the newly created database * @throws IOException I/O exception */ WebDAVResource createFile(final String n, final InputStream in) throws IOException { return addFile(null, n, in); } /** * Checks if any of the resources starts with the given path. * @param db name of database * @param path path * @return {@code true} if there are resources with the given prefix * @throws IOException I/O exception */ private boolean pathExists(final String db, final String path) throws IOException { final WebDAVQuery query = new WebDAVQuery(EXISTS.args(_DB_LIST.args("$db", "$path"))); query.bind("db", db); query.bind("path", path); return execute(query).equals(Text.TRUE); } /** * Checks if any resource with the specified name exists. * @param db name of database * @param path resource path * @return {@code true} if there are resources with the name * @throws IOException I/O exception */ private boolean exists(final String db, final String path) throws IOException { final WebDAVQuery query = new WebDAVQuery(_DB_EXISTS.args("$db", "$path")); query.bind("db", db); query.bind("path", path); return execute(query).equals(Text.TRUE); } /** * Creates a database with the given name and add the given document. * @param db database name * @param in data stream * @return object representing the newly created database * @throws IOException I/O exception */ private WebDAVResource createDb(final String db, final InputStream in) throws IOException { session().create(db, in); return WebDAVFactory.database(this, new WebDAVMetaData(db, timestamp(db))); } /** * Adds a document with the specified name to the given path. * @param db database * @param path path where the document will be added * @param in data stream * @return object representing the newly added XML * @throws IOException I/O exception */ private WebDAVResource addXML(final String db, final String path, final InputStream in) throws IOException { final LocalSession session = session(); session.execute(new Set(MainOptions.CHOP, false)); session.execute(new Open(db)); session.add(path, in); return WebDAVFactory.file(this, new WebDAVMetaData(db, path, timestamp(db), false, MediaType.APPLICATION_XML, null)); } /** * Adds a binary file with the specified name to the given path. * @param db database * @param path path where the file will be stored * @param in data stream * @return object representing the newly added file * @throws IOException I/O exception */ private WebDAVResource store(final String db, final String path, final InputStream in) throws IOException { final LocalSession session = session(); session.execute(new Open(db)); session.store(path, in); return WebDAVFactory.file(this, metaData(db, path)); } /** * Adds a file in to the given path. * @param db database * @param path path * @param in file content * @return object representing the newly added file * @throws IOException I/O exception */ private WebDAVResource addFile(final String db, final String path, final InputStream in) throws IOException { // use 4MB as buffer input try(BufferInput bi = new BufferInput(in, 1 << 22)) { // guess the content type from the first character if(peek(bi) == '<') { try { // add input as XML document return db == null ? createDb(dbName(path), bi) : addXML(db, path, bi); } catch(final IOException ex) { // reset stream if it did not work out try { bi.reset(); } catch(final IOException e) { // throw original exception if input cannot be reset throw ex; } } } // add input as raw file final String d; if(db == null) { d = dbName(path); createDb(d); } else { d = db; } return store(d, path, bi); } } /** * Checks if a folder is empty and create a dummy document. * @param db database * @param path path * @throws IOException I/O exception */ private void createDummy(final String db, final String path) throws IOException { // check if path is a folder and is empty if(path.matches("[^/]") || pathExists(db, path)) return; final LocalSession session = session(); session.execute(new Open(db)); session.store(path + SEP + DUMMY, new ArrayInput(Token.EMPTY)); } /** * Executes a query. * @param query query to be executed * @return result * @throws IOException error during query execution */ private String execute(final WebDAVQuery query) throws IOException { final XQuery xquery = new XQuery(query.toString()); for(final Entry<String, String> entry : query.entries()) { xquery.bind(entry.getKey(), entry.getValue()); } return session().execute(xquery); } /** * Executes a query and returns all results as a list. * @param query query to be executed * @return result * @throws IOException error during query execution */ private String[] results(final WebDAVQuery query) throws IOException { final StringList sl = new StringList(); for(final String result : Strings.split(execute(query), '\t')) { if(!result.isEmpty()) sl.add(result); } return sl.finish(); } /** * Constructor. * @return local session */ private LocalSession session() { if(ls == null) ls = new LocalSession(conn.context); return ls; } /** * Adds a character mapping to the specified string builder. * @param ch character to be added * @param sb string builder */ private static void add(final int ch, final StringBuilder sb) { if(sb.length() > 0) sb.append(','); sb.append((char) ch).append("=&#").append(ch).append(';'); } }