/*$Id$*/
package at.jku.sii.sqlitereader;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import at.jku.sii.sqlitereader.annotator.SimpleAnnotator;
import at.jku.sii.sqlitereader.btree.BTreePage;
import at.jku.sii.sqlitereader.btree.BTreePages;
import at.jku.sii.sqlitereader.btree.TableCell;
import at.jku.sii.sqlitereader.io.ArrayDataInput;
import at.jku.sii.sqlitereader.io.FileDataInput;
import at.jku.sii.sqlitereader.model.SQLiteObject;
import at.jku.sii.sqlitereader.model.master.SqliteMasterTable;
import at.jku.sii.sqlitereader.page.FreeListPage;
import at.jku.sii.sqlitereader.page.LockBytePage;
import at.jku.sii.sqlitereader.page.Page;
import at.jku.sii.sqlitereader.page.PointerMapPage;
import at.jku.sii.sqlitereader.page.RawDataPage;
public final class SqliteDataBase implements Dumpable {
public enum FileFormat {
LEGACY, WAL
}
public static final long HEADER_SIZE = 100;
private final String fileName;
private final FileFormat fileFormatWrite;
private final FileFormat fileFormatRead;
private final int pageSize;
private final int numPages;
private final int fileChangeCounter;
private final DataBaseEncoding encoding;
private final boolean incrementalVacuumMode;
private final int userVersion;
private final int versionValid4Number;
private final int versionSqlite;
private final List<Page> pages;
private int firstFreeListPage;
private int numFreeListPages;
private final List<FreeListPage> freePages;
private final byte reservedPageSpace;
private final int largestBTreePage;
private final List<PointerMapPage> pointerMapPages;
private final LockBytePage lockBytePage;
private int schemaCookie;
private int schemaFormat;
private int defaultPageCacheSize;
private final SqliteMasterTable sql_master_table;
private final SimpleAnnotator annotator;
public SqliteDataBase(File dbFile) {
FileDataInput file = null;
this.annotator = new SimpleAnnotator();
try {
file = new FileDataInput(dbFile, this.annotator);
this.fileName = dbFile.getAbsolutePath();
file.annotate(0, (int) HEADER_SIZE, "Header");
// start of header
readMagicHeader(file);
this.pageSize = readPageSize(file);
this.fileFormatWrite = file.readByte() == 1 ? FileFormat.LEGACY : FileFormat.WAL;
file.annotateLastByte("writeFileFormat", this.fileFormatWrite);
this.fileFormatRead = file.readByte() == 1 ? FileFormat.LEGACY : FileFormat.WAL;
file.annotateLastByte("readFileFormat", this.fileFormatRead);
this.reservedPageSpace = file.readByte("reservedPageSpace");
byte maxEmbedded = file.readByte("maxEmbedded");
assert (maxEmbedded == 64);
byte minEmbedded = file.readByte("minEmbedded");
assert (minEmbedded == 32);
byte leafPayload = file.readByte("leafPayload");
assert (leafPayload == 32);
this.fileChangeCounter = file.readInt("fileChangeCounter");
int storedNumPages = file.readInt("storedNumPages");
this.firstFreeListPage = file.readInt("firstFreeListPage");
this.numFreeListPages = file.readInt("numFreeListPages");
this.schemaCookie = file.readInt("schemaCookie");
this.schemaFormat = file.readInt("schemaForamt"); // one of 1..4
this.defaultPageCacheSize = file.readInt("defaultPageCacheSize");
this.largestBTreePage = file.readInt("largestBTreePage");
this.encoding = readEncoding(file);
file.annotateLastInt("Encoding", this.encoding);
this.userVersion = file.readInt("userVersion");
this.incrementalVacuumMode = file.readInt() > 0;
file.annotateLastInt("incrementalVacuumMode", this.incrementalVacuumMode);
// reserved
byte[] reserved = new byte[24];
file.readFully(reserved);
file.annotateLast(reserved.length, "reserved");
this.versionValid4Number = file.readInt("versionValid4Number");
this.versionSqlite = file.readInt("versionSqlite");
// end of header
// compute the num of Pages
if (isValidNumPages(storedNumPages, this.fileChangeCounter, this.versionValid4Number)) {
this.numPages = storedNumPages;
} else {
this.numPages = file.size() / this.pageSize;
}
// read all pages
this.pages = readPages(file, this.numPages, this.pageSize);
} finally {
if (file != null)
file.close();
file = null;
}
// read root btree page
BTreePage<TableCell> sql_master_btree = BTreePages.readMaster(this);
assert (sql_master_btree.isTable()); // must be a table
sql_master_btree.resolve(this);
// read pointer map pages
this.pointerMapPages = PointerMapPage.readPointerMapPages(this);
// read free pages
this.freePages = FreeListPage.readPages(this, this.firstFreeListPage, this.numFreeListPages);
// read lock byte page
this.lockBytePage = LockBytePage.read(this);
this.sql_master_table = new SqliteMasterTable(sql_master_btree);
this.sql_master_table.resolve(this);
}
private static List<Page> readPages(FileDataInput file, int numPages, int pageSize) {
List<Page> pages = new ArrayList<Page>(numPages);
file.seek(0); // seek to start
for (int i = 0; i < numPages; ++i) {
byte[] data = new byte[pageSize];
file.readFully(data);
// file.annotateLast(data.length, "Page", i + 1);
ArrayDataInput din = new ArrayDataInput(data, file.createLastSubAnnotator(data.length));
pages.add(new RawDataPage(i, din));
}
return pages;
}
private static DataBaseEncoding readEncoding(FileDataInput file) {
int encoding = file.readInt();
switch (encoding) {
case 1:
return DataBaseEncoding.UTF8;
case 2:
return DataBaseEncoding.UTF16LE;
case 3:
return DataBaseEncoding.UTF16BE;
default:
throw new IllegalStateException();
}
}
private static boolean isValidNumPages(int storedNumPages, int fileChangeCounter, int versionValid4Number) {
return storedNumPages > 0 && (fileChangeCounter == versionValid4Number);
}
public ArrayDataInput getPageBlock(int pageNum) {
pageNum--; // starting with 1
assert (pageNum < this.numPages);
Page page = this.pages.get(pageNum);
assert (page instanceof RawDataPage); // not yet resolved
return ((RawDataPage) page).getContent();
}
public void resolvePage(int pageNum, Page specific) {
pageNum--; // starting with 1
assert (pageNum < this.numPages);
final Page ori = this.pages.get(pageNum);
assert (ori instanceof RawDataPage); // not yet resolved
((RawDataPage) ori).getContent().annotate(0, this.pageSize, "Page", specific.getClass().getSimpleName());
this.pages.set(pageNum, specific);
}
public int getUsableSize() {
return this.pageSize - this.reservedPageSpace;
}
private static void readMagicHeader(FileDataInput file) {
byte[] b = new byte[16];
file.readFully(b);
file.annotateLast(b.length, "Magic Header", "SQLite format 3");
byte[] expected = { 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x20, 0x33, 0x00 };
for (int i = 0; i < 16; ++i)
assert (b[i] == expected[i]);
// System.out.println(Arrays.toString(b));
}
private static int readPageSize(FileDataInput file) {
int s = file.readShort();
if (s == 1)
s = 65536;
file.annotateLastShort("PageSize", s);
return s;
}
public SqliteMasterTable getSql_master_table() {
return this.sql_master_table;
}
public int getLargestBTreePage() {
return this.largestBTreePage;
}
public Iterable<FreeListPage> getFreePages() {
return this.freePages;
}
public Iterable<PointerMapPage> getPointerMapPages() {
return this.pointerMapPages;
}
public LockBytePage getLockBytePage() {
return this.lockBytePage;
}
public byte getReservedPageSpace() {
return this.reservedPageSpace;
}
public String getFileName() {
return this.fileName;
}
public FileFormat getFileFormatWrite() {
return this.fileFormatWrite;
}
public int getPageSize() {
return this.pageSize;
}
public FileFormat getFileFormatRead() {
return this.fileFormatRead;
}
public int getFileChangeCounter() {
return this.fileChangeCounter;
}
public int getNumPages() {
return this.numPages;
}
public DataBaseEncoding getEncoding() {
return this.encoding;
}
public boolean isIncrementalVacuumMode() {
return this.incrementalVacuumMode;
}
public int getUserVersion() {
return this.userVersion;
}
public int getVersionValid4Number() {
return this.versionValid4Number;
}
public int getVersionSqlite() {
return this.versionSqlite;
}
public int getDefaultPageCacheSize() {
return this.defaultPageCacheSize;
}
public int getSchemaCookie() {
return this.schemaCookie;
}
public int getSchemaFormat() {
return this.schemaFormat;
}
public SimpleAnnotator getAnnotator() {
return this.annotator;
}
@Override
public void dump(StringBuilder builder) {
builder.append("<h1>SqlliteDataBase</h1>");
builder.append("\n<h2>General:</h2>\n");
{
builder.append("<pre>");
builder.append(" fileName\t\t= ").append(this.fileName).append("\n fileFormatWrite\t= ").append(this.fileFormatWrite)
.append("\n fileFormatRead\t\t= ")
.append(this.fileFormatRead);
builder.append("\n fileChangeCounter\t= ").append(this.fileChangeCounter).append("\n encoding\t\t= ").append(this.encoding)
.append("\n incrementalVacuumMode\t= ").append(this.incrementalVacuumMode).append("\n userVersion\t\t= ").append(this.userVersion)
.append("\n versionValid4Number\t= ").append(this.versionValid4Number).append("\n versionSqlite\t\t= ").append(this.versionSqlite);
builder.append("\n reservedPageSpace\t= ").append(this.reservedPageSpace).append("\n largestBTreePage\t= ").append(this.largestBTreePage);
builder.append("\n schemaCookie\t\t= ").append(this.schemaCookie).append("\n schemaFormat\t\t= ").append(this.schemaFormat);
builder.append("\n defaultPageCacheSize\t= ").append(this.defaultPageCacheSize);
builder.append("</pre>");
}
builder.append("\n\n<h2>Free List:</h2>\n");
{
builder.append("<pre>");
builder.append(" firstFreeListPage\t= ").append(this.firstFreeListPage).append("\n numFreeListPages\t= ").append(this.numFreeListPages)
.append("\n");
builder.append("</pre>");
DumpUtils.join(this.freePages, builder, "\n");
}
builder.append("\n\n<h2>Pointer Map Pages:</h2>\n");
{
DumpUtils.join(this.pointerMapPages, builder, "\n");
}
builder.append("\n\n<h2>Lock Byte Page:</h2>\n");
{
if (this.lockBytePage != null)
this.lockBytePage.dump(builder);
else
builder.append("none");
}
builder.append("\n\n<h2>Pages:</h2>\n");
{
builder.append("<pre>");
builder.append(" pageSize\t= ").append(this.pageSize).append("\n numPages\t= ").append(this.numPages).append('\n');
builder.append("</pre>");
DumpUtils.join(this.pages, builder, "\n\n");
}
builder.append("\n\n");
this.sql_master_table.dump(builder);
builder.append("\n\n<h2>Schema:</h2>\n");
for (SQLiteObject obj : this.sql_master_table.objects())
obj.dump(builder);
// builder.append("\n\n");
// this.annotator.dump(builder);
}
public static void main(String[] args) {
final File dbFile = new File(args[0]);
SqliteDataBase db = new SqliteDataBase(dbFile);
HTMLAnnotationRenderer.render(db, dbFile);
System.out.println(DumpUtils.dump(db));
}
}