blob: c444292d8e92f5e60118221df9fa71cd640c9fbd [file] [log] [blame]
/*
* Copyright 2000-2013 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.
*/
/*
* @author max
*/
package com.intellij.util.io.storage;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.Forceable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.util.io.PagePool;
import com.intellij.util.io.RandomAccessDataFile;
import gnu.trove.TIntArrayList;
import java.io.File;
import java.io.IOException;
import java.text.MessageFormat;
public abstract class AbstractRecordsTable implements Disposable, Forceable {
private static final Logger LOG = Logger.getInstance("com.intellij.util.io.storage.AbstractRecordsTable");
private static final int HEADER_MAGIC_OFFSET = 0;
private static final int HEADER_VERSION_OFFSET = 4;
protected static final int DEFAULT_HEADER_SIZE = 8;
private static final int VERSION = 5;
private static final int DIRTY_MAGIC = 0x12ad34e4;
private static final int SAFELY_CLOSED_MAGIC = 0x1f2f3f4f + VERSION;
private static final int ADDRESS_OFFSET = 0;
private static final int SIZE_OFFSET = ADDRESS_OFFSET + 8;
private static final int CAPACITY_OFFSET = SIZE_OFFSET + 4;
protected static final int DEFAULT_RECORD_SIZE = CAPACITY_OFFSET + 4;
protected final RandomAccessDataFile myStorage;
private TIntArrayList myFreeRecordsList = null;
private boolean myIsDirty = false;
public AbstractRecordsTable(final File storageFilePath, final PagePool pool) throws IOException {
myStorage = new RandomAccessDataFile(storageFilePath, pool);
if (myStorage.length() == 0) {
myStorage.put(0, new byte[getHeaderSize()], 0, getHeaderSize());
markDirty();
}
else {
if (myStorage.getInt(HEADER_MAGIC_OFFSET) != getSafelyClosedMagic()) {
myStorage.dispose();
throw new IOException("Records table for '" + storageFilePath + "' haven't been closed correctly. Rebuild required.");
}
}
}
private int getSafelyClosedMagic() {
return SAFELY_CLOSED_MAGIC + getImplVersion();
}
protected int getHeaderSize() {
return DEFAULT_HEADER_SIZE;
}
protected abstract int getImplVersion();
protected abstract int getRecordSize();
protected abstract byte[] getZeros();
public int createNewRecord() throws IOException {
markDirty();
ensureFreeRecordsScanned();
if (myFreeRecordsList.isEmpty()) {
int result = getRecordsCount() + 1;
doCleanRecord(result);
if (getRecordsCount() != result) LOG.error("Failed to correctly allocate new record in: " + myStorage.getFile());
return result;
}
else {
final int result = myFreeRecordsList.remove(myFreeRecordsList.size() - 1);
assert getSize(result) == -1;
setSize(result, 0);
return result;
}
}
public int getRecordsCount() throws IOException {
int recordsLength = (int)myStorage.length() - getHeaderSize();
if ((recordsLength % getRecordSize()) != 0) {
throw new IOException(MessageFormat.format("Corrupted records: storageLength={0} recordsLength={1} recordSize={2}",
myStorage.length(), recordsLength, getRecordSize()));
}
return recordsLength / getRecordSize();
}
private void ensureFreeRecordsScanned() throws IOException {
if (myFreeRecordsList == null) {
myFreeRecordsList = scanForFreeRecords();
}
}
private TIntArrayList scanForFreeRecords() throws IOException {
final TIntArrayList result = new TIntArrayList();
for (int i = 1; i <= getRecordsCount(); i++) {
if (getSize(i) == -1) {
result.add(i);
}
}
return result;
}
private void doCleanRecord(int record) {
myStorage.put(getOffset(record, 0), getZeros(), 0, getRecordSize());
}
public long getAddress(int record) {
return myStorage.getLong(getOffset(record, ADDRESS_OFFSET));
}
public void setAddress(int record, long address) {
markDirty();
myStorage.putLong(getOffset(record, ADDRESS_OFFSET), address);
}
public int getSize(int record) {
return myStorage.getInt(getOffset(record, SIZE_OFFSET));
}
public void setSize(int record, int size) {
markDirty();
myStorage.putInt(getOffset(record, SIZE_OFFSET), size);
}
public int getCapacity(int record) {
return myStorage.getInt(getOffset(record, CAPACITY_OFFSET));
}
public void setCapacity(int record, int capacity) {
markDirty();
myStorage.putInt(getOffset(record, CAPACITY_OFFSET), capacity);
}
protected int getOffset(int record, int section) {
assert record > 0;
return getHeaderSize() + (record - 1) * getRecordSize() + section;
}
public void deleteRecord(final int record) throws IOException {
markDirty();
ensureFreeRecordsScanned();
doCleanRecord(record);
setSize(record, -1);
myFreeRecordsList.add(record);
}
public int getVersion() {
return myStorage.getInt(HEADER_VERSION_OFFSET);
}
public void setVersion(final int expectedVersion) {
markDirty();
myStorage.putInt(HEADER_VERSION_OFFSET, expectedVersion);
}
@Override
public void dispose() {
if (!myStorage.isDisposed()) {
markClean();
myStorage.dispose();
}
}
@Override
public void force() {
markClean();
myStorage.force();
}
public boolean flushSome(int maxPages) {
myStorage.flushSomePages(maxPages);
if (!myStorage.isDirty()) {
force();
return true;
}
return false;
}
@Override
public boolean isDirty() {
return myIsDirty || myStorage.isDirty();
}
public void markDirty() {
if (!myIsDirty) {
myIsDirty = true;
myStorage.putInt(HEADER_MAGIC_OFFSET, DIRTY_MAGIC);
}
}
private void markClean() {
if (myIsDirty) {
myIsDirty = false;
myStorage.putInt(HEADER_MAGIC_OFFSET, getSafelyClosedMagic());
}
}
}