org.apache.jackrabbit.core.fs.db.DatabaseFileSystem.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jackrabbit.core.fs.db.DatabaseFileSystem.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.jackrabbit.core.fs.db;

import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.fs.FileSystemException;
import org.apache.jackrabbit.core.fs.FileSystemPathUtil;
import org.apache.jackrabbit.core.fs.RandomAccessOutputStream;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.util.TransientFileFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.RepositoryException;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.RandomAccessFile;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.HashMap;

/**
 * Base class for database file systems. This class contains common
 * functionality for database file system subclasses that normally differ only
 * in the way the database connection is acquired. Subclasses should override
 * the {@link #getConnection()} method to return the configured database
 * connection.
 * <p>
 * See the {@link DbFileSystem} for a detailed description of the available
 * configuration options and database behaviour.
 */
public class DatabaseFileSystem implements FileSystem {

    /**
     * Logger instance
     */
    private static Logger log = LoggerFactory.getLogger(DatabaseFileSystem.class);

    protected static final String SCHEMA_OBJECT_PREFIX_VARIABLE = "${schemaObjectPrefix}";

    protected boolean initialized;

    protected String schema;
    protected String schemaObjectPrefix;

    /**
     * Whether the schema check must be done during initialization.
     */
    private boolean schemaCheckEnabled = true;

    // initial size of buffer used to serialize objects
    protected static final int INITIAL_BUFFER_SIZE = 8192;

    // jdbc connection
    protected Connection con;

    // time to sleep in ms before a reconnect is attempted
    protected static final int SLEEP_BEFORE_RECONNECT = 10000;

    // the map of prepared statements (key: sql stmt, value: prepared stmt)
    private HashMap preparedStatements = new HashMap();

    // SQL statements
    protected String selectExistSQL;
    protected String selectFileExistSQL;
    protected String selectFolderExistSQL;
    protected String selectChildCountSQL;
    protected String selectDataSQL;
    protected String selectLastModifiedSQL;
    protected String selectLengthSQL;
    protected String selectFileNamesSQL;
    protected String selectFolderNamesSQL;
    protected String selectFileAndFolderNamesSQL;
    protected String deleteFileSQL;
    protected String deleteFolderSQL;
    protected String insertFileSQL;
    protected String insertFolderSQL;
    protected String updateDataSQL;
    protected String updateLastModifiedSQL;
    protected String copyFileSQL;
    protected String copyFilesSQL;

    /**
     * Default constructor
     */
    public DatabaseFileSystem() {
        schema = "default";
        schemaObjectPrefix = "";
        initialized = false;
    }

    //----------------------------------------------------< setters & getters >
    public String getSchemaObjectPrefix() {
        return schemaObjectPrefix;
    }

    public void setSchemaObjectPrefix(String schemaObjectPrefix) {
        // make sure prefix is all uppercase
        this.schemaObjectPrefix = schemaObjectPrefix.toUpperCase();
    }

    public String getSchema() {
        return schema;
    }

    public void setSchema(String schema) {
        this.schema = schema;
    }

    /**
     * @return whether the schema check is enabled
     */
    public final boolean isSchemaCheckEnabled() {
        return schemaCheckEnabled;
    }

    /**
     * @param enabled set whether the schema check is enabled
     */
    public final void setSchemaCheckEnabled(boolean enabled) {
        schemaCheckEnabled = enabled;
    }

