package org.basex.query.func;
import static org.basex.query.QueryText.*;
import static org.basex.query.util.Err.*;
import static org.basex.util.Token.*;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.Random;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import org.basex.build.Parser;
import org.basex.build.file.HTMLParser;
import org.basex.core.Prop;
import org.basex.io.IO;
import org.basex.io.IOContent;
import org.basex.io.IOFile;
import org.basex.io.Zip;
import org.basex.io.in.NewlineInput;
import org.basex.io.serial.Serializer;
import org.basex.io.serial.SerializerException;
import org.basex.io.serial.SerializerProp;
import org.basex.query.QueryContext;
import org.basex.query.QueryException;
import org.basex.query.expr.Expr;
import org.basex.query.item.ANode;
import org.basex.query.item.B64;
import org.basex.query.item.DBNode;
import org.basex.query.item.FAttr;
import org.basex.query.item.FElem;
import org.basex.query.item.Hex;
import org.basex.query.item.Item;
import org.basex.query.item.NodeType;
import org.basex.query.item.QNm;
import org.basex.query.item.Str;
import org.basex.query.iter.AxisIter;
import org.basex.query.util.DataBuilder;
import org.basex.util.Atts;
import org.basex.util.InputInfo;
import org.basex.util.TokenBuilder;
import org.basex.util.list.ByteList;
import org.basex.util.list.StringList;
/**
* Functions on zip files.
*
* @author BaseX Team 2005-12, BSD License
* @author Christian Gruen
*/
public final class FNZip extends StandardFunc {
/** Element: zip:file. */
private static final QNm E_FILE = new QNm(token("zip:file"), ZIPURI);
/** Element: zip:dir. */
private static final QNm E_DIR = new QNm(token("zip:dir"), ZIPURI);
/** Element: zip:entry. */
private static final QNm E_ENTRY = new QNm(token("zip:entry"), ZIPURI);
/** Attribute: href. */
private static final QNm A_HREF = new QNm(token("href"));
/** Attribute: name. */
private static final QNm A_NAME = new QNm(token("name"));
/** Attribute: src. */
private static final QNm A_SRC = new QNm(token("src"));
/** Attribute: src. */
private static final QNm A_METHOD = new QNm(token("method"));
/** Method "base64". */
private static final String M_BASE64 = "base64";
/** Method "hex". */
private static final String M_HEX = "hex";
/**
* Constructor.
* @param ii input info
* @param f function definition
* @param e arguments
*/
public FNZip(final InputInfo ii, final Function f, final Expr... e) {
super(ii, f, e);
}
@Override
public Item item(final QueryContext ctx, final InputInfo ii)
throws QueryException {
checkAdmin(ctx);
switch(sig) {
case _ZIP_BINARY_ENTRY: return binaryEntry(ctx);
case _ZIP_TEXT_ENTRY: return textEntry(ctx);
case _ZIP_HTML_ENTRY: return xmlEntry(ctx, true);
case _ZIP_XML_ENTRY: return xmlEntry(ctx, false);
case _ZIP_ENTRIES: return entries(ctx);
case _ZIP_ZIP_FILE: return zipFile(ctx);
case _ZIP_UPDATE_ENTRIES: return updateEntries(ctx);
default: return super.item(ctx, ii);
}
}
/**
* Returns a xs:base64Binary item, created from a binary file.
* Returns a binary entry.
* @param ctx query context
* @return binary result
* @throws QueryException query exception
*/
private B64 binaryEntry(final QueryContext ctx) throws QueryException {
return new B64(entry(ctx));
}
/**
* Returns a string, created from a text file.
* @param ctx query context
* @return binary result
* @throws QueryException query exception
*/
private Str textEntry(final QueryContext ctx) throws QueryException {
final String enc = expr.length < 3 ? null : string(checkStr(expr[2], ctx));
final IO io = new IOContent(entry(ctx));
try {
return Str.get(new NewlineInput(io, enc).content());
} catch(final IOException ex) {
throw ZIPFAIL.thrw(input, ex.getMessage());
}
}
/**
* Returns a document node, created from an XML or HTML file.
* @param ctx query context
* @param html html flag
* @return binary result
* @throws QueryException query exception
*/
private ANode xmlEntry(final QueryContext ctx, final boolean html)
throws QueryException {
final Prop prop = ctx.context.prop;
final IO io = new IOContent(entry(ctx));
try {
return new DBNode(html ? new HTMLParser(io, "", prop) :
Parser.xmlParser(io, prop), prop);
} catch(final IOException ex) {
throw SAXERR.thrw(input, ex);
}
}
/**
* Returns a zip archive description.
* @param ctx query context
* @return binary result
* @throws QueryException query exception
*/
private ANode entries(final QueryContext ctx) throws QueryException {
final String file = string(checkStr(expr[0], ctx));
// check file path
final IOFile path = new IOFile(file);
if(!path.exists()) ZIPNOTFOUND.thrw(input, file);
// loop through file
ZipFile zf = null;
try {
zf = new ZipFile(file);
// create result node
final FElem root = new FElem(E_FILE, new Atts(ZIP, ZIPURI));
root.add(new FAttr(A_HREF, token(path.path())));
createEntries(paths(zf).iterator(), root, "");
return root;
} catch(final IOException ex) {
throw ZIPFAIL.thrw(input, ex.getMessage());
} finally {
if(zf != null) try { zf.close(); } catch(final IOException e) { }
}
}
/**
* Creates the zip archive nodes in a recursive manner.
* @param it iterator
* @param par parent node
* @param pref directory prefix
* @return current prefix
*/
private static String createEntries(final Iterator<String> it,
final FElem par, final String pref) {
String path = null;
boolean curr = false;
while(curr || it.hasNext()) {
if(!curr) {
path = it.next();
curr = true;
}
if(path == null) break;
// current entry is located in a higher/other directory
if(!path.startsWith(pref)) return path;
// current file starts with new directory
final int i = path.lastIndexOf('/');
final String dir = i == -1 ? path : path.substring(0, i);
final String name = path.substring(i + 1);
if(name.isEmpty()) {
// path ends with slash: create directory
path = createEntries(it, createDir(par, dir), dir);
} else {
// create file
createFile(par, name);
curr = false;
}
}
return null;
}
/**
* Creates a directory element.
* @param par parent node
* @param name name of directory
* @return element
*/
private static FElem createDir(final FElem par, final String name) {
final FElem e = new FElem(E_DIR);
e.add(new FAttr(A_NAME, token(name)));
par.add(e);
return e;
}
/**
* Creates a file element.
* @param par parent node
* @param name name of directory
*/
private static void createFile(final FElem par, final String name) {
final FElem e = new FElem(E_ENTRY);
e.add(new FAttr(A_NAME, token(name)));
par.add(e);
}
/**
* Creates a new zip file.
* @param ctx query context
* @return binary result
* @throws QueryException query exception
*/
private Item zipFile(final QueryContext ctx) throws QueryException {
// check argument
final ANode elm = (ANode) checkType(expr[0].item(ctx, input), NodeType.ELM);
if(!elm.qname().eq(E_FILE)) ZIPUNKNOWN.thrw(input, elm.qname());
// get file
final String file = attribute(elm, A_HREF, true);
// write zip file
FileOutputStream fos = null;
boolean ok = true;
try {
fos = new FileOutputStream(file);
final ZipOutputStream zos =
new ZipOutputStream(new BufferedOutputStream(fos));
create(zos, elm.children(), "", null, ctx);
zos.close();
} catch(final IOException ex) {
ok = false;
ZIPFAIL.thrw(input, ex.getMessage());
} finally {
if(fos != null) {
try { fos.close(); } catch(final IOException ex) { }
if(!ok) new IOFile(file).delete();
}
}
return null;
}
/**
* Adds files to the specified zip output, or copies files from the
* specified file.
* @param zos output stream
* @param ai axis iterator
* @param root root path
* @param ctx query context
* @param zf original zip file (or {@code null})
* @throws QueryException query exception
* @throws IOException I/O exception
*/
private void create(final ZipOutputStream zos, final AxisIter ai,
final String root, final ZipFile zf, final QueryContext ctx)
throws QueryException, IOException {
final byte[] data = new byte[IO.BLOCKSIZE];
for(ANode node; (node = ai.next()) != null;) {
// get entry type
final QNm mode = node.qname();
final boolean dir = mode.eq(E_DIR);
if(!dir && !mode.eq(E_ENTRY)) ZIPUNKNOWN.thrw(input, mode);
// file path: if null, the zip base name is used
String name = attribute(node, A_NAME, false);
// source: if null, the node's children are serialized
String src = attribute(node, A_SRC, false);
if(src != null) src = src.replaceAll("\\\\", "/");
if(name == null) {
// throw exception if both attributes are null
if(src == null) throw ZIPINVALID.thrw(input, node.qname(), A_SRC);
name = src;
}
name = name.replaceAll(".*/", "");
// add slash to directories
if(dir) name += '/';
zos.putNextEntry(new ZipEntry(root + name));
if(dir) {
create(zos, node.children(), root + name, zf, ctx);
} else {
if(src != null) {
// write file to zip archive
if(!new IOFile(src).exists()) ZIPNOTFOUND.thrw(input, src);
BufferedInputStream bis = null;
try {
bis = new BufferedInputStream(new FileInputStream(src));
for(int c; (c = bis.read(data)) != -1;) zos.write(data, 0, c);
} finally {
if(bis != null) try { bis.close(); } catch(final IOException e) { }
}
} else {
// no source reference: the child nodes are treated as file contents
final AxisIter ch = node.children();
final String m = attribute(node, A_METHOD, false);
// retrieve first child (might be null)
ANode n = ch.next();
// access original zip file if available, and if no children exist
ZipEntry ze = null;
if(zf != null && n == null) ze = zf.getEntry(root + name);
if(ze != null) {
// add old zip entry
final InputStream zis = zf.getInputStream(ze);
for(int c; (c = zis.read(data)) != -1;) zos.write(data, 0, c);
} else if(n != null) {
// write new binary content to archive
final boolean hex = M_HEX.equals(m);
if(hex || M_BASE64.equals(m)) {
// treat children as base64/hex
final ByteList bl = new ByteList();
do bl.add(n.string()); while((n = ch.next()) != null);
final byte[] bytes = bl.toArray();
zos.write((hex ? new Hex(bytes) : new B64(bytes)).toJava());
} else {
// serialize new nodes
try {
final Serializer ser = Serializer.get(zos, serPar(node, ctx));
do {
DataBuilder.stripNS(n, ZIPURI, ctx).serialize(ser);
} while((n = ch.next()) != null);
ser.close();
} catch(final SerializerException ex) {
throw ex.getCause(input);
}
}
}
}
zos.closeEntry();
}
}
}
/**
* Returns serialization parameters.
* @param node node with parameters
* @param ctx query context
* @return properties
* @throws SerializerException serializer exception
*/
private static SerializerProp serPar(final ANode node, final QueryContext ctx)
throws SerializerException {
// interpret query parameters
final TokenBuilder tb = new TokenBuilder();
final AxisIter ati = node.attributes();
for(ANode at; (at = ati.next()) != null;) {
final QNm name = at.qname();
if(name.eq(A_NAME) || name.eq(A_SRC)) continue;
if(tb.size() != 0) tb.add(',');
tb.add(name.local()).add('=').add(at.string());
}
return tb.size() == 0 ? ctx.serProp(true) :
new SerializerProp(tb.toString());
}
/**
* Updates a zip archive.
* @param ctx query context
* @return empty result
* @throws QueryException query exception
*/
private Item updateEntries(final QueryContext ctx) throws QueryException {
// check argument
final ANode elm = (ANode) checkType(expr[0].item(ctx, input), NodeType.ELM);
if(!elm.qname().eq(E_FILE)) ZIPUNKNOWN.thrw(input, elm.qname());
// sorted paths in original file
final String in = attribute(elm, A_HREF, true);
// target and temporary output file
final IOFile target = new IOFile(string(checkStr(expr[1], ctx)));
IOFile out;
do {
out = new IOFile(target.path() + new Random().nextInt(0x7FFFFFFF));
} while(out.exists());
// open zip file
if(!new IOFile(in).exists()) ZIPNOTFOUND.thrw(input, in);
ZipFile zf = null;
boolean ok = true;
try {
zf = new ZipFile(in);
// write zip file
FileOutputStream fos = null;
try {
fos = new FileOutputStream(out.path());
final ZipOutputStream zos =
new ZipOutputStream(new BufferedOutputStream(fos));
// fill new zip file with entries from old file and description
create(zos, elm.children(), "", zf, ctx);
zos.close();
} catch(final IOException ex) {
ok = false;
ZIPFAIL.thrw(input, ex.getMessage());
} finally {
if(fos != null) try { fos.close(); } catch(final IOException ex) { }
}
} catch(final IOException ex) {
throw ZIPFAIL.thrw(input, ex.getMessage());
} finally {
if(zf != null) try { zf.close(); } catch(final IOException e) { }
if(ok) {
// rename temporary file to final target
target.delete();
out.rename(target);
} else {
// remove temporary file
out.delete();
}
}
return null;
}
/**
* Returns a list of all file paths.
* @param zf zip file file to be parsed
* @return binary result
*/
private static StringList paths(final ZipFile zf) {
// traverse all zip entries and create intermediate map,
// as zip entries are not sorted
//final StringList paths = new StringList();
final TreeSet<String> paths = new TreeSet<String>();
final Enumeration<? extends ZipEntry> en = zf.entries();
// loop through all files
while(en.hasMoreElements()) {
final ZipEntry ze = en.nextElement();
final String name = ze.getName();
final int i = name.lastIndexOf('/');
// add directory
if(i > -1 && i + 1 < name.length()) paths.add(name.substring(0, i + 1));
paths.add(name);
}
final StringList sl = new StringList();
for(final String path : paths) sl.add(path);
return sl;
}
/**
* Returns the value of the specified attribute.
* @param elm element node
* @param name attribute to be found
* @param force if set to {@code true}, an exception is thrown if the
* attribute is not found
* @return attribute value
* @throws QueryException query exception
*/
private String attribute(final ANode elm, final QNm name, final boolean force)
throws QueryException {
final byte[] val = elm.attribute(name);
if(val == null && force) throw ZIPINVALID.thrw(input, elm.qname(), name);
return val == null ? null : string(val);
}
/**
* Returns an entry from a zip file.
* @param ctx query context
* @return binary result
* @throws QueryException query exception
*/
private byte[] entry(final QueryContext ctx) throws QueryException {
final IOFile file = new IOFile(string(checkStr(expr[0], ctx)));
final String path = string(checkStr(expr[1], ctx));
if(!file.exists()) ZIPNOTFOUND.thrw(input, file);
try {
return new Zip(file).read(path);
} catch(final FileNotFoundException ex) {
throw ZIPNOTFOUND.thrw(input, file + "/" + path);
} catch(final IOException ex) {
throw ZIPFAIL.thrw(input, ex.getMessage());
}
}
@Override
public boolean uses(final Use u) {
return u == Use.NDT || super.uses(u);
}
}