/* Copyright (c) 2001-2009, The HSQL Development Group * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of the HSQL Development Group nor the names of its * contributors may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG, * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.hsqldb.scriptio; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.OutputStream; import org.hsqldb.Database; import org.hsqldb.DatabaseManager; import org.hsqldb.Error; import org.hsqldb.ErrorCode; import org.hsqldb.HsqlNameManager.HsqlName; import org.hsqldb.NumberSequence; import org.hsqldb.SchemaObject; import org.hsqldb.Session; import org.hsqldb.Table; import org.hsqldb.TableBase; import org.hsqldb.Tokens; import org.hsqldb.lib.FileAccess; import org.hsqldb.lib.FileUtil; import org.hsqldb.lib.HsqlTimer; import org.hsqldb.lib.Iterator; import org.hsqldb.navigator.RowIterator; import org.hsqldb.navigator.RowSetNavigator; import org.hsqldb.result.Result; //import org.hsqldb.lib.StopWatch; /** * @todo - can lock the database engine as readonly in a wrapper for this when * used at checkpoint */ /** * Handles all logging to file operations. A log consists of three kinds of * blocks:<p> * * DDL BLOCK: definition of DB objects, users and rights at startup time<br> * DATA BLOCK: all data for MEMORY tables at startup time<br> * LOG BLOCK: SQL statements logged since startup or the last CHECKPOINT<br> * * The implementation of this class and its subclasses support the formats * used for writing the data. Since 1.7.2 the data can also be * written as binray in order to speed up shutdown and startup.<p> * * From 1.7.2, two separate files are used, one for the DDL + DATA BLOCK and * the other for the LOG BLOCK.<p> * * A related use for this class is for saving a current snapshot of the * database data to a user-defined file. This happens in the SHUTDOWN COMPACT * process or done as a result of the SCRIPT command. In this case, the * DATA block contains the CACHED table data as well.<p> * * DatabaseScriptReader and its subclasses read back the data at startup time. * * @author Fred Toussi (fredt@users dot sourceforge.net) * @version 1.8.0 * @since 1.7.2 */ public abstract class ScriptWriterBase implements Runnable { Database database; String outFile; OutputStream fileStreamOut; FileAccess.FileSync outDescriptor; int tableRowCount; HsqlName schemaToLog; boolean isClosed; /** * this determines if the script is the normal script (false) used * internally by the engine or a user-initiated snapshot of the DB (true) */ boolean isDump; boolean includeCachedData; long byteCount; volatile boolean needsSync; volatile boolean forceSync; volatile boolean busyWriting; private int syncCount; static final int INSERT = 0; static final int INSERT_WITH_SCHEMA = 1; /** the last schema for last sessionId */ Session currentSession; public static final String[] LIST_SCRIPT_FORMATS = new String[] { Tokens.T_TEXT, Tokens.T_BINARY, null, Tokens.T_COMPRESSED }; public static final int SCRIPT_TEXT_170 = 0; public static final int SCRIPT_BINARY_172 = 1; public static final int SCRIPT_ZIPPED_BINARY_172 = 3; public static ScriptWriterBase newScriptWriter(Database db, String file, boolean includeCachedData, boolean newFile, int scriptType) { if (scriptType == SCRIPT_TEXT_170) { return new ScriptWriterText(db, file, includeCachedData, newFile, false); } else if (scriptType == SCRIPT_BINARY_172) { return new ScriptWriterBinary(db, file, includeCachedData, newFile); } else { return new ScriptWriterZipped(db, file, includeCachedData, newFile); } } ScriptWriterBase() {} ScriptWriterBase(Database db, String file, boolean includeCachedData, boolean isNewFile, boolean isDump) { this.isDump = isDump; initBuffers(); boolean exists = false; if (isDump) { exists = FileUtil.getDefaultInstance().exists(file); } else { exists = db.getFileAccess().isStreamElement(file); } if (exists && isNewFile) { throw Error.error(ErrorCode.FILE_IO_ERROR, file); } this.database = db; this.includeCachedData = includeCachedData; outFile = file; currentSession = database.sessionManager.getSysSession(); // start with neutral schema - no SET SCHEMA to log schemaToLog = currentSession.loggedSchema = currentSession.currentSchema; openFile(); } public void reopen() { openFile(); } protected abstract void initBuffers(); /** * Called internally or externally in write delay intervals. */ public void sync() { if (isClosed) { return; } synchronized (fileStreamOut) { if (needsSync) { if (busyWriting) { forceSync = true; return; } try { fileStreamOut.flush(); outDescriptor.sync(); syncCount++; } catch (IOException e) { Error.printSystemOut("flush() or sync() error: " + e.toString()); } needsSync = false; forceSync = false; } } } public void close() { stop(); if (isClosed) { return; } try { synchronized (fileStreamOut) { needsSync = false; isClosed = true; fileStreamOut.flush(); outDescriptor.sync(); fileStreamOut.close(); } } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR); } byteCount = 0; } public long size() { return byteCount; } public void writeAll() { try { writeDDL(); writeExistingData(); finishStream(); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR); } } /** * File is opened in append mode although in current usage the file * never pre-exists */ protected void openFile() { try { FileAccess fa = isDump ? FileUtil.getDefaultInstance() : database.getFileAccess(); OutputStream fos = fa.openOutputStreamElement(outFile); outDescriptor = fa.getFileSync(fos); fileStreamOut = new BufferedOutputStream(fos, 2 << 12); } catch (IOException e) { throw Error.error(ErrorCode.FILE_IO_ERROR, ErrorCode.M_Message_Pair, new Object[] { e.toString(), outFile }); } } /** * This is not really useful in the current usage but may be if this * class is used in a different way. */ protected void finishStream() throws IOException {} protected void writeDDL() throws IOException { Result ddlPart = database.getScript(!includeCachedData); writeSingleColumnResult(ddlPart); } protected void writeExistingData() throws IOException { // start with blank schema - SET SCHEMA to log currentSession.loggedSchema = null; Iterator schemas = database.schemaManager.allSchemaNameIterator(); while (schemas.hasNext()) { String schema = (String) schemas.next(); Iterator tables = database.schemaManager.databaseObjectIterator(schema, SchemaObject.TABLE); while (tables.hasNext()) { Table t = (Table) tables.next(); // write all memory table data // write cached table data unless index roots have been written // write all text table data apart from readonly text tables // unless index roots have been written boolean script = false; switch (t.getTableType()) { case TableBase.MEMORY_TABLE : script = true; break; case TableBase.CACHED_TABLE : script = includeCachedData; break; case TableBase.TEXT_TABLE : script = includeCachedData && !t.isReadOnly(); break; } try { if (script) { schemaToLog = t.getName().schema; writeTableInit(t); RowIterator it = t.rowIterator(currentSession); while (it.hasNext()) { writeRow(currentSession, t, it.getNextRow().getData()); } writeTableTerm(t); } } catch (Exception e) { throw Error.error(ErrorCode.FILE_IO_ERROR, e.toString()); } } } writeDataTerm(); } protected void writeTableInit(Table t) throws IOException {} protected void writeTableTerm(Table t) throws IOException { if (t.isDataReadOnly() && !t.isTemp() && !t.isText()) { StringBuffer a = new StringBuffer("SET TABLE "); a.append(t.getName().statementName); a.append(" READONLY TRUE"); writeLogStatement(currentSession, a.toString()); } } protected void writeSingleColumnResult(Result r) throws IOException { RowSetNavigator nav = r.initialiseNavigator(); while (nav.hasNext()) { Object[] data = (Object[]) nav.getNext(); writeLogStatement(currentSession, (String) data[0]); } } abstract void writeRow(Session session, Table table, Object[] data) throws IOException; protected abstract void writeDataTerm() throws IOException; protected abstract void addSessionId(Session session) throws IOException; public abstract void writeLogStatement(Session session, String s) throws IOException; public abstract void writeInsertStatement(Session session, Table table, Object[] data) throws IOException; public abstract void writeDeleteStatement(Session session, Table table, Object[] data) throws IOException; public abstract void writeSequenceStatement(Session session, NumberSequence seq) throws IOException; public abstract void writeCommitStatement(Session session) throws IOException; // private Object timerTask; // long write delay for scripts : 60s protected volatile int writeDelay = 60000; public void run() { try { if (writeDelay != 0) { sync(); } /** @todo: try to do Cache.cleanUp() here, too */ } catch (Exception e) { // ignore exceptions // may be InterruptedException or IOException } } public void setWriteDelay(int delay) { writeDelay = delay; int period = writeDelay == 0 ? 1000 : writeDelay; HsqlTimer.setPeriod(timerTask, period); } public void start() { int period = writeDelay == 0 ? 1000 : writeDelay; timerTask = DatabaseManager.getTimer().schedulePeriodicallyAfter(0, period, this, false); } public void stop() { if (timerTask != null) { HsqlTimer.cancel(timerTask); timerTask = null; } } public int getWriteDelay() { return writeDelay; } }