/*
* Copyright (C) 2014 Jörg Prante
*
* Licensed 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.xbib.io.archive;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.logging.ESLogger;
import org.elasticsearch.common.logging.ESLoggerFactory;
import org.xbib.io.BytesProgressWatcher;
import org.xbib.io.Session;
import org.xbib.io.StringPacket;
import org.xbib.io.compress.CompressCodecService;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Date;
import java.util.EnumSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
/**
* A basic archive session
*/
public abstract class ArchiveSession<I extends ArchiveInputStream, O extends ArchiveOutputStream> implements Session<StringPacket> {
private final static ESLogger logger = ESLoggerFactory.getLogger(ArchiveSession.class.getSimpleName());
private final static CompressCodecService codecService = CompressCodecService.getInstance();
private final static ArchiveService archiveService = ArchiveService.getInstance();
private boolean isOpen;
private EnumSet<Mode> mode;
private File file;
private Path path;
private I in;
private O out;
private BytesProgressWatcher watcher;
private long packetCounter;
private AtomicLong archiveCounter = new AtomicLong();
protected ArchiveSession(BytesProgressWatcher watcher) {
this.watcher = watcher;
this.packetCounter = 0L;
}
public BytesProgressWatcher getWatcher() {
if (watcher == null) {
watcher = new BytesProgressWatcher(0L);
}
return watcher;
}
public long getPacketCounter() {
return packetCounter;
}
protected abstract String getName();
@Override
public synchronized void open(EnumSet<Mode> mode, Path path) throws IOException {
if (isOpen) {
return;
}
this.mode = mode;
this.path = path;
this.file = path.toFile();
if (mode.contains(Mode.READ)) {
this.in = createArchiveInputStream();
this.isOpen = this.in != null;
if (!isOpen) {
throw new FileNotFoundException("can't open for input, check existence or access rights: " + file.getAbsolutePath());
}
} else if (mode.contains(Mode.WRITE)) {
this.out = createArchiveOutputStream(false);
this.isOpen = this.out != null;
if (!isOpen) {
throw new FileNotFoundException("can't open for output, check existence or access rights: " + file.getAbsolutePath());
}
} else if (mode.contains(Mode.OVERWRITE)) {
this.out = createArchiveOutputStream(true);
this.isOpen = this.out != null;
if (!isOpen) {
throw new FileNotFoundException("can't open for output, check existence or access rights: " + file.getAbsolutePath());
}
}
}
@SuppressWarnings("unchecked")
private I createArchiveInputStream() throws IOException {
I archiveIn;
FileInputStream in;
if (file.isFile() && file.canRead()) {
in = new FileInputStream(file);
} else {
throw new FileNotFoundException("can't open for input, check existence or access rights: " + path);
}
String pathStr = path.toString();
Set<String> streamCodecs = CompressCodecService.getCodecs();
for (String codec : streamCodecs) {
if (pathStr.endsWith("." + codec)) {
archiveIn = (I) archiveService.getCodec(getName()).createArchiveInputStream(codecService.getCodec(codec).decode(in));
archiveIn.setWatcher(watcher);
return archiveIn;
}
}
archiveIn = (I) archiveService.getCodec(getName()).createArchiveInputStream(in);
archiveIn.setWatcher(watcher);
return archiveIn;
}
@SuppressWarnings("unchecked")
private O createArchiveOutputStream(boolean overwrite) throws IOException {
O archiveOut;
FileOutputStream out;
if (!file.exists() || file.length() == 0 || overwrite) {
if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
throw new IOException("can not create directory: " + file.getParent());
}
out = new FileOutputStream(file);
} else {
throw new FileNotFoundException("can't open for output, check existence or access rights: " + file.getAbsolutePath());
}
String pathStr = path.toString();
Set<String> streamCodecs = CompressCodecService.getCodecs();
for (String codec : streamCodecs) {
if (pathStr.endsWith("." + codec)) {
archiveOut = (O) archiveService.getCodec(getName()).createArchiveOutputStream(codecService.getCodec(codec).encode(out));
archiveOut.setWatcher(watcher);
return archiveOut;
}
}
archiveOut = (O) archiveService.getCodec(getName()).createArchiveOutputStream(out);
archiveOut.setWatcher(watcher);
return archiveOut;
}
@Override
public StringPacket newPacket() {
return new StringPacket();
}
@Override
public synchronized StringPacket read() throws IOException {
if (!isOpen()) {
throw new IOException("not open");
}
if (in == null) {
throw new IOException("no input stream found");
}
ArchiveEntry entry = in.getNextEntry();
if (entry == null) {
return null;
}
StringPacket packet = newPacket();
String name = entry.getName();
packet.meta("name", name);
ArchiveUtils.decodeArchiveEntryName(packet, name);
int size = (int) entry.getEntrySize();
if (size >= 0) {
byte[] b = new byte[size]; // naive but fast, heap may explode
int num = in.read(b, 0, size); // fill byte array from stream
packet.payload(new String(b, "UTF-8"));
} else {
// slow copy, unknown size (zip deflate method)
ByteArrayOutputStream b = new ByteArrayOutputStream();
Streams.copy(in, b);
packet.payload(new String(b.toByteArray(), "UTF-8"));
}
packetCounter++;
return packet;
}
@Override
@SuppressWarnings("unchecked")
public synchronized void write(StringPacket packet) throws IOException {
if (!isOpen()) {
throw new IOException("not open");
}
if (out == null) {
throw new IOException("no output stream found");
}
if (packet == null || packet.payload() == null) {
throw new IOException("no payload to write for entry");
}
byte[] buf = packet.payload().getBytes("UTF-8");
String name = ArchiveUtils.encodeArchiveEntryName(packet);
ArchiveEntry entry = out.newArchiveEntry();
entry.setName(name);
entry.setLastModified(new Date());
entry.setEntrySize(buf.length);
out.putArchiveEntry(entry);
out.write(buf);
out.closeArchiveEntry();
packetCounter++;
if (watcher.getBytesToTransfer() != 0 && watcher.getBytesTransferred() > watcher.getBytesToTransfer()) {
logger.debug("bytes watcher: transferred = {}, rate {}",
watcher.getBytesTransferred(), watcher.getRecentByteRatePerSecond());
switchToNextArchive();
watcher.resetWatcher();
}
}
@Override
public synchronized void close() throws IOException {
if (!isOpen) {
return;
}
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
this.isOpen = false;
}
@Override
public boolean isOpen() {
return isOpen;
}
/**
* Switches to next archive stream if a certain byte limit was set
*
* @throws IOException
*/
private void switchToNextArchive() throws IOException {
close();
String filename = file.getName();
String prefix = Long.toString(archiveCounter.get()) + ".";
if (filename.startsWith(prefix)) {
filename = filename.substring(prefix.length());
}
filename = archiveCounter.incrementAndGet() + "." + filename;
this.file = new File(file.getParent() + File.separator + filename);
this.path = file.toPath();
open(mode, path);
}
}