TextCache.java

/* Copyright (c) 2001-2025, 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.persist;

import org.hsqldb.Database;
import org.hsqldb.DatabaseType;
import org.hsqldb.Row;
import org.hsqldb.Table;
import org.hsqldb.error.Error;
import org.hsqldb.error.ErrorCode;
import org.hsqldb.error.HsqlException;
import org.hsqldb.lib.FileUtil;
import org.hsqldb.lib.HsqlByteArrayOutputStream;
import org.hsqldb.lib.LongKeyHashMap;
import org.hsqldb.rowio.RowInputText;
import org.hsqldb.rowio.RowInputTextQuoted;
import org.hsqldb.rowio.RowOutputText;
import org.hsqldb.rowio.RowOutputTextQuoted;

// Ito Kazumitsu 20030328 - patch 1.7.2 - character encoding support
// Dimitri Maziuk - patch for NL in string support
// sqlbob@users - updated for 1.8.0 to allow new-lines in fields
// fredt@users - updated for 1.8.0 to allow correct behaviour with transactions

/**
 * Acts as a buffer manager for a single TEXT table and its Row data.<p>
 *
 * Handles read/write operations on the table's text format data file using a
 * compatible pair of org.hsqldb.rowio input/output class instances.
 *
 *
 * fredt - This used to write rows as soon as they are inserted
 * but now this is subject to transaction management.
 * A memory buffer contains the rows not yet committed.
 * Refactored for version 2.2.6.
 *
 * @author Bob Preston (sqlbob@users dot sourceforge.net)
 * @author Fred Toussi (fredt@users dot sourceforge.net)
 * @version 2.7.3
 * @since 1.7.0
 */
public class TextCache extends DataFileCache {

    //
    TextFileSettings textFileSettings;

    //state of Cache
    protected String                     header;
    protected Table                      table;
    private LongKeyHashMap<CachedObject> uncommittedCache;
    HsqlByteArrayOutputStream buffer = new HsqlByteArrayOutputStream(128);

    //

    /**
     *  The source string for a cached table is evaluated and the parameters
     *  are used to open the source file.<p>
     *
     *  Settings are used in this order: (1) settings specified in the
     *  source string for the table (2) global database settings in
     *  *.properties file (3) program defaults
     */
    TextCache(Table table, String name) {

        super(table.database, name);

        this.table       = table;
        uncommittedCache = new LongKeyHashMap<>();
    }

    protected void initParams(
            Database database,
            String settingsString,
            boolean defrag) {

        this.database = database;
        fa            = FileUtil.getFileUtil();
        textFileSettings = new TextFileSettings(
            database.getProperties(),
            settingsString);
        dataFileName  = textFileSettings.getFileName();

        if (dataFileName == null) {
            throw Error.error(ErrorCode.X_S0501);
        }

        dataFileName  = ((FileUtil) fa).canonicalOrAbsolutePath(dataFileName);
        maxCacheRows  = textFileSettings.getMaxCacheRows();
        maxCacheBytes = textFileSettings.getMaxCacheBytes();

        // max size is 256G
        maxDataFileSize  = (long) Integer.MAX_VALUE * Logger.largeDataFactor;
        cachedRowPadding = 1;
        dataFileScale    = 1;
    }

    protected void initBuffers() {

        if (textFileSettings.isQuoted || textFileSettings.isAllQuoted) {
            rowIn  = new RowInputTextQuoted(textFileSettings);
            rowOut = new RowOutputTextQuoted(textFileSettings);
        } else {
            rowIn  = new RowInputText(textFileSettings);
            rowOut = new RowOutputText(textFileSettings);
        }
    }

    /**
     *  Opens a data source file.
     */
    public void open(boolean readonly) {

        fileFreePosition = 0;

        try {
            int type = database.getType() == DatabaseType.DB_RES
                       ? RAFile.DATA_FILE_JAR
                       : RAFile.DATA_FILE_TEXT;

            dataFile = RAFile.newScaledRAFile(
                database,
                dataFileName,
                readonly,
                type);
            fileFreePosition = dataFile.length();

            if (fileFreePosition > maxDataFileSize) {
                throw Error.error(ErrorCode.DATA_FILE_IS_FULL);
            }

            initBuffers();

            spaceManager = new DataSpaceManagerSimple(this, readonly);
        } catch (Throwable t) {
            throw Error.error(
                t,
                ErrorCode.FILE_IO_ERROR,
                ErrorCode.M_TextCache_opening_file_error,
                new String[]{ dataFileName, t.toString() });
        }

        cacheReadonly = readonly;
    }

    public void release() {
        close();
    }

    /**
     *  Writes newly created rows to disk. In the current implementation,
     *  such rows have already been saved, so this method just removes a
     *  source file that has no rows.
     */
    public void close() {

        if (dataFile == null) {
            return;
        }

        writeLock.lock();

        try {
            cache.saveAll();

            boolean empty = (dataFile.length()
                             <= textFileSettings.bytesForLineEnd.length);

            dataFile.synch();
            dataFile.close();

            dataFile = null;

            if (empty && !cacheReadonly) {
                FileUtil.getFileUtil().delete(dataFileName);
            }

            uncommittedCache.clear();
        } catch (Throwable t) {
            throw Error.error(
                t,
                ErrorCode.FILE_IO_ERROR,
                ErrorCode.M_TextCache_closing_file_error,
                new String[]{ dataFileName, t.toString() });
        } finally {
            writeLock.unlock();
        }
    }

