/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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 com.intellij.util.io;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.util.Processor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.ByteBuffer;
/**
* Created by Maxim.Mossienko on 1/9/14.
*/
public class AppendableStorageBackedByResizableMappedFile extends ResizeableMappedFile {
private final MyDataIS myReadStream;
private byte[] myAppendBuffer;
private volatile int myFileLength;
private volatile int myBufferPosition;
private static final int ourAppendBufferLength = 4096;
public AppendableStorageBackedByResizableMappedFile(final File file,
int initialSize,
@Nullable PagedFileStorage.StorageLockContext lockContext,
int pageSize,
boolean valuesAreBufferAligned) throws IOException {
super(file, initialSize, lockContext, pageSize, valuesAreBufferAligned);
myReadStream = new MyDataIS(this);
myFileLength = (int)length();
myCompressedAppendableFile = /*CompressedAppendableFile.ENABLED &&*/ false ? new MyCompressedAppendableFile(file) :null;
}
private void flushKeyStoreBuffer() {
if (myBufferPosition > 0) {
put(myFileLength, myAppendBuffer, 0, myBufferPosition);
myFileLength += myBufferPosition;
myBufferPosition = 0;
}
}
@Override
public void force() {
flushKeyStoreBuffer();
if (myCompressedAppendableFile != null) myCompressedAppendableFile.force();
super.force();
}
@Override
public void close() {
flushKeyStoreBuffer();
if (myCompressedAppendableFile != null) myCompressedAppendableFile.dispose();
super.close();
}
private final CompressedAppendableFile myCompressedAppendableFile;
private final boolean testMode = false;
public <Data> Data read(final int addr, KeyDescriptor<Data> descriptor) throws IOException {
Data tempData = null;
if (myCompressedAppendableFile != null) {
tempData = myCompressedAppendableFile.read(addr, descriptor);
if (!testMode) return tempData;
}
if (myFileLength <= addr) {
Data data =
descriptor.read(new DataInputStream(new UnsyncByteArrayInputStream(myAppendBuffer, addr - myFileLength, myBufferPosition)));
if (tempData != null && !descriptor.isEqual(data, tempData)) {
assert false;
}
return data;
}
// we do not need to flushKeyBuffer since we store complete records
myReadStream.setup(addr, myFileLength);
Data data = descriptor.read(myReadStream);
if (tempData != null && !descriptor.isEqual(data, tempData)) {
assert false;
}
return data;
}
public <Data> boolean processAll(Processor<Data> processor, KeyDescriptor<Data> descriptor) throws IOException {
assert !isDirty();
DataInputStream keysStream2 = myCompressedAppendableFile != null ?myCompressedAppendableFile.getStream(0) : null;
if (!testMode && keysStream2 != null) {
getPagedFileStorage().lock(); // todo support it inside myCompressedAppendableFile to avoid filling the cache
try {
try {
while (true) {
Data key = descriptor.read(keysStream2);
if (!processor.process(key)) return false;
}
}
catch (EOFException e) {
// Done
}
return true;
}
finally {
getPagedFileStorage().unlock();
keysStream2.close();
}
}
DataInputStream keysStream = new DataInputStream(new BufferedInputStream(new LimitedInputStream(new FileInputStream(getPagedFileStorage().getFile()),
myFileLength) {
@Override
public int available() throws IOException {
return remainingLimit();
}
}, 32768));
try {
try {
while (true) {
Data key = descriptor.read(keysStream);
if (keysStream2 != null) {
Data tempKey = descriptor.read(keysStream2);
if (!descriptor.isEqual(key, tempKey)) {
assert false;
}
}
if (!processor.process(key)) return false;
}
}
catch (EOFException e) {
// Done
}
return true;
}
finally {
keysStream.close();
}
}
public int getCurrentLength() {
int currentLength;
if (myCompressedAppendableFile != null) {
currentLength = (int)myCompressedAppendableFile.length();
if (testMode) {
if (currentLength != myBufferPosition + myFileLength) {
assert false;
}
}
} else {
currentLength = myBufferPosition + myFileLength;
}
return currentLength;
}
public <Data> int append(Data value, KeyDescriptor<Data> descriptor) throws IOException {
final BufferExposingByteArrayOutputStream bos = new BufferExposingByteArrayOutputStream();
DataOutput out = new DataOutputStream(bos);
descriptor.save(out, value);
final int size = bos.size();
final byte[] buffer = bos.getInternalBuffer();
int currentLength = getCurrentLength();
if (myCompressedAppendableFile != null) {
//myCompressedAppendableFile.append(value, descriptor);
myCompressedAppendableFile.append(buffer, size);
if (!testMode) return currentLength;
}
if (size > ourAppendBufferLength) {
flushKeyStoreBuffer();
put(currentLength, buffer, 0, size);
myFileLength += size;
}
else {
if (size > ourAppendBufferLength - myBufferPosition) {
flushKeyStoreBuffer();
}
// myAppendBuffer will contain complete records
if (myAppendBuffer == null) {
myAppendBuffer = new byte[ourAppendBufferLength];
}
System.arraycopy(buffer, 0, myAppendBuffer, myBufferPosition, size);
myBufferPosition += size;
}
return currentLength;
}
public <Data> boolean checkBytesAreTheSame(final int addr, Data value, KeyDescriptor<Data> descriptor) throws IOException {
final DataInputStream compressedStream;
final boolean[] sameValue = new boolean[1];
OutputStream comparer;
if (myCompressedAppendableFile != null) {
compressedStream = myCompressedAppendableFile.getStream(addr);
comparer = new OutputStream() {
boolean same = true;
@Override
public void write(int b) throws IOException {
if (same) {
same = compressedStream.readByte() == (byte)b;
}
}
@Override
public void close() {
sameValue[0] = same;
}
};
} else {
comparer = buildOldComparerStream(addr, sameValue);
}
DataOutput out = new DataOutputStream(comparer);
descriptor.save(out, value);
comparer.close();
if (testMode) {
final boolean[] sameValue2 = new boolean[1];
OutputStream comparer2 = buildOldComparerStream(addr, sameValue2);
out = new DataOutputStream(comparer2);
descriptor.save(out, value);
comparer2.close();
if (sameValue[0] != sameValue2[0]) {
assert false;
}
}
return sameValue[0];
}
@NotNull
private OutputStream buildOldComparerStream(final int addr, final boolean[] sameValue) {
OutputStream comparer;
final PagedFileStorage storage = getPagedFileStorage();
if (myFileLength <= addr) {
//noinspection IOResourceOpenedButNotSafelyClosed
comparer = new OutputStream() {
int address = addr - myFileLength;
boolean same = true;
@Override
public void write(int b) {
if (same) {
same = address < myBufferPosition && myAppendBuffer[address++] == (byte)b;
}
}
@Override
public void close() {
sameValue[0] = same;
}
};
}
else {
//noinspection IOResourceOpenedButNotSafelyClosed
comparer = new OutputStream() {
int base = addr;
int address = storage.getOffsetInPage(addr);
boolean same = true;
ByteBuffer buffer = storage.getByteBuffer(addr, false).getCachedBuffer();
final int myPageSize = storage.myPageSize;
@Override
public void write(int b) {
if (same) {
if (myPageSize == address && address < myFileLength) { // reached end of current byte buffer
base += address;
buffer = storage.getByteBuffer(base, false).getCachedBuffer();
address = 0;
}
same = address < myFileLength && buffer.get(address++) == (byte)b;
}
}
@Override
public void close() {
sameValue[0] = same;
}
};
}
return comparer;
}
private static class MyDataIS extends DataInputStream {
private MyDataIS(ResizeableMappedFile raf) {
super(new MyBufferedIS(new MappedFileInputStream(raf, 0, 0)));
}
public void setup(long pos, long limit) {
((MyBufferedIS)in).setup(pos, limit);
}
}
private static class MyBufferedIS extends BufferedInputStream {
public MyBufferedIS(final InputStream in) {
super(in, 512);
}
public void setup(long pos, long limit) {
this.pos = 0;
count = 0;
((MappedFileInputStream)in).setup(pos, limit);
}
}
private class MyCompressedAppendableFile extends CompressedAppendableFile {
private final File myFile;
private DataOutputStream myChunkLengthTableStream;
public MyCompressedAppendableFile(File file) {
super(AppendableStorageBackedByResizableMappedFile.this.getPagedFileStorage().getFile());
myFile = file;
}
@NotNull
@Override
protected InputStream getChunkInputStream(File appendFile, long offset, int pageSize) throws IOException {
byte[] buf = new byte[pageSize];
get(offset, buf, 0, pageSize);
return new ByteArrayInputStream(buf);
}
@Override
protected void saveChunk(BufferExposingByteArrayOutputStream compressedChunk, long endOfFileOffset) throws IOException {
AppendableStorageBackedByResizableMappedFile.super
.put(endOfFileOffset, compressedChunk.getInternalBuffer(), 0, compressedChunk.size());
if (myChunkLengthTableStream == null) {
myChunkLengthTableStream =
new DataOutputStream(new BufferedOutputStream(new FileOutputStream(getChunkLengthFile(), true)));
}
DataInputOutputUtil.writeINT(myChunkLengthTableStream, compressedChunk.size());
}
@Override
public synchronized void force() {
super.force();
try {
if (myChunkLengthTableStream != null) myChunkLengthTableStream.flush();
} catch (IOException ignore) {}
}
@Override
public synchronized void dispose() {
super.dispose();
if (myChunkLengthTableStream != null) {
try {
myChunkLengthTableStream.close();
} catch (IOException ignore) {}
}
}
@NotNull
@Override
protected File getChunksFile() {
return myFile;
}
}
}