/*
* Copyright 2012 b1.org
*
* 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.b1.pack.standard.writer;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.b1.pack.api.builder.Writable;
import org.b1.pack.api.common.EncryptionMethod;
import org.b1.pack.api.writer.WriterProvider;
import org.b1.pack.standard.common.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
class BlockWriter extends ChunkWriter {
private final String archiveId;
private final List<VolumeWriter> suspendedWriters = Lists.newArrayList();
private final MemoryOutputStream readyContent = new MemoryOutputStream();
private final WriterProvider provider;
private final String compressionMethod;
private CompositeWritable suspendedContent = new CompositeWritable();
private PackCipher packCipher;
private VolumeWriter volumeWriter;
private RecordPointer catalogPointer;
private Long objectCount;
private long maxContentSize;
private boolean compressed;
private boolean firstBlockInChunk;
public BlockWriter(WriterProvider provider, String compressionMethod) {
this.provider = provider;
this.compressionMethod = compressionMethod;
EncryptionMethod method = provider.getEncryptionMethod();
if (method == null) {
archiveId = Volumes.createArchiveId();
} else {
Preconditions.checkArgument(EncryptionMethod.AES.equals(method.getName()), "Unsupported encryption method: %s", method.getName());
byte[] salt = Volumes.generateRandomBytes(32);
archiveId = Volumes.encodeBase64(salt);
packCipher = new PackCipher(method.getPassword(), salt, method.getIterationCount());
}
}
@Override
public RecordPointer getCurrentPointer() throws IOException {
ensureFreeSpace();
return new RecordPointer(volumeWriter.getVolumeNumber(), volumeWriter.getStreamEnd(), getContentSize());
}
public void setObjectCount(Long objectCount) {
this.objectCount = objectCount;
}
public void setCompressed(boolean compressed) throws IOException {
flushContent();
this.compressed = compressed;
firstBlockInChunk = true;
}
public RecordPointer saveCatalogPointer() throws IOException {
flushContent();
if (catalogPointer != null) {
return getCurrentPointer();
}
catalogPointer = getCurrentPointer();
if (volumeWriter != null) {
volumeWriter.setCatalogPointer(catalogPointer);
maxContentSize = getMaxContentSize();
if (maxContentSize <= 0) {
volumeWriter.setCatalogPointer(null);
catalogPointer = getCurrentPointer();
volumeWriter.setCatalogPointer(catalogPointer);
}
}
return catalogPointer;
}
@Override
public void write(int b) throws IOException {
ensureFreeSpace();
readyContent.write(b);
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
while (len > 0) {
int size = Ints.checkedCast(Math.min(len, ensureFreeSpace()));
readyContent.write(b, off, size);
off += size;
len -= size;
}
}
@Override
public void write(Writable value) throws IOException {
long off = 0;
long len = value.getSize();
while (len > 0) {
long size = Math.min(len, ensureFreeSpace());
suspendReadyContent();
suspendedContent.add(new PartialWritable(value, off, off + size));
off += size;
len -= size;
}
}
public void save() throws IOException {
flushContent();
for (VolumeWriter writer : suspendedWriters) {
writer.close(false);
}
suspendedWriters.clear();
volumeWriter.flush();
}
@Override
public void close() throws IOException {
save();
volumeWriter.close(true);
}
public void cleanup() {
for (VolumeWriter writer : suspendedWriters) {
writer.cleanup();
}
if (volumeWriter != null) {
volumeWriter.cleanup();
}
}
private long getContentSize() {
return suspendedContent.getSize() + readyContent.size();
}
private void suspendReadyContent() {
if (readyContent.size() > 0) {
suspendedContent.add(new ByteArrayWritable(readyContent.toByteArray()));
readyContent.reset();
}
}
private long ensureFreeSpace() throws IOException {
long result = maxContentSize - getContentSize();
if (result > 0) {
return result;
}
if (volumeWriter == null) {
volumeWriter = createVolumeWriter(1);
} else {
flushContent();
}
long contentSize = getMaxContentSize();
if (contentSize <= 0) {
completeVolumeWriter();
volumeWriter = createVolumeWriter(volumeWriter.getVolumeNumber() + 1);
contentSize = getMaxContentSize();
Preconditions.checkArgument(contentSize > 0, "Volume size too small");
}
return maxContentSize = contentSize;
}
private VolumeWriter createVolumeWriter(long volumeNumber) throws IOException {
return new VolumeWriter(archiveId, volumeNumber, objectCount, compressionMethod, provider.getMaxVolumeSize(),
provider.getVolume(volumeNumber), catalogPointer, packCipher == null ? null : packCipher.getVolumeCipher(volumeNumber));
}
private void completeVolumeWriter() throws IOException {
if (volumeWriter.isSuspended()) {
suspendedWriters.add(volumeWriter);
} else {
volumeWriter.close(false);
}
}
private long getMaxContentSize() {
long space = volumeWriter.getFreeSpace();
long size = Math.min(space, Constants.MAX_CHUNK_SIZE);
long contentSize = size - Math.max(0, getBlockSize(size) - space);
return contentSize >= Constants.MIN_CHUNK_SIZE ? contentSize : 0;
}
private long getBlockSize(final long chunkSize) {
return createBlock(new Writable() {
@Override
public long getSize() {
return chunkSize;
}
@Override
public void writeTo(OutputStream stream, long start, long end) throws IOException {
throw new UnsupportedOperationException();
}
}).getSize();
}
private void flushContent() throws IOException {
if (suspendedContent.getSize() > 0) {
suspendReadyContent();
volumeWriter.suspendBlock(createBlock(suspendedContent));
suspendedContent = new CompositeWritable();
afterFlush();
} else if (readyContent.size() > 0) {
volumeWriter.writeBlock(createBlock(new ByteArrayWritable(readyContent.getBuf(), readyContent.size())));
readyContent.reset();
afterFlush();
}
}
private void afterFlush() {
maxContentSize = 0;
firstBlockInChunk = false;
}
private PbBlock createBlock(Writable content) {
PbPlainBlock block = new PbPlainBlock(content);
return compressed ? PbBlock.wrapLzmaBlock(firstBlockInChunk, block) : PbBlock.wrapPlainBlock(block);
}
}