    /**
     * Closes the source file and deletes it if it is not read-only.
     */
    void purge() {

        writeLock.lock();

        try {
            uncommittedCache.clear();

            if (cacheReadonly) {
                release();
            } else {
                if (dataFile != null) {
                    dataFile.close();

                    dataFile = null;
                }

                FileUtil.getFileUtil().delete(dataFileName);
            }
        } catch (Throwable t) {
            throw Error.error(
                t,
                ErrorCode.FILE_IO_ERROR,
                ErrorCode.M_TextCache_purging_file_error,
                new String[]{ dataFileName, t.toString() });
        } finally {
            writeLock.unlock();
        }
    }

    /**
     *
     */
    public void remove(CachedObject object) {

        writeLock.lock();

        try {
            long         pos = object.getPos();
            CachedObject row = uncommittedCache.remove(pos);

            if (row != null) {
                return;
            }

            cache.release(pos);
        } finally {
            writeLock.unlock();
        }
    }

    public void removePersistence(CachedObject row) {

        writeLock.lock();

        try {
            clearRowImage(row);
        } finally {
            writeLock.unlock();
        }
    }

    private void clearRowImage(CachedObject row) {

        try {
            int length = row.getStorageSize();
            int count  = length - textFileSettings.bytesForLineEnd.length;

            rowOut.reset();

            HsqlByteArrayOutputStream out = rowOut.getOutputStream();

            for (; count > 0; count -= textFileSettings.bytesForSpace.length) {
                out.write(textFileSettings.bytesForSpace);
            }

            out.write(textFileSettings.bytesForLineEnd);
            dataFile.seek(row.getPos());
            dataFile.write(out.getBuffer(), 0, out.size());
        } catch (Throwable t) {
            throw Error.runtimeError(ErrorCode.U_S0500, t.getMessage());
        }
    }

    public void add(CachedObject object, boolean keep) {

        writeLock.lock();

        try {
            uncommittedCache.put(object.getPos(), object);
        } finally {
            writeLock.unlock();
        }
    }

    /** cannot use isInMemory() for text cached object */
    public CachedObject get(
            CachedObject object,
            PersistentStore store,
            boolean keep) {

        if (object == null) {
            return null;
        }

        writeLock.lock();

        try {
            CachedObject existing = cache.get(object.getPos());

            if (existing != null) {
                return object;
            }

            try {
                buffer.reset(object.getStorageSize());
                dataFile.seek(object.getPos());
                dataFile.read(buffer.getBuffer(), 0, object.getStorageSize());
                buffer.setSize(object.getStorageSize());

                String rowString = buffer.toString(
                    textFileSettings.charEncoding);

                ((RowInputText) rowIn).setSource(
                    rowString,
                    object.getPos(),
                    buffer.size());
                store.get(object, rowIn);
                cache.put(object);

                Row row = (Row) object;

                if (textFileSettings.isNullDef) {
                    table.generateDefaultForNull(row.getData());
                }

                return object;
            } catch (Throwable t) {
                database.logger.logSevereEvent(
                    dataFileName + " getFromFile problem " + object.getPos(),
                    t);
                cache.clearUnchanged();

                return object;
            }
        } finally {
            writeLock.unlock();
        }
    }

    public CachedObject get(long i, PersistentStore store, boolean keep) {
        throw Error.runtimeError(ErrorCode.U_S0500, "TextCache");
    }

    protected void saveRows(CachedObject[] rows, int offset, int count) {

        // no-op
    }

    /**
     * The row is always in uncommittedCache.
     * Saves the row as normal and removes it
     */
    public void saveRow(CachedObject row) {

        writeLock.lock();

        try {
            setFileModified();
            saveRowNoLock(row);
            uncommittedCache.remove(row.getPos());
            cache.put(row);
        } catch (Throwable e) {
            database.logger.logSevereEvent("saveRow failed", e);

            throw Error.error(ErrorCode.DATA_FILE_ERROR, e);
        } finally {
            writeLock.unlock();
        }
    }

    public String getHeader() {
        return header;
    }

    public void setHeaderInitialise(String header) {
        this.header = header;
    }

    public void setHeader(String header) {

        if (textFileSettings.isIgnoreFirst && fileFreePosition == 0) {
            try {
                writeHeader(header);

                this.header = header;
            } catch (HsqlException e) {
                throw new HsqlException(
                    e,
                    Error.getMessage(ErrorCode.GENERAL_IO_ERROR),
                    ErrorCode.GENERAL_IO_ERROR);
            }

            return;
        }

        throw Error.error(ErrorCode.TEXT_TABLE_HEADER);
    }

    private void writeHeader(String header) {

        try {
            byte[] buf       = null;
            String firstLine = header + TextFileSettings.NL;

            buf = firstLine.getBytes(textFileSettings.charEncoding);

            dataFile.seek(0);
            dataFile.write(buf, 0, buf.length);

            fileFreePosition = buf.length;
        } catch (Throwable t) {
            throw Error.error(ErrorCode.TEXT_FILE_IO, t);
        }
    }

    public long getLineNumber() {
        return ((RowInputText) rowIn).getLineNumber();
    }

    public TextFileSettings getTextFileSettings() {
        return textFileSettings;
    }

    public boolean isIgnoreFirstLine() {
        return textFileSettings.isIgnoreFirst;
    }

    protected void setFileModified() {
        fileModified = true;
    }

    public TextFileReader getTextFileReader() {

        return TextFileReader8.newTextFileReader(
            dataFile,
            textFileSettings,
            rowIn,
            cacheReadonly);
    }
}