    //-------------------------------------------< java.lang.Object overrides >
    /**
     * {@inheritDoc}
     */
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        } else if (obj instanceof DatabaseFileSystem) {
            DatabaseFileSystem other = (DatabaseFileSystem) obj;
            return equals(schema, other.schema) && equals(schemaObjectPrefix, other.schemaObjectPrefix);
        } else {
            return false;
        }
    }

    private static boolean equals(Object a, Object b) {
        if (a == null && b == null) {
            return true;
        } else if (a == null || b == null) {
            return false;
        } else {
            return a.equals(b);
        }
    }

    /**
     * Returns zero to satisfy the Object equals/hashCode contract.
     * This class is mutable and not meant to be used as a hash key.
     *
     * @return always zero
     * @see Object#hashCode()
     */
    public int hashCode() {
        return 0;
    }

    //-----------------------------------------------------------< FileSystem >

    /**
     * {@inheritDoc}
     */
    public void init() throws FileSystemException {
        if (initialized) {
            throw new IllegalStateException("already initialized");
        }

        try {
            // setup jdbc connection
            initConnection();

            // make sure schemaObjectPrefix consists of legal name characters only
            prepareSchemaObjectPrefix();

            // check if schema objects exist and create them if necessary
            if (isSchemaCheckEnabled()) {
                checkSchema();
            }

            // build sql statements
            buildSQLStatements();

            // prepare statements
            initPreparedStatements();

            // finally verify that there's a file system root entry
            verifyRootExists();

            initialized = true;
        } catch (Exception e) {
            String msg = "failed to initialize file system";
            log.error(msg, e);
            throw new FileSystemException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void close() throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        try {
            // close shared prepared statements
            Iterator it = preparedStatements.values().iterator();
            while (it.hasNext()) {
                closeStatement((PreparedStatement) it.next());
            }
            preparedStatements.clear();

            // close jdbc connection
            closeConnection(con);
        } catch (SQLException e) {
            String msg = "error closing file system";
            log.error(msg, e);
            throw new FileSystemException(msg, e);
        } finally {
            initialized = false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void createFolder(String folderPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(folderPath);

        if (!exists(folderPath)) {
            createDeepFolder(folderPath);
        } else {
            throw new FileSystemException("file system entry already exists: " + folderPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteFile(String filePath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        String parentDir = FileSystemPathUtil.getParentDir(filePath);
        String name = FileSystemPathUtil.getName(filePath);

        int count = 0;
        synchronized (deleteFileSQL) {
            try {
                Statement stmt = executeStmt(deleteFileSQL, new Object[] { parentDir, name });
                count = stmt.getUpdateCount();
            } catch (SQLException e) {
                String msg = "failed to delete file: " + filePath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }

        if (count == 0) {
            throw new FileSystemException("no such file: " + filePath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteFolder(String folderPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(folderPath);

        if (folderPath.equals(FileSystem.SEPARATOR)) {
            throw new FileSystemException("cannot delete root");
        }

        String parentDir = FileSystemPathUtil.getParentDir(folderPath);
        String name = FileSystemPathUtil.getName(folderPath);

        int count = 0;
        synchronized (deleteFolderSQL) {
            try {
                Statement stmt = executeStmt(deleteFolderSQL,
                        new Object[] { parentDir, name, folderPath, folderPath + FileSystem.SEPARATOR + "%" });
                count = stmt.getUpdateCount();
            } catch (SQLException e) {
                String msg = "failed to delete folder: " + folderPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }

        if (count == 0) {
            throw new FileSystemException("no such folder: " + folderPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean exists(String path) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(path);

        String parentDir = FileSystemPathUtil.getParentDir(path);
        String name = FileSystemPathUtil.getName(path);

        synchronized (selectExistSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectExistSQL, new Object[] { parentDir, name });
                rs = stmt.getResultSet();

                // a file system entry exists if the result set
                // has at least one entry
                return rs.next();
            } catch (SQLException e) {
                String msg = "failed to check existence of file system entry: " + path;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isFile(String path) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(path);

        String parentDir = FileSystemPathUtil.getParentDir(path);
        String name = FileSystemPathUtil.getName(path);

        synchronized (selectFileExistSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFileExistSQL, new Object[] { parentDir, name });
                rs = stmt.getResultSet();

                // a file exists if the result set has at least one entry
                return rs.next();
            } catch (SQLException e) {
                String msg = "failed to check existence of file: " + path;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isFolder(String path) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(path);

        String parentDir = FileSystemPathUtil.getParentDir(path);
        String name = FileSystemPathUtil.getName(path);

        synchronized (selectFolderExistSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFolderExistSQL, new Object[] { parentDir, name });
                rs = stmt.getResultSet();

                // a folder exists if the result set has at least one entry
                return rs.next();
            } catch (SQLException e) {
                String msg = "failed to check existence of folder: " + path;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public long lastModified(String path) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(path);

        String parentDir = FileSystemPathUtil.getParentDir(path);
        String name = FileSystemPathUtil.getName(path);

        synchronized (selectLastModifiedSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectLastModifiedSQL, new Object[] { parentDir, name });
                rs = stmt.getResultSet();
                if (!rs.next()) {
                    throw new FileSystemException("no such file system entry: " + path);
                }
                return rs.getLong(1);
            } catch (SQLException e) {
                String msg = "failed to determine lastModified of file system entry: " + path;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public long length(String filePath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        String parentDir = FileSystemPathUtil.getParentDir(filePath);
        String name = FileSystemPathUtil.getName(filePath);

        synchronized (selectLengthSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectLengthSQL, new Object[] { parentDir, name });
                rs = stmt.getResultSet();
                if (!rs.next()) {
                    throw new FileSystemException("no such file: " + filePath);
                }
                return rs.getLong(1);
            } catch (SQLException e) {
                String msg = "failed to determine length of file: " + filePath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasChildren(String path) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(path);

        if (!exists(path)) {
            throw new FileSystemException("no such file system entry: " + path);
        }

        synchronized (selectChildCountSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectChildCountSQL, new Object[] { path });
                rs = stmt.getResultSet();
                if (!rs.next()) {
                    return false;
                }
                int count = rs.getInt(1);
                if (FileSystemPathUtil.denotesRoot(path)) {
                    // ingore file system root entry
                    count--;
                }
                return (count > 0);
            } catch (SQLException e) {
                String msg = "failed to determine child count of file system entry: " + path;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public String[] list(String folderPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(folderPath);

        if (!isFolder(folderPath)) {
            throw new FileSystemException("no such folder: " + folderPath);
        }

        synchronized (selectFileAndFolderNamesSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFileAndFolderNamesSQL, new Object[] { folderPath });
                rs = stmt.getResultSet();
                ArrayList names = new ArrayList();
                while (rs.next()) {
                    String name = rs.getString(1);
                    if (name.length() == 0 && FileSystemPathUtil.denotesRoot(folderPath)) {
                        // this is the file system root entry, skip...
                        continue;
                    }
                    names.add(name);
                }
                return (String[]) names.toArray(new String[names.size()]);
            } catch (SQLException e) {
                String msg = "failed to list child entries of folder: " + folderPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public String[] listFiles(String folderPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(folderPath);

        if (!isFolder(folderPath)) {
            throw new FileSystemException("no such folder: " + folderPath);
        }

        synchronized (selectFileNamesSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFileNamesSQL, new Object[] { folderPath });
                rs = stmt.getResultSet();
                ArrayList names = new ArrayList();
                while (rs.next()) {
                    names.add(rs.getString(1));
                }
                return (String[]) names.toArray(new String[names.size()]);
            } catch (SQLException e) {
                String msg = "failed to list file entries of folder: " + folderPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public String[] listFolders(String folderPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(folderPath);

        if (!isFolder(folderPath)) {
            throw new FileSystemException("no such folder: " + folderPath);
        }

        synchronized (selectFolderNamesSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFolderNamesSQL, new Object[] { folderPath });
                rs = stmt.getResultSet();
                ArrayList names = new ArrayList();
                while (rs.next()) {
                    String name = rs.getString(1);
                    if (name.length() == 0 && FileSystemPathUtil.denotesRoot(folderPath)) {
                        // this is the file system root entry, skip...
                        continue;
                    }
                    names.add(name);
                }
                return (String[]) names.toArray(new String[names.size()]);
            } catch (SQLException e) {
                String msg = "failed to list folder entries of folder: " + folderPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void touch(String filePath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        String parentDir = FileSystemPathUtil.getParentDir(filePath);
        String name = FileSystemPathUtil.getName(filePath);

        int count = 0;
        synchronized (updateLastModifiedSQL) {
            try {
                Statement stmt = executeStmt(updateLastModifiedSQL,
                        new Object[] { new Long(System.currentTimeMillis()), parentDir, name });
                count = stmt.getUpdateCount();
            } catch (SQLException e) {
                String msg = "failed to touch file: " + filePath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }

        if (count == 0) {
            throw new FileSystemException("no such file: " + filePath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public InputStream getInputStream(String filePath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        String parentDir = FileSystemPathUtil.getParentDir(filePath);
        String name = FileSystemPathUtil.getName(filePath);

        synchronized (selectDataSQL) {
            try {
                Statement stmt = executeStmt(selectDataSQL, new Object[] { parentDir, name });

                final ResultSet rs = stmt.getResultSet();
                if (!rs.next()) {
                    throw new FileSystemException("no such file: " + filePath);
                }
                InputStream in = rs.getBinaryStream(1);
                /**
                 * return an InputStream wrapper in order to
                 * close the ResultSet when the stream is closed
                 */
                return new FilterInputStream(in) {
                    public void close() throws IOException {
                        super.close();
                        // close ResultSet
                        closeResultSet(rs);
                    }
                };
            } catch (SQLException e) {
                String msg = "failed to retrieve data of file: " + filePath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public OutputStream getOutputStream(final String filePath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        final String parentDir = FileSystemPathUtil.getParentDir(filePath);
        final String name = FileSystemPathUtil.getName(filePath);

        if (!isFolder(parentDir)) {
            throw new FileSystemException("path not found: " + parentDir);
        }

        if (isFolder(filePath)) {
            throw new FileSystemException("path denotes folder: " + filePath);
        }

        try {
            TransientFileFactory fileFactory = TransientFileFactory.getInstance();
            final File tmpFile = fileFactory.createTransientFile("bin", null, null);

            return new FilterOutputStream(new FileOutputStream(tmpFile)) {

                public void write(byte[] bytes, int off, int len) throws IOException {
                    out.write(bytes, off, len);
                }

                public void close() throws IOException {
                    out.flush();
                    ((FileOutputStream) out).getFD().sync();
                    out.close();

                    InputStream in = null;
                    try {
                        if (isFile(filePath)) {
                            synchronized (updateDataSQL) {
                                long length = tmpFile.length();
                                in = new FileInputStream(tmpFile);
                                executeStmt(updateDataSQL, new Object[] { new SizedInputStream(in, length),
                                        new Long(System.currentTimeMillis()), new Long(length), parentDir, name });
                            }
                        } else {
                            synchronized (insertFileSQL) {
                                long length = tmpFile.length();
                                in = new FileInputStream(tmpFile);
                                executeStmt(insertFileSQL,
                                        new Object[] { parentDir, name, new SizedInputStream(in, length),
                                                new Long(System.currentTimeMillis()), new Long(length) });
                            }
                        }

                    } catch (Exception e) {
                        IOException ioe = new IOException(e.getMessage());
                        ioe.initCause(e);
                        throw ioe;
                    } finally {
                        if (in != null) {
                            in.close();
                        }
                        // temp file can now safely be removed
                        tmpFile.delete();
                    }
                }
            };
        } catch (Exception e) {
            String msg = "failed to open output stream to file: " + filePath;
            log.error(msg, e);
            throw new FileSystemException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public RandomAccessOutputStream getRandomAccessOutputStream(final String filePath)
            throws FileSystemException, UnsupportedOperationException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(filePath);

        final String parentDir = FileSystemPathUtil.getParentDir(filePath);
        final String name = FileSystemPathUtil.getName(filePath);

        if (!isFolder(parentDir)) {
            throw new FileSystemException("path not found: " + parentDir);
        }

        if (isFolder(filePath)) {
            throw new FileSystemException("path denotes folder: " + filePath);
        }

        try {
            TransientFileFactory fileFactory = TransientFileFactory.getInstance();
            final File tmpFile = fileFactory.createTransientFile("bin", null, null);

            // @todo FIXME use java.sql.Blob

            if (isFile(filePath)) {
                // file entry exists, spool contents to temp file first
                InputStream in = getInputStream(filePath);
                OutputStream out = new FileOutputStream(tmpFile);
                try {
                    IOUtils.copy(in, out);
                } finally {
                    out.close();
                    in.close();
                }
            }

            return new RandomAccessOutputStream() {
                private final RandomAccessFile raf = new RandomAccessFile(tmpFile, "rw");

                public void close() throws IOException {
                    raf.close();

                    InputStream in = null;
                    try {
                        if (isFile(filePath)) {
                            synchronized (updateDataSQL) {
                                long length = tmpFile.length();
                                in = new FileInputStream(tmpFile);
                                executeStmt(updateDataSQL, new Object[] { new SizedInputStream(in, length),
                                        new Long(System.currentTimeMillis()), new Long(length), parentDir, name });
                            }
                        } else {
                            synchronized (insertFileSQL) {
                                long length = tmpFile.length();
                                in = new FileInputStream(tmpFile);
                                executeStmt(insertFileSQL,
                                        new Object[] { parentDir, name, new SizedInputStream(in, length),
                                                new Long(System.currentTimeMillis()), new Long(length) });
                            }
                        }

                    } catch (Exception e) {
                        IOException ioe = new IOException(e.getMessage());
                        ioe.initCause(e);
                        throw ioe;
                    } finally {
                        if (in != null) {
                            in.close();
                        }
                        // temp file can now safely be removed
                        tmpFile.delete();
                    }
                }

                public void seek(long position) throws IOException {
                    raf.seek(position);
                }

                public void write(int b) throws IOException {
                    raf.write(b);
                }

                public void flush() /*throws IOException*/ {
                    // nop
                }

                public void write(byte[] b) throws IOException {
                    raf.write(b);
                }

                public void write(byte[] b, int off, int len) throws IOException {
                    raf.write(b, off, len);
                }
            };
        } catch (Exception e) {
            String msg = "failed to open output stream to file: " + filePath;
            log.error(msg, e);
            throw new FileSystemException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void copy(String srcPath, String destPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(srcPath);
        FileSystemPathUtil.checkFormat(destPath);

        if (isFolder(srcPath)) {
            // src is a folder
            copyDeepFolder(srcPath, destPath);
        } else {
            // src is a file
            copyFile(srcPath, destPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void move(String srcPath, String destPath) throws FileSystemException {
        if (!initialized) {
            throw new IllegalStateException("not initialized");
        }

        FileSystemPathUtil.checkFormat(srcPath);
        FileSystemPathUtil.checkFormat(destPath);

        // @todo optimize move (use sql update stmts)
        copy(srcPath, destPath);
        if (isFile(srcPath)) {
            deleteFile(srcPath);
        } else {
            deleteFolder(srcPath);
        }
    }

    //----------------------------------< misc. helper methods & overridables >

    /**
     * Initializes the database connection used by this file system.
     * <p>
     * Subclasses should normally override the {@link #getConnection()}
     * method instead of this one. The default implementation calls
     * {@link #getConnection()} to get the database connection and disables
     * the autocommit feature.
     *
     * @throws Exception if an error occurs
     */
    protected void initConnection() throws Exception {
        con = getConnection();
        // JCR-1013: Setter may fail unnecessarily on a managed connection
        if (!con.getAutoCommit()) {
            con.setAutoCommit(true);
        }
    }

    /**
     * Abstract factory method for creating a new database connection. This
     * method is called by {@link #initConnection()} when the file system is
     * started. The returned connection should come with the default JDBC
     * settings, as the {@link #initConnection()} method will explicitly set
     * the <code>autoCommit</code> and other properties as needed.
     * <p>
     * Note that the returned database connection is kept during the entire
     * lifetime of the file system, after which it is closed by
     * {@link #close()} using the {@link #closeConnection(Connection)} method.
     *
     * @return new connection
     * @throws Exception if an error occurs
     */
    protected Connection getConnection() throws Exception {
        throw new UnsupportedOperationException("Override in a subclass!");
    }

    /**
     * Closes the given database connection. This method is called by
     * {@link #close()} to close the connection acquired using
     * {@link #getConnection()} when the file system was started.
     * <p>
     * The default implementation just calls the {@link Connection#close()}
     * method of the given connection, but subclasses can override this
     * method to provide more extensive database and connection cleanup.
     *
     * @param connection database connection
     * @throws SQLException if an error occurs
     */
    protected void closeConnection(Connection connection) throws SQLException {
        connection.close();
    }

    /**
     * Re-establishes the database connection. This method is called by
     * {@link #executeStmt(String, Object[])} after a <code>SQLException</code>
     * had been encountered.
     *
     * @return true if the connection could be successfully re-established,
     *         false otherwise.
     */
    protected synchronized boolean reestablishConnection() {
        // in any case try to shut down current connection
        // gracefully in order to avoid potential memory leaks

        // close shared prepared statements
        Iterator it = preparedStatements.values().iterator();
        while (it.hasNext()) {
            closeStatement((PreparedStatement) it.next());
        }
        try {
            closeConnection(con);
        } catch (Exception ignore) {
        }

        // sleep for a while to give database a chance
        // to restart before a reconnect is attempted

        try {
            Thread.sleep(SLEEP_BEFORE_RECONNECT);
        } catch (InterruptedException ignore) {
        }

        // now try to re-establish connection

        try {
            initConnection();
            initPreparedStatements();
            return true;
        } catch (Exception e) {
            log.error("failed to re-establish connection", e);
            // reconnect failed
            return false;
        }
    }

    /**
     * Executes the given SQL statement with the specified parameters.
     * If a <code>SQLException</code> is encountered <i>one</i> attempt is made
     * to re-establish the database connection and re-execute the statement.
     *
     * @param sql    statement to execute
     * @param params parameters to set
     * @return the <code>Statement</code> object that had been executed
     * @throws SQLException if an error occurs
     */
    protected Statement executeStmt(String sql, Object[] params) throws SQLException {
        int trials = 2;
        while (true) {
            PreparedStatement stmt = (PreparedStatement) preparedStatements.get(sql);
            try {
                for (int i = 0; i < params.length; i++) {
                    if (params[i] instanceof SizedInputStream) {
                        SizedInputStream in = (SizedInputStream) params[i];
                        stmt.setBinaryStream(i + 1, in, (int) in.getSize());
                    } else {
                        stmt.setObject(i + 1, params[i]);
                    }
                }
                stmt.execute();
                resetStatement(stmt);
                return stmt;
            } catch (SQLException se) {
                if (--trials == 0) {
                    // no more trials, re-throw
                    throw se;
                }
                log.warn("execute failed, about to reconnect...", se.getMessage());

                // try to reconnect
                if (reestablishConnection()) {
                    // reconnect succeeded; check whether it's possible to
                    // re-execute the prepared stmt with the given parameters
                    for (int i = 0; i < params.length; i++) {
                        if (params[i] instanceof SizedInputStream) {
                            SizedInputStream in = (SizedInputStream) params[i];
                            if (in.isConsumed()) {
                                // we're unable to re-execute the prepared stmt
                                // since an InputStream paramater has already
                                // been 'consumed';
                                // re-throw previous SQLException
                                throw se;
                            }
                        }
                    }

                    // try again to execute the statement
                    continue;
                } else {
                    // reconnect failed, re-throw previous SQLException
                    throw se;
                }
            }
        }
    }

    /**
     * Makes sure that <code>schemaObjectPrefix</code> does only consist of
     * characters that are allowed in names on the target database. Illegal
     * characters will be escaped as necessary.
     *
     * @throws Exception if an error occurs
     */
    protected void prepareSchemaObjectPrefix() throws Exception {
        DatabaseMetaData metaData = con.getMetaData();
        String legalChars = metaData.getExtraNameCharacters();
        legalChars += "ABCDEFGHIJKLMNOPQRSTUVWXZY0123456789_";

        String prefix = schemaObjectPrefix.toUpperCase();
        StringBuffer escaped = new StringBuffer();
        for (int i = 0; i < prefix.length(); i++) {
            char c = prefix.charAt(i);
            if (legalChars.indexOf(c) == -1) {
                escaped.append("_x");
                String hex = Integer.toHexString(c);
                escaped.append("0000".toCharArray(), 0, 4 - hex.length());
                escaped.append(hex);
                escaped.append("_");
            } else {
                escaped.append(c);
            }
        }
        schemaObjectPrefix = escaped.toString();
    }

    /**
     * Checks if the required schema objects exist and creates them if they
     * don't exist yet.
     *
     * @throws Exception if an error occurs
     */
    protected void checkSchema() throws Exception {
        DatabaseMetaData metaData = con.getMetaData();
        String tableName = schemaObjectPrefix + "FSENTRY";
        if (metaData.storesLowerCaseIdentifiers()) {
            tableName = tableName.toLowerCase();
        } else if (metaData.storesUpperCaseIdentifiers()) {
            tableName = tableName.toUpperCase();
        }
        ResultSet rs = metaData.getTables(null, null, tableName, null);
        boolean schemaExists;
        try {
            schemaExists = rs.next();
        } finally {
            rs.close();
        }

        if (!schemaExists) {
            // read ddl from resources
            InputStream in = DatabaseFileSystem.class.getResourceAsStream(schema + ".ddl");
            if (in == null) {
                String msg = "Configuration error: unknown schema '" + schema + "'";
                log.debug(msg);
                throw new RepositoryException(msg);
            }
            BufferedReader reader = new BufferedReader(new InputStreamReader(in));
            Statement stmt = con.createStatement();
            try {
                String sql = reader.readLine();
                while (sql != null) {
                    // Skip comments and empty lines
                    if (!sql.startsWith("#") && sql.length() > 0) {
                        // replace prefix variable
                        sql = createSchemaSql(sql);
                        stmt.executeUpdate(sql);
                    }
                    // read next sql stmt
                    sql = reader.readLine();
                }
            } finally {
                IOUtils.closeQuietly(in);
                closeStatement(stmt);
            }
        }
    }

    /**
     * Replace wildcards.
     */
    protected String createSchemaSql(String sql) {
        sql = Text.replace(sql, SCHEMA_OBJECT_PREFIX_VARIABLE, schemaObjectPrefix);
        return sql;
    }

    /**
     * Builds the SQL statements
     */
    protected void buildSQLStatements() {
        insertFileSQL = "insert into " + schemaObjectPrefix + "FSENTRY "
                + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + "FSENTRY_LASTMOD, FSENTRY_LENGTH) "
                + "values (?, ?, ?, ?, ?)";

        insertFolderSQL = "insert into " + schemaObjectPrefix + "FSENTRY "
                + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_LASTMOD, FSENTRY_LENGTH) " + "values (?, ?, ?, 0)";

        updateDataSQL = "update " + schemaObjectPrefix + "FSENTRY "
                + "set FSENTRY_DATA = ?, FSENTRY_LASTMOD = ?, FSENTRY_LENGTH = ? "
                + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + "and FSENTRY_DATA is not null";

        updateLastModifiedSQL = "update " + schemaObjectPrefix + "FSENTRY set FSENTRY_LASTMOD = ? "
                + "where FSENTRY_PATH = ? and FSENTRY_NAME = ? " + "and FSENTRY_DATA is not null";

        selectExistSQL = "select 1 from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ?";

        selectFileExistSQL = "select 1 from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null";

        selectFolderExistSQL = "select 1 from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ? and FSENTRY_DATA is null";

        selectFileNamesSQL = "select FSENTRY_NAME from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_DATA is not null";

        selectFolderNamesSQL = "select FSENTRY_NAME from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_DATA is null";

        selectFileAndFolderNamesSQL = "select FSENTRY_NAME from " + schemaObjectPrefix
                + "FSENTRY where FSENTRY_PATH = ?";

        selectChildCountSQL = "select count(FSENTRY_NAME) from " + schemaObjectPrefix
                + "FSENTRY where FSENTRY_PATH = ?  ";

        selectDataSQL = "select FSENTRY_DATA from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null";

        selectLastModifiedSQL = "select FSENTRY_LASTMOD from " + schemaObjectPrefix
                + "FSENTRY where FSENTRY_PATH = ? " + "and FSENTRY_NAME = ?";

        selectLengthSQL = "select FSENTRY_LENGTH from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null";

        deleteFileSQL = "delete from " + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? "
                + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null";

        deleteFolderSQL = "delete from " + schemaObjectPrefix + "FSENTRY where "
                + "(FSENTRY_PATH = ? and FSENTRY_NAME = ? and FSENTRY_DATA is null) " + "or (FSENTRY_PATH = ?) "
                + "or (FSENTRY_PATH like ?) ";

        copyFileSQL = "insert into " + schemaObjectPrefix + "FSENTRY "
                + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + "FSENTRY_LASTMOD, FSENTRY_LENGTH) "
                + "select ?, ?, FSENTRY_DATA, " + "FSENTRY_LASTMOD, FSENTRY_LENGTH from " + schemaObjectPrefix
                + "FSENTRY where FSENTRY_PATH = ? " + "and FSENTRY_NAME = ? and FSENTRY_DATA is not null";

        copyFilesSQL = "insert into " + schemaObjectPrefix + "FSENTRY "
                + "(FSENTRY_PATH, FSENTRY_NAME, FSENTRY_DATA, " + "FSENTRY_LASTMOD, FSENTRY_LENGTH) "
                + "select ?, FSENTRY_NAME, FSENTRY_DATA, " + "FSENTRY_LASTMOD, FSENTRY_LENGTH from "
                + schemaObjectPrefix + "FSENTRY where FSENTRY_PATH = ? " + "and FSENTRY_DATA is not null";
    }

    /**
     * Initializes the map of prepared statements.
     *
     * @throws SQLException if an error occurs
     */
    protected void initPreparedStatements() throws SQLException {
        preparedStatements.put(selectExistSQL, con.prepareStatement(selectExistSQL));
        preparedStatements.put(selectFileExistSQL, con.prepareStatement(selectFileExistSQL));
        preparedStatements.put(selectFolderExistSQL, con.prepareStatement(selectFolderExistSQL));
        preparedStatements.put(selectChildCountSQL, con.prepareStatement(selectChildCountSQL));
        preparedStatements.put(selectDataSQL, con.prepareStatement(selectDataSQL));
        preparedStatements.put(selectLastModifiedSQL, con.prepareStatement(selectLastModifiedSQL));
        preparedStatements.put(selectLengthSQL, con.prepareStatement(selectLengthSQL));
        preparedStatements.put(selectFileNamesSQL, con.prepareStatement(selectFileNamesSQL));
        preparedStatements.put(selectFolderNamesSQL, con.prepareStatement(selectFolderNamesSQL));
        preparedStatements.put(selectFileAndFolderNamesSQL, con.prepareStatement(selectFileAndFolderNamesSQL));
        preparedStatements.put(deleteFileSQL, con.prepareStatement(deleteFileSQL));
        preparedStatements.put(deleteFolderSQL, con.prepareStatement(deleteFolderSQL));
        preparedStatements.put(insertFileSQL, con.prepareStatement(insertFileSQL));
        preparedStatements.put(insertFolderSQL, con.prepareStatement(insertFolderSQL));
        preparedStatements.put(updateDataSQL, con.prepareStatement(updateDataSQL));
        preparedStatements.put(updateLastModifiedSQL, con.prepareStatement(updateLastModifiedSQL));
        preparedStatements.put(copyFileSQL, con.prepareStatement(copyFileSQL));
        preparedStatements.put(copyFilesSQL, con.prepareStatement(copyFilesSQL));

    }

    /**
     * Verifies that the root file system entry exists. If it doesn't exist yet
     * it will be automatically created.
     *
     * @throws Exception if an error occurs
     */
    protected void verifyRootExists() throws Exception {
        // check if root file system entry exists
        synchronized (selectFolderExistSQL) {
            ResultSet rs = null;
            try {
                Statement stmt = executeStmt(selectFolderExistSQL, new Object[] { FileSystem.SEPARATOR, "" });
                rs = stmt.getResultSet();

                if (rs.next()) {
                    // root entry exists
                    return;
                }
            } catch (SQLException e) {
                String msg = "failed to check existence of file system root entry";
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            } finally {
                closeResultSet(rs);
            }
        }

        // the root entry doesn't exist yet, create it...
        createDeepFolder(FileSystem.SEPARATOR);
    }

    /**
     * Creates the specified files system folder entry, recursively creating
     * any non-existing intermediate folder entries.
     *
     * @param folderPath folder entry to create
     * @throws FileSystemException if an error occurs
     */
    protected void createDeepFolder(String folderPath) throws FileSystemException {
        String parentDir = FileSystemPathUtil.getParentDir(folderPath);
        String name = FileSystemPathUtil.getName(folderPath);

        if (!FileSystemPathUtil.denotesRoot(folderPath)) {
            if (!exists(parentDir)) {
                createDeepFolder(parentDir);
            }
        }

        synchronized (insertFolderSQL) {
            try {
                executeStmt(insertFolderSQL,
                        new Object[] { parentDir, name, new Long(System.currentTimeMillis()) });
            } catch (SQLException e) {
                String msg = "failed to create folder entry: " + folderPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }
    }

    /**
     * Recursively copies the given folder to the given destination.
     *
     * @param srcPath folder to be copied
     * @param destPath destination path to which the folder is to be copied
     * @throws FileSystemException if an error occurs
     */
    protected void copyDeepFolder(String srcPath, String destPath) throws FileSystemException {

        if (!exists(destPath)) {
            createDeepFolder(destPath);
        }

        String[] names = listFolders(srcPath);

        for (int i = 0; i < names.length; i++) {
            String src = (FileSystemPathUtil.denotesRoot(srcPath) ? srcPath + names[i]
                    : srcPath + FileSystem.SEPARATOR + names[i]);
            String dest = (FileSystemPathUtil.denotesRoot(destPath) ? destPath + names[i]
                    : destPath + FileSystem.SEPARATOR + names[i]);
            copyDeepFolder(src, dest);
        }

        synchronized (copyFilesSQL) {
            try {
                executeStmt(copyFilesSQL, new Object[] { destPath, srcPath });
            } catch (SQLException e) {
                String msg = "failed to copy file entries from " + srcPath + " to " + destPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }
    }

    /**
     * Copies the given file entry to the given destination path. The parent
     * folder of the destination path will be created if it doesn't exist
     * already. If the destination path refers to an existing file, the file
     * will be overwritten.
     *
     * @param srcPath file to be copied
     * @param destPath destination path to which the file is to be copied
     * @throws FileSystemException if an error occurs
     */
    protected void copyFile(String srcPath, String destPath) throws FileSystemException {

        final String srcParentDir = FileSystemPathUtil.getParentDir(srcPath);
        final String srcName = FileSystemPathUtil.getName(srcPath);

        final String destParentDir = FileSystemPathUtil.getParentDir(destPath);
        final String destName = FileSystemPathUtil.getName(destPath);

        if (!exists(destParentDir)) {
            createDeepFolder(destParentDir);
        }
        if (isFile(destPath)) {
            deleteFile(destPath);
        }

        int count = 0;
        synchronized (copyFileSQL) {
            try {
                Statement stmt = executeStmt(copyFileSQL,
                        new Object[] { destParentDir, destName, srcParentDir, srcName });
                count = stmt.getUpdateCount();
            } catch (SQLException e) {
                String msg = "failed to copy file from " + srcPath + " to " + destPath;
                log.error(msg, e);
                throw new FileSystemException(msg, e);
            }
        }

        if (count == 0) {
            throw new FileSystemException("no such file: " + srcPath);
        }
    }

    /**
     * Resets the given <code>PreparedStatement</code> by clearing the parameters
     * and warnings contained.
     * <p/>
     * NOTE: This method MUST be called in a synchronized context as neither
     * this method nor the <code>PreparedStatement</code> instance on which it
     * operates are thread safe.
     *
     * @param stmt The <code>PreparedStatement</code> to reset. If
     *             <code>null</code> this method does nothing.
     */
    protected void resetStatement(PreparedStatement stmt) {
        if (stmt != null) {
            try {
                stmt.clearParameters();
                stmt.clearWarnings();
            } catch (SQLException se) {
                log.error("failed resetting PreparedStatement", se);
            }
        }
    }

    protected void closeResultSet(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException se) {
                log.error("failed closing ResultSet", se);
            }
        }
    }

    protected void closeStatement(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (SQLException se) {
                log.error("failed closing Statement", se);
            }
        }
    }

    //--------------------------------------------------------< inner classes >

    class SizedInputStream extends FilterInputStream {
        private final long size;
        private boolean consumed = false;

        SizedInputStream(InputStream in, long size) {
            super(in);
            this.size = size;
        }

        long getSize() {
            return size;
        }

        boolean isConsumed() {
            return consumed;
        }

        public int read() throws IOException {
            consumed = true;
            return super.read();
        }

        public long skip(long n) throws IOException {
            consumed = true;
            return super.skip(n);
        }

        public int read(byte[] b) throws IOException {
            consumed = true;
            return super.read(b);
        }

        public int read(byte[] b, int off, int len) throws IOException {
            consumed = true;
            return super.read(b, off, len);
        }
    }
}