/** * Returns atomic text node merging operations if necessary for the given node PRE and its right * neighbor PRE+1. * * @param a node PRE value * @param d target data reference * @return list of text merging operations */ private AtomicUpdateList necessaryMerges(final int a, final Data d) { final AtomicUpdateList mergeTwoNodes = new AtomicUpdateList(d); final int s = d.meta.size; final int b = a + 1; // don't leave table if (a >= s || b >= s || a < 0 || b < 0) return mergeTwoNodes; // only merge texts if (d.kind(a) != Data.TEXT || d.kind(b) != Data.TEXT) return mergeTwoNodes; // only merge neighboring texts if (d.parent(a, Data.TEXT) != d.parent(b, Data.TEXT)) return mergeTwoNodes; mergeTwoNodes.addDelete(b); mergeTwoNodes.addUpdateValue(a, Data.TEXT, Token.concat(d.text(a, true), d.text(b, true))); return mergeTwoNodes; }
/** * Functions on archives. * * @author BaseX Team 2005-12, BSD License * @author Christian Gruen */ public class FNArchive extends StandardFunc { /** Archive namespace. */ private static final Atts NS = new Atts(ARCHIVE, ARCHIVEURI); /** Element: Entry. */ private static final QNm Q_ENTRY = new QNm("archive:entry", ARCHIVEURI); /** Element: options. */ private static final QNm Q_OPTIONS = new QNm("archive:options", ARCHIVEURI); /** Option: algorithm. */ private static final QNm Q_FORMAT = new QNm("archive:format", ARCHIVEURI); /** Option: algorithm. */ private static final QNm Q_ALGORITHM = new QNm("archive:algorithm", ARCHIVEURI); /** Root node test. */ private static final ExtTest TEST = new ExtTest(NodeType.ELM, Q_ENTRY); /** Level. */ private static final QNm Q_LEVEL = new QNm("compression-level"); /** Encoding. */ private static final QNm Q_ENCODING = new QNm("encoding"); /** Last modified. */ private static final QNm Q_LAST_MOD = new QNm("last-modified"); /** Compressed size. */ private static final QNm Q_COMP_SIZE = new QNm("compressed-size"); /** Uncompressed size. */ private static final QNm Q_SIZE = new QNm("size"); /** Value. */ private static final QNm Q_VALUE = new QNm("value"); /** Option: format. */ private static final byte[] FORMAT = Token.token("format"); /** Option: algorithm. */ private static final byte[] ALGORITHM = Token.token("algorithm"); /** Option: algorithm: deflate. */ private static final byte[] DEFLATE = Token.token("deflate"); /** Option: algorithm: stored. */ private static final byte[] STORED = Token.token("stored"); /** Option: algorithm: unknown. */ private static final byte[] UNKNOWN = Token.token("unknown"); /** * Constructor. * * @param ii input info * @param f function definition * @param e arguments */ public FNArchive(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 _ARCHIVE_ENTRIES: return entries(ctx); case _ARCHIVE_EXTRACT_TEXT: return extractText(ctx); case _ARCHIVE_EXTRACT_BINARY: return extractBinary(ctx); default: return super.iter(ctx); } } @Override public Item item(final QueryContext ctx, final InputInfo ii) throws QueryException { checkCreate(ctx); switch (sig) { case _ARCHIVE_CREATE: return create(ctx); case _ARCHIVE_UPDATE: return update(ctx); case _ARCHIVE_DELETE: return delete(ctx); case _ARCHIVE_OPTIONS: return options(ctx); default: return super.item(ctx, ii); } } /** * Creates a new archive. * * @param ctx query context * @return archive * @throws QueryException query exception */ private B64 create(final QueryContext ctx) throws QueryException { final Iter entr = ctx.iter(expr[0]); final Iter cont = ctx.iter(expr[1]); final Item opt = expr.length > 2 ? expr[2].item(ctx, info) : null; final TokenMap map = new FuncParams(Q_OPTIONS, info).parse(opt); final byte[] f = map.get(FORMAT); final String format = f != null ? string(lc(f)) : "zip"; final ArchiveOut out = ArchiveOut.get(format, info); // check algorithm final byte[] alg = map.get(ALGORITHM); int level = ZipEntry.DEFLATED; if (alg != null) { if (format.equals("zip") && !eq(alg, STORED, DEFLATE) || format.equals("gzip") && !eq(alg, DEFLATE)) { ARCH_SUPP.thrw(info, ALGORITHM, alg); } if (eq(alg, STORED)) level = ZipEntry.STORED; else if (eq(alg, DEFLATE)) level = ZipEntry.DEFLATED; } out.level(level); try { int e = 0; int c = 0; Item en, cn; while (true) { en = entr.next(); cn = cont.next(); if (en == null || cn == null) break; if (out instanceof GZIPOut && c > 0) ARCH_ONE.thrw(info, format.toUpperCase(Locale.ENGLISH)); add(checkElmStr(en), cn, out, level); e++; c++; } // count remaining entries if (cn != null) do c++; while (cont.next() != null); if (en != null) do e++; while (entr.next() != null); if (e != c) throw ARCH_DIFF.thrw(info, e, c); } catch (final IOException ex) { Util.debug(ex); throw ARCH_FAIL.thrw(info, ex); } finally { out.close(); } return new B64(out.toArray()); } /** * Returns the options of an archive. * * @param ctx query context * @return entries * @throws QueryException query exception */ private FElem options(final QueryContext ctx) throws QueryException { final B64 archive = (B64) checkType(checkItem(expr[0], ctx), AtomType.B64); String format = null; int level = -1; final ArchiveIn arch = ArchiveIn.get(archive.input(info), info); try { format = arch.format(); while (arch.more()) { final ZipEntry ze = arch.entry(); if (ze.isDirectory()) continue; level = ze.getMethod(); break; } } catch (final IOException ex) { Util.debug(ex); ARCH_FAIL.thrw(info, ex); } finally { arch.close(); } // create result element final FElem e = new FElem(Q_OPTIONS, NS); if (format != null) e.add(new FElem(Q_FORMAT).add(Q_VALUE, format)); if (level >= 0) { final byte[] lvl = level == 8 ? DEFLATE : level == 0 ? STORED : UNKNOWN; e.add(new FElem(Q_ALGORITHM).add(Q_VALUE, lvl)); } return e; } /** * Returns the entries of an archive. * * @param ctx query context * @return entries * @throws QueryException query exception */ private Iter entries(final QueryContext ctx) throws QueryException { final B64 archive = (B64) checkType(checkItem(expr[0], ctx), AtomType.B64); final ValueBuilder vb = new ValueBuilder(); final ArchiveIn in = ArchiveIn.get(archive.input(info), info); try { while (in.more()) { final ZipEntry ze = in.entry(); if (ze.isDirectory()) continue; final FElem e = new FElem(Q_ENTRY, NS); long s = ze.getSize(); if (s != -1) e.add(Q_SIZE, token(s)); s = ze.getTime(); if (s != -1) e.add(Q_LAST_MOD, new Dtm(s, info).string(info)); s = ze.getCompressedSize(); if (s != -1) e.add(Q_COMP_SIZE, token(s)); e.add(ze.getName()); vb.add(e); } return vb; } catch (final IOException ex) { Util.debug(ex); throw ARCH_FAIL.thrw(info, ex); } finally { in.close(); } } /** * Extracts text entries. * * @param ctx query context * @return text entry * @throws QueryException query exception */ private ValueBuilder extractText(final QueryContext ctx) throws QueryException { final String enc = encoding(2, ARCH_ENCODING, ctx); final ValueBuilder vb = new ValueBuilder(); for (final byte[] b : extract(ctx)) vb.add(Str.get(encode(b, enc))); return vb; } /** * Extracts binary entries. * * @param ctx query context * @return binary entry * @throws QueryException query exception */ private ValueBuilder extractBinary(final QueryContext ctx) throws QueryException { final ValueBuilder vb = new ValueBuilder(); for (final byte[] b : extract(ctx)) vb.add(new B64(b)); return vb; } /** * Updates an archive. * * @param ctx query context * @return updated archive * @throws QueryException query exception */ private B64 update(final QueryContext ctx) throws QueryException { final B64 archive = (B64) checkType(checkItem(expr[0], ctx), AtomType.B64); // entries to be updated final TokenObjMap<Item[]> hm = new TokenObjMap<Item[]>(); final Iter entr = ctx.iter(expr[1]); final Iter cont = ctx.iter(expr[2]); int e = 0; int c = 0; Item en, cn; while (true) { en = entr.next(); cn = cont.next(); if (en == null || cn == null) break; hm.add(checkElmStr(en).string(info), new Item[] {en, cn}); e++; c++; } // count remaining entries if (cn != null) do c++; while (cont.next() != null); if (en != null) do e++; while (entr.next() != null); if (e != c) ARCH_DIFF.thrw(info, e, c); final ArchiveIn in = ArchiveIn.get(archive.input(info), info); final ArchiveOut out = ArchiveOut.get(in.format(), info); try { if (in instanceof GZIPIn) ARCH_MODIFY.thrw(info, in.format().toUpperCase(Locale.ENGLISH)); // delete entries to be updated while (in.more()) if (!hm.contains(token(in.entry().getName()))) out.write(in); // add new and updated entries for (final byte[] h : hm) { if (h == null) continue; final Item[] it = hm.get(h); add(it[0], it[1], out, ZipEntry.DEFLATED); } } catch (final IOException ex) { Util.debug(ex); ARCH_FAIL.thrw(info, ex); } finally { in.close(); out.close(); } return new B64(out.toArray()); } /** * Deletes files from an archive. * * @param ctx query context * @return updated archive * @throws QueryException query exception */ private B64 delete(final QueryContext ctx) throws QueryException { final B64 archive = (B64) checkType(checkItem(expr[0], ctx), AtomType.B64); // entries to be deleted final TokenObjMap<Item[]> hm = new TokenObjMap<Item[]>(); final Iter names = ctx.iter(expr[1]); for (Item en; (en = names.next()) != null; ) { hm.add(checkElmStr(en).string(info), null); } final ArchiveIn in = ArchiveIn.get(archive.input(info), info); final ArchiveOut out = ArchiveOut.get(in.format(), info); try { if (in instanceof GZIPIn) ARCH_MODIFY.thrw(info, in.format().toUpperCase(Locale.ENGLISH)); while (in.more()) if (!hm.contains(token(in.entry().getName()))) out.write(in); } catch (final IOException ex) { Util.debug(ex); ARCH_FAIL.thrw(info, ex); } finally { in.close(); out.close(); } return new B64(out.toArray()); } /** * Extracts entries from the archive. * * @param ctx query context * @return text entries * @throws QueryException query exception */ private TokenList extract(final QueryContext ctx) throws QueryException { final B64 archive = (B64) checkType(checkItem(expr[0], ctx), AtomType.B64); TokenSet hs = null; if (expr.length > 1) { // filter result to specified entries hs = new TokenSet(); final Iter names = ctx.iter(expr[1]); for (Item en; (en = names.next()) != null; ) { hs.add(checkElmStr(en).string(info)); } } final TokenList tl = new TokenList(); final ArchiveIn in = ArchiveIn.get(archive.input(info), info); try { while (in.more()) { final ZipEntry ze = in.entry(); if (ze.isDirectory()) continue; if (hs == null || hs.delete(token(ze.getName())) != 0) tl.add(in.read()); } } catch (final IOException ex) { Util.debug(ex); ARCH_FAIL.thrw(info, ex); } finally { in.close(); } return tl; } /** * Adds the specified entry to the output stream. * * @param entry entry descriptor * @param con contents * @param out output archive * @param level default compression level * @throws QueryException query exception * @throws IOException I/O exception */ private void add(final Item entry, final Item con, final ArchiveOut out, final int level) throws QueryException, IOException { // create new zip entry final String name = string(entry.string(info)); if (name.isEmpty()) ARCH_EMPTY.thrw(info); final ZipEntry ze = new ZipEntry(name); String en = null; // compression level byte[] lvl = null; if (entry instanceof ANode) { final ANode el = (ANode) entry; lvl = el.attribute(Q_LEVEL); // last modified final byte[] mod = el.attribute(Q_LAST_MOD); if (mod != null) { try { ze.setTime(new Int(new Dtm(mod, info)).itr()); } catch (final QueryException qe) { ARCH_DATETIME.thrw(info, mod); } } // encoding final byte[] enc = el.attribute(Q_ENCODING); if (enc != null) { en = string(enc); if (!Charset.isSupported(en)) ARCH_ENCODING.thrw(info, enc); } } // data to be compressed byte[] val = checkStrBin(con); if (con instanceof AStr && en != null && en != Token.UTF8) val = encode(val, en); try { out.level(lvl == null ? level : toInt(lvl)); } catch (final IllegalArgumentException ex) { ARCH_LEVEL.thrw(info, lvl); } out.write(ze, val); } /** * Encodes the specified string to another encoding. * * @param val value to be encoded * @param en encoding * @return encoded string * @throws QueryException query exception */ private byte[] encode(final byte[] val, final String en) throws QueryException { try { return FNConvert.toString(new ArrayInput(val), en); } catch (final IOException ex) { throw ARCH_ENCODE.thrw(info, ex); } } /** * Checks if the specified item is a string or element. * * @param it item to be checked * @return item * @throws QueryException query exception */ private Item checkElmStr(final Item it) throws QueryException { if (it instanceof AStr || TEST.eq(it)) return it; throw ELMSTRTYPE.thrw(info, Q_ENTRY.string(), it.type); } }