blob: cfe83f60de0c96f0bcadd84171e6a50a09fa8863 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.jackrabbit.vault.fs.io;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.annotation.Nonnull;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.CloseShieldInputStream;
import org.apache.jackrabbit.vault.fs.api.VaultInputSource;
import org.apache.jackrabbit.vault.fs.config.ConfigurationException;
import org.apache.jackrabbit.vault.fs.config.DefaultMetaInf;
import org.apache.jackrabbit.vault.fs.config.MetaInf;
import org.apache.jackrabbit.vault.fs.config.VaultSettings;
import org.apache.jackrabbit.vault.util.Constants;
import org.apache.jackrabbit.vault.util.Text;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Implements an archive based on a zip stream, but deflates the entries first into a buffer and later into a temporary
* file, if the content length exceeds the buffer size.
*/
public class ZipStreamArchive extends AbstractArchive {
/**
* default logger
*/
private static final Logger log = LoggerFactory.getLogger(ZipStreamArchive.class);
/**
* max allowed package size for using a memory archive
*/
private static final int DEFAULT_BUFFER_SIZE = 1024*1024;
/**
* the input stream that is consumed in this archive
*/
private InputStream in;
/**
* the temporary file if the stream needs to be copied to disk.
*/
private File tmpFile;
/**
* A random access file of the temp file
*/
private RandomAccessFile raf;
/**
* the decompressed data of the stream if the contents are small.
*/
private byte[] decompressed;
/**
* the maximum buffer size
*/
private final int maxBufferSize;
/**
* the current write position into the decompressed buffer
*/
private int pos;
/**
* the root entry of this archive
*/
private EntryImpl root;
/**
* the meta info that is loaded in this archive
*/
private DefaultMetaInf inf;
/**
* internal buffer used for copying.
*/
private final byte[] buffer = new byte[0x10000];
/**
* Creates a new zip stream archive on the given input stream.
* @param in the input stream to read from.
*/
public ZipStreamArchive(@Nonnull InputStream in) {
this(in, DEFAULT_BUFFER_SIZE);
}
/**
* Creates an ew zip stream archive on the given input stream.
* @param in the input stream to read from.
* @param maxBufferSize size of buffer to keep content in memory.
*/
public ZipStreamArchive(@Nonnull InputStream in, int maxBufferSize) {
this.in = in;
this.maxBufferSize = maxBufferSize;
}
@Override
public void open(boolean strict) throws IOException {
if (raf != null || decompressed != null) {
return;
}
decompressed = new byte[maxBufferSize];
pos = 0;
root = new EntryImpl("");
inf = new DefaultMetaInf();
// scan the zip and copy data to temporary file
ZipInputStream zin = new ZipInputStream(in);
try {
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
String name = entry.getName();
// check for meta inf
if (name.startsWith(Constants.META_DIR + "/")) {
try {
inf.load(new CloseShieldInputStream(zin), "inputstream:" + name);
} catch (ConfigurationException e) {
throw new IOException(e);
}
}
String[] names = Text.explode(name, '/');
if (names.length > 0) {
EntryImpl je = root;
for (int i=0; i<names.length; i++) {
if (i == names.length -1 && !entry.isDirectory()) {
// copy stream
long pos = getPosition();
long len = copy(zin);
je = je.add(new EntryImpl(names[i], safeGetTime(entry), pos, len));
} else {
je = je.add(names[i]);
}
}
if (log.isDebugEnabled()) {
log.debug("scanning jar: {}", name);
}
}
}
if (inf.getFilter() == null) {
log.debug("Zip stream does not contain filter definition.");
}
if (inf.getConfig() == null) {
log.debug("Zip stream does not contain vault config.");
}
if (inf.getSettings() == null) {
log.debug("Zip stream does not contain vault settings. using default.");
VaultSettings settings = new VaultSettings();
settings.getIgnoredNames().add(".svn");
inf.setSettings(settings);
}
if (inf.getProperties() == null) {
log.debug("Zip stream does not contain properties.");
}
if (inf.getNodeTypes().isEmpty()) {
log.debug("Zip stream does not contain nodetypes.");
}
} finally {
IOUtils.closeQuietly(zin);
}
}
/**
* Gets the write position of either the random access file or the memory buffer.
* @return the write position.
* @throws IOException if an I/O error occurrs.
*/
private long getPosition() throws IOException {
if (raf != null) {
return raf.getFilePointer();
}
return pos;
}
/**
* Copies the input stream either into the random access file or the memory buffer.
* @param in the input stream to copy
* @return the number of bytes written to the destination.
* @throws IOException if an I/O error occurrs.
*/
private long copy(@Nonnull InputStream in) throws IOException {
if (raf != null) {
return copyToRaf(in);
}
return copyToBuffer(in);
}
/**
* Copies the input stream to the buffer but check for overflow. If the buffer size is exceeded, the entire buffer
* is copied to a random access file and the rest of the input stream is appended there.
* @param in the input stream to copy
* @return the number of bytes written to the destination.
* @throws IOException if an I/O error occurrs.
*/
private long copyToBuffer(@Nonnull InputStream in) throws IOException {
int read;
int total = 0;
while ((read = in.read(decompressed, pos, decompressed.length - pos)) > 0) {
total += read;
pos += read;
if (pos == decompressed.length) {
// switch to raf
tmpFile = File.createTempFile("__vlttmpbuffer", ".dat");
raf = new RandomAccessFile(tmpFile, "rw");
raf.write(decompressed);
decompressed = null;
return total + copyToRaf(in);
}
}
return total;
}
/**
* copies the input stream into the random access file
* @param in the input stream
* @return the total number of bytes copied
* @throws IOException if an error occurrs.
*/
private long copyToRaf(@Nonnull InputStream in) throws IOException {
int read;
int total = 0;
while ((read = in.read(buffer)) > 0) {
raf.write(buffer, 0, read);
total += read;
}
return total;
}
@Override
public InputStream openInputStream(Entry entry) throws IOException {
return createInputStream((EntryImpl) entry);
}
@Override
public VaultInputSource getInputSource(Entry entry) throws IOException {
return new RafInputSource((EntryImpl) entry);
}
@Override
public MetaInf getMetaInf() {
return inf;
}
@Override
public void close() {
if (in != null) {
IOUtils.closeQuietly(in);
}
if (raf != null) {
try {
raf.close();
} catch (IOException e) {
// ignore
}
raf = null;
}
if (tmpFile != null) {
FileUtils.deleteQuietly(tmpFile);
tmpFile = null;
}
if (decompressed != null) {
// keep array so isBuffered works after closing
decompressed = new byte[0];
}
}
@Override
public Entry getRoot() throws IOException {
return root;
}
/**
* Checks if this archive is currently buffered (and not using a temporary file).
* @return {@code true} if buffered.
*/
public boolean isBuffered() {
return decompressed != null;
}
/**
* creates an input stream that either read from the buffer or the random access file.
* @param entry the archive entry
* @return the input stream
*/
private InputStream createInputStream(@Nonnull EntryImpl entry) {
if (raf == null) {
return new ByteArrayInputStream(decompressed, (int) entry.pos, (int) entry.len);
}
return new RafInputStream(entry);
}
/**
* internal input source implementation that is based on entries of this archive.
*/
private class RafInputSource extends VaultInputSource {
private final EntryImpl entry;
private RafInputSource(EntryImpl entry) {
this.entry = entry;
}
@Override
public InputStream getByteStream() {
return createInputStream(entry);
}
public long getContentLength() {
return entry.len;
}
public long getLastModified() {
return entry.time;
}
}
/**
* internal input stream implementation that read from the random access file.
*/
private class RafInputStream extends InputStream {
private long pos;
private long end;
private long mark;
private RafInputStream(EntryImpl entry) {
pos = entry.pos;
end = pos + entry.len;
}
@Override
public int read() throws IOException {
if (pos < end) {
raf.seek(pos++);
return raf.read();
} else {
return -1;
}
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (pos >= end) {
return -1;
}
len = Math.min(len, (int) (end-pos));
raf.seek(pos);
int read = raf.read(b, off, len);
if (read < 0) {
return -1;
}
pos += read;
return read;
}
@Override
public int read(byte[] b) throws IOException {
return read(b, 0, b.length);
}
@Override
public long skip(long n) throws IOException {
if (pos >= end) {
return -1;
}
n = Math.min(n, end - pos);
pos+= n;
return n;
}
@Override
public int available() throws IOException {
return (int) (end - pos);
}
@Override
public void close() throws IOException {
// ignore
}
@Override
public void mark(int readlimit) {
mark = pos;
}
@Override
public void reset() throws IOException {
pos = mark;
}
@Override
public boolean markSupported() {
return true;
}
}
/**
* archive entry implementation
*/
private static class EntryImpl implements Entry {
public final String name;
public final long time;
public final long pos;
public final long len;
public Map<String, EntryImpl> children;
private EntryImpl(String name) {
this.name = name;
this.time = 0;
pos = -1;
len = 0;
}
private EntryImpl(String name, long time, long pos, long len) {
this.name = name;
this.time = time;
this.pos = pos;
this.len = len;
}
@Override
public String getName() {
return name;
}
@Override
public boolean isDirectory() {
return pos < 0;
}
public EntryImpl add(EntryImpl e) {
if (children == null) {
children = new LinkedHashMap<String, EntryImpl>();
}
children.put(e.getName(), e);
return e;
}
public EntryImpl add(String name) {
EntryImpl e;
if (children == null) {
children = new LinkedHashMap<String, EntryImpl>();
} else {
e = children.get(name);
if (e != null) {
return e;
}
}
e = new EntryImpl(name);
children.put(name, e);
return e;
}
@Override
public Collection<? extends Entry> getChildren() {
return children == null
? Collections.<EntryImpl>emptyList()
: children.values();
}
@Override
public Entry getChild(String name) {
return children == null ? null : children.get(name);
}
}
/**
* Safely returns the modification time of the zip entry or 0, if reading the time would
* result in an error. for example due to http://bugs.java.com/view_bug.do?bug_id=JDK-8184940
*
* @param e the zip entry
* @return the modification time
*/
private static long safeGetTime(ZipEntry e) {
try {
return e.getTime();
} catch (Exception e1) {
return 0;
}
}
}