You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jackrabbit.apache.org by th...@apache.org on 2014/02/05 10:27:23 UTC

svn commit: r1564687 [4/6] - in /jackrabbit/trunk: ./ jackrabbit-aws-ext/ jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ jackrabbit-aws-ext/src/main/java/org/apache/jackrabbit/aws/ext/ds/ jackrabbit-aws-ext/src/test/java/org/apache/jac...

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,1000 @@
+/*
+ * 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.data.db;
+
+import org.apache.commons.io.input.CountingInputStream;
+import org.apache.jackrabbit.core.data.AbstractDataStore;
+import org.apache.jackrabbit.core.data.DataIdentifier;
+import org.apache.jackrabbit.core.data.DataRecord;
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.core.data.MultiDataStoreAware;
+import org.apache.jackrabbit.core.util.db.CheckSchemaOperation;
+import org.apache.jackrabbit.core.util.db.ConnectionFactory;
+import org.apache.jackrabbit.core.util.db.ConnectionHelper;
+import org.apache.jackrabbit.core.util.db.DatabaseAware;
+import org.apache.jackrabbit.core.util.db.DbUtility;
+import org.apache.jackrabbit.core.util.db.StreamWrapper;
+import org.apache.jackrabbit.util.Text;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+import java.util.WeakHashMap;
+
+import javax.jcr.RepositoryException;
+import javax.sql.DataSource;
+
+/**
+ * A data store implementation that stores the records in a database using JDBC.
+ *
+ * Configuration:
+ * <pre>
+ * &lt;DataStore class="org.apache.jackrabbit.core.data.db.DbDataStore">
+ *     &lt;param name="{@link #setUrl(String) url}" value="jdbc:postgresql:test"/>
+ *     &lt;param name="{@link #setUser(String) user}" value="sa"/>
+ *     &lt;param name="{@link #setPassword(String) password}" value="sa"/>
+ *     &lt;param name="{@link #setDatabaseType(String) databaseType}" value="postgresql"/>
+ *     &lt;param name="{@link #setDriver(String) driver}" value="org.postgresql.Driver"/>
+ *     &lt;param name="{@link #setMinRecordLength(int) minRecordLength}" value="1024"/>
+ *     &lt;param name="{@link #setMaxConnections(int) maxConnections}" value="2"/>
+ *     &lt;param name="{@link #setCopyWhenReading(boolean) copyWhenReading}" value="true"/>
+ *     &lt;param name="{@link #setTablePrefix(String) tablePrefix}" value=""/>
+ *     &lt;param name="{@link #setSchemaObjectPrefix(String) schemaObjectPrefix}" value=""/>
+ *     &lt;param name="{@link #setSchemaCheckEnabled(String) schemaCheckEnabled}" value="true"/>
+ * &lt/DataStore>
+ * </pre>
+ * <p>
+ * Only URL, user name and password usually need to be set.
+ * The remaining settings are generated using the database URL sub-protocol from the
+ * database type resource file.
+ * <p>
+ * JNDI can be used to get the connection. In this case, use the javax.naming.InitialContext as the driver,
+ * and the JNDI name as the URL. If the user and password are configured in the JNDI resource,
+ * they should not be configured here. Example JNDI settings:
+ * <pre>
+ * &lt;param name="driver" value="javax.naming.InitialContext" />
+ * &lt;param name="url" value="java:comp/env/jdbc/Test" />
+ * </pre>
+ * <p>
+ * For Microsoft SQL Server 2005, there is a problem reading large BLOBs. You will need to use
+ * the JDBC driver version 1.2 or newer, and append ;responseBuffering=adaptive to the database URL.
+ * Don't append ;selectMethod=cursor, otherwise it can still run out of memory.
+ * Example database URL: jdbc:sqlserver://localhost:4220;DatabaseName=test;responseBuffering=adaptive
+ * <p>
+ * By default, the data is copied to a temp file when reading, to avoid problems when reading multiple
+ * blobs at the same time.
+ * <p>
+ * The tablePrefix can be used to specify a schema and / or catalog name:
+ * &lt;param name="tablePrefix" value="ds.">
+ */
+public class DbDataStore extends AbstractDataStore
+        implements DatabaseAware, MultiDataStoreAware {
+
+    /**
+     * The default value for the minimum object size.
+     */
+    public static final int DEFAULT_MIN_RECORD_LENGTH = 100;
+
+    /**
+     * Write to a temporary file to get the length (slow, but always works).
+     * This is the default setting.
+     */
+    public static final String STORE_TEMP_FILE = "tempFile";
+
+    /**
+     * Call PreparedStatement.setBinaryStream(..., -1)
+     */
+    public static final String STORE_SIZE_MINUS_ONE = "-1";
+
+    /**
+     * Call PreparedStatement.setBinaryStream(..., Integer.MAX_VALUE)
+     */
+    public static final String STORE_SIZE_MAX = "max";
+
+    /**
+     * The digest algorithm used to uniquely identify records.
+     */
+    protected static final String DIGEST = "SHA-1";
+
+    /**
+     * The prefix used for temporary objects.
+     */
+    protected static final String TEMP_PREFIX = "TEMP_";
+
+    /**
+     * Logger instance
+     */
+    private static Logger log = LoggerFactory.getLogger(DbDataStore.class);
+
+    /**
+     * The minimum modified date. If a file is accessed (read or write) with a modified date
+     * older than this value, the modified date is updated to the current time.
+     */
+    protected long minModifiedDate;
+
+    /**
+     * The database URL used.
+     */
+    protected String url;
+
+    /**
+     * The database driver.
+     */
+    protected String driver;
+
+    /**
+     * The user name.
+     */
+    protected String user;
+
+    /**
+     * The password
+     */
+    protected String password;
+
+    /**
+     * The database type used.
+     */
+    protected String databaseType;
+
+    /**
+     * The minimum size of an object that should be stored in this data store.
+     */
+    protected int minRecordLength = DEFAULT_MIN_RECORD_LENGTH;
+
+    /**
+     * The prefix for the datastore table, empty by default.
+     */
+    protected String tablePrefix = "";
+
+    /**
+     * The prefix of the table names. By default it is empty.
+     */
+    protected String schemaObjectPrefix = "";
+
+    /**
+     * Whether the schema check must be done during initialization.
+     */
+    private boolean schemaCheckEnabled = true;
+
+    /**
+     * The logical name of the DataSource to use.
+     */
+    protected String dataSourceName;
+
+    /**
+     * This is the property 'table'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String tableSQL = "DATASTORE";
+
+    /**
+     * This is the property 'createTable'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String createTableSQL =
+        "CREATE TABLE ${tablePrefix}${table}(ID VARCHAR(255) PRIMARY KEY, LENGTH BIGINT, LAST_MODIFIED BIGINT, DATA BLOB)";
+
+    /**
+     * This is the property 'insertTemp'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String insertTempSQL =
+        "INSERT INTO ${tablePrefix}${table} VALUES(?, 0, ?, NULL)";
+
+    /**
+     * This is the property 'updateData'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String updateDataSQL =
+        "UPDATE ${tablePrefix}${table} SET DATA=? WHERE ID=?";
+
+    /**
+     * This is the property 'updateLastModified'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String updateLastModifiedSQL =
+        "UPDATE ${tablePrefix}${table} SET LAST_MODIFIED=? WHERE ID=? AND LAST_MODIFIED<?";
+
+    /**
+     * This is the property 'update'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String updateSQL =
+        "UPDATE ${tablePrefix}${table} SET ID=?, LENGTH=?, LAST_MODIFIED=? " +
+        "WHERE ID=? AND LAST_MODIFIED=?";
+
+    /**
+     * This is the property 'delete'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String deleteSQL =
+        "DELETE FROM ${tablePrefix}${table} WHERE ID=?";
+
+    /**
+     * This is the property 'deleteOlder'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String deleteOlderSQL =
+        "DELETE FROM ${tablePrefix}${table} WHERE LAST_MODIFIED<?";
+
+    /**
+     * This is the property 'selectMeta'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String selectMetaSQL =
+        "SELECT LENGTH, LAST_MODIFIED FROM ${tablePrefix}${table} WHERE ID=?";
+
+    /**
+     * This is the property 'selectAll'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String selectAllSQL =
+        "SELECT ID FROM ${tablePrefix}${table}";
+
+    /**
+     * This is the property 'selectData'
+     * in the [databaseType].properties file, initialized with the default value.
+     */
+    protected String selectDataSQL =
+        "SELECT ID, DATA FROM ${tablePrefix}${table} WHERE ID=?";
+
+    /**
+     * The stream storing mechanism used.
+     */
+    protected String storeStream = STORE_TEMP_FILE;
+
+    /**
+     * Copy the stream to a temp file before returning it.
+     * Enabled by default to support concurrent reads.
+     */
+    protected boolean copyWhenReading = true;
+
+    /**
+     * All data identifiers that are currently in use are in this set until they are garbage collected.
+     */
+    protected Map<DataIdentifier, WeakReference<DataIdentifier>> inUse =
+        Collections.synchronizedMap(new WeakHashMap<DataIdentifier, WeakReference<DataIdentifier>>());
+
+    /**
+     * The temporary identifiers that are currently in use.
+     */
+    protected List<String> temporaryInUse = Collections.synchronizedList(new ArrayList<String>());
+
+    /**
+     * The {@link ConnectionHelper} set in the {@link #init(String)} method.
+     * */
+    protected ConnectionHelper conHelper;
+
+    /**
+     * The repositories {@link ConnectionFactory}.
+     */
+    private ConnectionFactory connectionFactory;
+
+    public void setConnectionFactory(ConnectionFactory connnectionFactory) {
+        this.connectionFactory = connnectionFactory;
+    }
+
+    public DataRecord addRecord(InputStream stream) throws DataStoreException {
+        InputStream fileInput = null;
+        String tempId = null;
+        ResultSet rs = null;
+        try {
+            long tempModified;
+            while (true) {
+                try {
+                    tempModified = System.currentTimeMillis();
+                    String id = UUID.randomUUID().toString();
+                    tempId = TEMP_PREFIX + id;
+                    temporaryInUse.add(tempId);
+                    // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=?
+                    rs = conHelper.query(selectMetaSQL, tempId);
+                    boolean hasNext = rs.next();
+                    DbUtility.close(rs);
+                    rs = null;
+                    if (hasNext) {
+                        // re-try in the very, very unlikely event that the row already exists
+                        continue;
+                    }
+                    // INSERT INTO DATASTORE VALUES(?, 0, ?, NULL)
+                    conHelper.exec(insertTempSQL, tempId, tempModified);
+                    break;
+                } catch (Exception e) {
+                    throw convert("Can not insert new record", e);
+                } finally {
+                    DbUtility.close(rs);
+                    // prevent that rs.close() is called again
+                    rs = null;
+                }
+            }
+            MessageDigest digest = getDigest();
+            DigestInputStream dIn = new DigestInputStream(stream, digest);
+            CountingInputStream in = new CountingInputStream(dIn);
+            StreamWrapper wrapper;
+            if (STORE_SIZE_MINUS_ONE.equals(storeStream)) {
+                wrapper = new StreamWrapper(in, -1);
+            } else if (STORE_SIZE_MAX.equals(storeStream)) {
+                wrapper = new StreamWrapper(in, Integer.MAX_VALUE);
+            } else if (STORE_TEMP_FILE.equals(storeStream)) {
+                File temp = moveToTempFile(in);
+                long length = temp.length();
+                wrapper = new StreamWrapper(new TempFileInputStream(temp, true), length);
+            } else {
+                throw new DataStoreException("Unsupported stream store algorithm: " + storeStream);
+            }
+            // UPDATE DATASTORE SET DATA=? WHERE ID=?
+            conHelper.exec(updateDataSQL, wrapper, tempId);
+            long length = in.getByteCount();
+            DataIdentifier identifier =
+                    new DataIdentifier(encodeHexString(digest.digest()));
+            usesIdentifier(identifier);
+            String id = identifier.toString();
+            long newModified;
+            while (true) {
+                newModified = System.currentTimeMillis();
+                if (checkExisting(tempId, length, identifier)) {
+                    touch(identifier, newModified);
+                    conHelper.exec(deleteSQL, tempId);
+                    break;
+                }
+                try {
+                    // UPDATE DATASTORE SET ID=?, LENGTH=?, LAST_MODIFIED=?
+                    // WHERE ID=? AND LAST_MODIFIED=?
+                    int count = conHelper.update(updateSQL,
+                            id, length, newModified, tempId, tempModified);
+                    // If update count is 0, the last modified time of the
+                    // temporary row was changed - which means we need to
+                    // re-try using a new last modified date (a later one)
+                    // because we need to ensure the new last modified date
+                    // is _newer_ than the old (otherwise the garbage
+                    // collection could delete rows)
+                    if (count != 0) {
+                        // update was successful
+                        break;
+                    }
+                } catch (SQLException e) {
+                    // duplicate key (the row already exists) - repeat
+                    // we use exception handling for flow control here, which is bad,
+                    // but the alternative is to use UPDATE ... WHERE ... (SELECT ...)
+                    // which could cause a deadlock in some databases - also,
+                    // duplicate key will only occur if somebody else concurrently
+                    // added the same record (which is very unlikely)
+                }
+                // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=?
+                rs = conHelper.query(selectMetaSQL, tempId);
+                if (!rs.next()) {
+                    // the row was deleted, which is unexpected / not allowed
+                    String msg =
+                        DIGEST + " temporary entry deleted: " +
+                            " id=" + tempId + " length=" + length;
+                    log.error(msg);
+                    throw new DataStoreException(msg);
+                }
+                tempModified = rs.getLong(2);
+                DbUtility.close(rs);
+                rs = null;
+            }
+            usesIdentifier(identifier);
+            DbDataRecord record = new DbDataRecord(this, identifier, length, newModified);
+            return record;
+        } catch (Exception e) {
+            throw convert("Can not insert new record", e);
+        } finally {
+            if (tempId != null) {
+                temporaryInUse.remove(tempId);
+            }
+            DbUtility.close(rs);
+            if (fileInput != null) {
+                try {
+                    fileInput.close();
+                } catch (IOException e) {
+                    throw convert("Can not close temporary file", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * Check if a row with this ID already exists.
+     *
+     * @return true if the row exists and the length matches
+     * @throw DataStoreException if a row exists, but the length is different
+     */
+    private boolean checkExisting(String tempId, long length, DataIdentifier identifier) throws DataStoreException, SQLException {
+        String id = identifier.toString();
+        // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID=?
+        ResultSet rs = null;
+        try {
+            rs = conHelper.query(selectMetaSQL, id);
+            if (rs.next()) {
+                long oldLength = rs.getLong(1);
+                long lastModified = rs.getLong(2);
+                if (oldLength != length) {
+                    String msg =
+                        DIGEST + " collision: temp=" + tempId
+                        + " id=" + id + " length=" + length
+                        + " oldLength=" + oldLength;
+                    log.error(msg);
+                    throw new DataStoreException(msg);
+                }
+                DbUtility.close(rs);
+                rs = null;
+                touch(identifier, lastModified);
+                // row already exists
+                conHelper.exec(deleteSQL, tempId);
+                return true;
+            }
+        } finally {
+            DbUtility.close(rs);
+        }
+        return false;
+    }
+
+    /**
+     * Creates a temp file and copies the data there.
+     * The input stream is closed afterwards.
+     *
+     * @param in the input stream
+     * @return the file
+     * @throws IOException
+     */
+    private File moveToTempFile(InputStream in) throws IOException {
+        File temp = File.createTempFile("dbRecord", null);
+        TempFileInputStream.writeToFileAndClose(in, temp);
+        return temp;
+    }
+
+    public synchronized void deleteRecord(DataIdentifier identifier) throws DataStoreException {
+        try {
+            conHelper.exec(deleteSQL, identifier.toString());
+        } catch (Exception e) {
+            throw convert("Can not delete record", e);
+        }
+    }
+
+    public synchronized int deleteAllOlderThan(long min) throws DataStoreException {
+        try {
+            ArrayList<String> touch = new ArrayList<String>();
+            ArrayList<DataIdentifier> ids = new ArrayList<DataIdentifier>(inUse.keySet());
+            for (DataIdentifier identifier: ids) {
+                if (identifier != null) {
+                    touch.add(identifier.toString());
+                }
+            }
+            touch.addAll(temporaryInUse);
+            for (String key : touch) {
+                updateLastModifiedDate(key, 0);
+            }
+            // DELETE FROM DATASTORE WHERE LAST_MODIFIED<?
+            return conHelper.update(deleteOlderSQL, min);
+        } catch (Exception e) {
+            throw convert("Can not delete records", e);
+        }
+    }
+
+    public Iterator<DataIdentifier> getAllIdentifiers() throws DataStoreException {
+        ArrayList<DataIdentifier> list = new ArrayList<DataIdentifier>();
+        ResultSet rs = null;
+        try {
+            // SELECT ID FROM DATASTORE
+            rs = conHelper.query(selectAllSQL);
+            while (rs.next()) {
+                String id = rs.getString(1);
+                if (!id.startsWith(TEMP_PREFIX)) {
+                    DataIdentifier identifier = new DataIdentifier(id);
+                    list.add(identifier);
+                }
+            }
+            log.debug("Found " + list.size() + " identifiers.");
+            return list.iterator();
+        } catch (Exception e) {
+            throw convert("Can not read records", e);
+        } finally {
+            DbUtility.close(rs);
+        }
+    }
+
+    public int getMinRecordLength() {
+        return minRecordLength;
+    }
+
+    /**
+     * Set the minimum object length.
+     * The maximum value is around 32000.
+     *
+     * @param minRecordLength the length
+     */
+    public void setMinRecordLength(int minRecordLength) {
+        this.minRecordLength = minRecordLength;
+    }
+
+    public DataRecord getRecordIfStored(DataIdentifier identifier) throws DataStoreException {
+        usesIdentifier(identifier);
+        ResultSet rs = null;
+        try {
+            String id = identifier.toString();
+            // SELECT LENGTH, LAST_MODIFIED FROM DATASTORE WHERE ID = ?
+            rs = conHelper.query(selectMetaSQL, id);
+            if (!rs.next()) {
+                return null;
+            }
+            long length = rs.getLong(1);
+            long lastModified = rs.getLong(2);
+            DbUtility.close(rs);
+            rs = null;
+            lastModified = touch(identifier, lastModified);
+            return new DbDataRecord(this, identifier, length, lastModified);
+        } catch (Exception e) {
+            throw convert("Can not read identifier " + identifier, e);
+        } finally {
+            DbUtility.close(rs);
+        }
+    }
+
+    /**
+     * Open the input stream. This method sets those fields of the caller
+     * that need to be closed once the input stream is read.
+     *
+     * @param inputStream the database input stream object
+     * @param identifier data identifier
+     * @throws DataStoreException if the data store could not be accessed,
+     *          or if the given identifier is invalid
+     */
+    InputStream openStream(DbInputStream inputStream, DataIdentifier identifier) throws DataStoreException {
+        ResultSet rs = null;
+        try {
+            // SELECT ID, DATA FROM DATASTORE WHERE ID = ?
+            rs = conHelper.query(selectDataSQL, identifier.toString());
+            if (!rs.next()) {
+                throw new DataStoreException("Record not found: " + identifier);
+            }
+            InputStream stream = rs.getBinaryStream(2);
+            if (stream == null) {
+                stream = new ByteArrayInputStream(new byte[0]);
+                DbUtility.close(rs);
+            } else if (copyWhenReading) {
+                // If we copy while reading, create a temp file and close the stream
+                File temp = moveToTempFile(stream);
+                stream = new BufferedInputStream(new TempFileInputStream(temp, false));
+                DbUtility.close(rs);
+            } else {
+                stream = new BufferedInputStream(stream);
+                inputStream.setResultSet(rs);
+            }
+            return stream;
+        } catch (Exception e) {
+            DbUtility.close(rs);
+            throw convert("Retrieving database resource ", e);
+        }
+    }
+
+    public synchronized void init(String homeDir) throws DataStoreException {
+        try {
+            initDatabaseType();
+
+            conHelper = createConnectionHelper(getDataSource());
+
+            if (isSchemaCheckEnabled()) {
+                createCheckSchemaOperation().run();
+            }
+        } catch (Exception e) {
+            throw convert("Can not init data store, driver=" + driver + " url=" + url + " user=" + user +
+                    " schemaObjectPrefix=" + schemaObjectPrefix + " tableSQL=" + tableSQL + " createTableSQL=" + createTableSQL, e);
+        }
+    }
+
+    private DataSource getDataSource() throws Exception {
+        if (getDataSourceName() == null || "".equals(getDataSourceName())) {
+            return connectionFactory.getDataSource(getDriver(), getUrl(), getUser(), getPassword());
+        } else {
+            return connectionFactory.getDataSource(dataSourceName);
+        }
+    }
+
+    /**
+     * This method is called from the {@link #init(String)} method of this class and returns a
+     * {@link ConnectionHelper} instance which is assigned to the {@code conHelper} field. Subclasses may
+     * override it to return a specialized connection helper.
+     *
+     * @param dataSrc the {@link DataSource} of this persistence manager
+     * @return a {@link ConnectionHelper}
+     * @throws Exception on error
+     */
+    protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception {
+        return new ConnectionHelper(dataSrc, false);
+    }
+
+    /**
+     * This method is called from {@link #init(String)} after the
+     * {@link #createConnectionHelper(DataSource)} method, and returns a default {@link CheckSchemaOperation}.
+     *
+     * @return a new {@link CheckSchemaOperation} instance
+     */
+    protected final CheckSchemaOperation createCheckSchemaOperation() {
+        String tableName = tablePrefix + schemaObjectPrefix + tableSQL;
+        return new CheckSchemaOperation(conHelper, new ByteArrayInputStream(createTableSQL.getBytes()), tableName);
+    }
+
+    protected void initDatabaseType() throws DataStoreException {
+        boolean failIfNotFound = false;
+        if (databaseType == null) {
+            if (dataSourceName != null) {
+                try {
+                    databaseType = connectionFactory.getDataBaseType(dataSourceName);
+                } catch (RepositoryException e) {
+                    throw new DataStoreException(e);
+                }
+            } else {
+                if (!url.startsWith("jdbc:")) {
+                    return;
+                }
+                int start = "jdbc:".length();
+                int end = url.indexOf(':', start);
+                databaseType = url.substring(start, end);
+            }
+        } else {
+            failIfNotFound = true;
+        }
+
+        InputStream in =
+            DbDataStore.class.getResourceAsStream(databaseType + ".properties");
+        if (in == null) {
+            if (failIfNotFound) {
+                String msg =
+                    "Configuration error: The resource '" + databaseType
+                    + ".properties' could not be found;"
+                    + " Please verify the databaseType property";
+                log.debug(msg);
+                throw new DataStoreException(msg);
+            } else {
+                return;
+            }
+        }
+        Properties prop = new Properties();
+        try {
+            try {
+                prop.load(in);
+            } finally {
+            in.close();
+            }
+        } catch (IOException e) {
+            String msg = "Configuration error: Could not read properties '" + databaseType + ".properties'";
+            log.debug(msg);
+            throw new DataStoreException(msg, e);
+        }
+        if (driver == null) {
+            driver = getProperty(prop, "driver", driver);
+        }
+        tableSQL = getProperty(prop, "table", tableSQL);
+        createTableSQL = getProperty(prop, "createTable", createTableSQL);
+        insertTempSQL = getProperty(prop, "insertTemp", insertTempSQL);
+        updateDataSQL = getProperty(prop, "updateData", updateDataSQL);
+        updateLastModifiedSQL = getProperty(prop, "updateLastModified", updateLastModifiedSQL);
+        updateSQL = getProperty(prop, "update", updateSQL);
+        deleteSQL = getProperty(prop, "delete", deleteSQL);
+        deleteOlderSQL = getProperty(prop, "deleteOlder", deleteOlderSQL);
+        selectMetaSQL = getProperty(prop, "selectMeta", selectMetaSQL);
+        selectAllSQL = getProperty(prop, "selectAll", selectAllSQL);
+        selectDataSQL = getProperty(prop, "selectData", selectDataSQL);
+        storeStream = getProperty(prop, "storeStream", storeStream);
+        if (!STORE_SIZE_MINUS_ONE.equals(storeStream)
+                && !STORE_TEMP_FILE.equals(storeStream)
+                && !STORE_SIZE_MAX.equals(storeStream)) {
+            String msg = "Unsupported Stream store mechanism: " + storeStream
+                    + " supported are: " + STORE_SIZE_MINUS_ONE + ", "
+                    + STORE_TEMP_FILE + ", " + STORE_SIZE_MAX;
+            log.debug(msg);
+            throw new DataStoreException(msg);
+        }
+    }
+
+    /**
+     * Get the expanded property value. The following placeholders are supported:
+     * ${table}: the table name (the default is DATASTORE) and
+     * ${tablePrefix}: tablePrefix plus schemaObjectPrefix as set in the configuration
+     *
+     * @param prop the properties object
+     * @param key the key
+     * @param defaultValue the default value
+     * @return the property value (placeholders are replaced)
+     */
+    protected String getProperty(Properties prop, String key, String defaultValue) {
+        String sql = prop.getProperty(key, defaultValue);
+        sql = Text.replace(sql, "${table}", tableSQL).trim();
+        sql = Text.replace(sql, "${tablePrefix}", tablePrefix + schemaObjectPrefix).trim();
+        return sql;
+    }
+
+    /**
+     * Convert an exception to a data store exception.
+     *
+     * @param cause the message
+     * @param e the root cause
+     * @return the data store exception
+     */
+    protected DataStoreException convert(String cause, Exception e) {
+        log.warn(cause, e);
+        if (e instanceof DataStoreException) {
+            return (DataStoreException) e;
+        } else {
+            return new DataStoreException(cause, e);
+        }
+    }
+
+    public void updateModifiedDateOnAccess(long before) {
+        log.debug("Update modifiedDate on access before " + before);
+        minModifiedDate = before;
+    }
+
+    /**
+     * Update the modified date of an entry if required.
+     *
+     * @param identifier the entry identifier
+     * @param lastModified the current last modified date
+     * @return the new modified date
+     */
+    long touch(DataIdentifier identifier, long lastModified) throws DataStoreException {
+        usesIdentifier(identifier);
+        return updateLastModifiedDate(identifier.toString(), lastModified);
+    }
+
+    private long updateLastModifiedDate(String key, long lastModified) throws DataStoreException {
+        if (lastModified < minModifiedDate) {
+            long now = System.currentTimeMillis();
+            try {
+                // UPDATE DATASTORE SET LAST_MODIFIED = ? WHERE ID = ? AND LAST_MODIFIED < ?
+                conHelper.update(updateLastModifiedSQL, now, key, now);
+                return now;
+            } catch (Exception e) {
+                throw convert("Can not update lastModified", e);
+            }
+        }
+        return lastModified;
+    }
+
+    /**
+     * Get the database type (if set).
+     * @return the database type
+     */
+    public String getDatabaseType() {
+        return databaseType;
+    }
+
+    /**
+     * Set the database type. By default the sub-protocol of the JDBC database URL is used if it is not set.
+     * It must match the resource file [databaseType].properties. Example: mysql.
+     *
+     * @param databaseType
+     */
+    public void setDatabaseType(String databaseType) {
+        this.databaseType = databaseType;
+    }
+
+    /**
+     * Get the database driver
+     *
+     * @return the driver
+     */
+    public String getDriver() {
+        return driver;
+    }
+
+    /**
+     * Set the database driver class name.
+     * If not set, the default driver class name for the database type is used,
+     * as set in the [databaseType].properties resource; key 'driver'.
+     *
+     * @param driver
+     */
+    public void setDriver(String driver) {
+        this.driver = driver;
+    }
+
+    /**
+     * Get the password.
+     *
+     * @return the password
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    /**
+     * Set the password.
+     *
+     * @param password
+     */
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    /**
+     * Get the database URL.
+     *
+     * @return the URL
+     */
+    public String getUrl() {
+        return url;
+    }
+
+    /**
+     * Set the database URL.
+     * Example: jdbc:postgresql:test
+     *
+     * @param url
+     */
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    /**
+     * Get the user name.
+     *
+     * @return the user name
+     */
+    public String getUser() {
+        return user;
+    }
+
+    /**
+     * Set the user name.
+     *
+     * @param user
+     */
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    /**
+     * @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;
+    }
+
+    public synchronized void close() throws DataStoreException {
+        // nothing to do
+    }
+
+    protected void usesIdentifier(DataIdentifier identifier) {
+        inUse.put(identifier, new WeakReference<DataIdentifier>(identifier));
+    }
+
+    public void clearInUse() {
+        inUse.clear();
+    }
+
+    protected synchronized MessageDigest getDigest() throws DataStoreException {
+        try {
+            return MessageDigest.getInstance(DIGEST);
+        } catch (NoSuchAlgorithmException e) {
+            throw convert("No such algorithm: " + DIGEST, e);
+        }
+    }
+
+    /**
+     * Get the maximum number of concurrent connections.
+     *
+     * @deprecated
+     * @return the maximum number of connections.
+     */
+    public int getMaxConnections() {
+        return -1;
+    }
+
+    /**
+     * Set the maximum number of concurrent connections in the pool.
+     * At least 3 connections are required if the garbage collection process is used.
+     *
+     *@deprecated
+     * @param maxConnections the new value
+     */
+    public void setMaxConnections(int maxConnections) {
+        // no effect
+    }
+
+    /**
+     * Is a stream copied to a temporary file before returning?
+     *
+     * @return the setting
+     */
+    public boolean getCopyWhenReading() {
+        return copyWhenReading;
+    }
+
+    /**
+     * The the copy setting. If enabled,
+     * a stream is always copied to a temporary file when reading a stream.
+     *
+     * @param copyWhenReading the new setting
+     */
+    public void setCopyWhenReading(boolean copyWhenReading) {
+        this.copyWhenReading = copyWhenReading;
+    }
+
+    /**
+     * Get the table prefix.
+     *
+     * @return the table prefix.
+     */
+    public String getTablePrefix() {
+        return tablePrefix;
+    }
+
+    /**
+     * Set the new table prefix. The default is empty.
+     * The table name is constructed like this:
+     * ${tablePrefix}${schemaObjectPrefix}${tableName}
+     *
+     * @param tablePrefix the new value
+     */
+    public void setTablePrefix(String tablePrefix) {
+        this.tablePrefix = tablePrefix;
+    }
+
+    /**
+     * Get the schema prefix.
+     *
+     * @return the schema object prefix
+     */
+    public String getSchemaObjectPrefix() {
+        return schemaObjectPrefix;
+    }
+
+    /**
+     * Set the schema object prefix. The default is empty.
+     * The table name is constructed like this:
+     * ${tablePrefix}${schemaObjectPrefix}${tableName}
+     *
+     * @param schemaObjectPrefix the new prefix
+     */
+    public void setSchemaObjectPrefix(String schemaObjectPrefix) {
+        this.schemaObjectPrefix = schemaObjectPrefix;
+    }
+
+    public String getDataSourceName() {
+        return dataSourceName;
+    }
+
+    public void setDataSourceName(String dataSourceName) {
+        this.dataSourceName = dataSourceName;
+    }
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbInputStream.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,190 @@
+/*
+ * 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.data.db;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.sql.ResultSet;
+
+import org.apache.commons.io.input.AutoCloseInputStream;
+import org.apache.jackrabbit.core.data.DataIdentifier;
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.core.util.db.DbUtility;
+
+/**
+ * This class represents an input stream backed by a database. The database
+ * objects are only acquired when reading from the stream, and stay open until
+ * the stream is closed, fully read, or garbage collected.
+ * <p>
+ * This class does not support mark/reset. It is always to be wrapped
+ * using a BufferedInputStream.
+ */
+public class DbInputStream extends AutoCloseInputStream {
+
+    protected DbDataStore store;
+    protected DataIdentifier identifier;
+    protected boolean endOfStream;
+
+    protected ResultSet rs;
+
+    /**
+     * Create a database input stream for the given identifier.
+     * Database access is delayed until the first byte is read from the stream.
+     *
+     * @param store the database data store
+     * @param identifier the data identifier
+     */
+    protected DbInputStream(DbDataStore store, DataIdentifier identifier) {
+        super(null);
+        this.store = store;
+        this.identifier = identifier;
+    }
+
+    /**
+     * Open the stream if required.
+     *
+     * @throws IOException
+     */
+    protected void openStream() throws IOException {
+        if (endOfStream) {
+            throw new EOFException();
+        }
+        if (in == null) {
+            try {
+                in = store.openStream(this, identifier);
+            } catch (DataStoreException e) {
+                IOException e2 = new IOException(e.getMessage());
+                e2.initCause(e);
+                throw e2;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     * When the stream is consumed, the database objects held by the instance are closed.
+     */
+    public int read() throws IOException {
+        if (endOfStream) {
+            return -1;
+        }
+        openStream();
+        int c = in.read();
+        if (c == -1) {
+            endOfStream = true;
+            close();
+        }
+        return c;
+    }
+
+    /**
+     * {@inheritDoc}
+     * When the stream is consumed, the database objects held by the instance are closed.
+     */
+    public int read(byte[] b) throws IOException {
+        return read(b, 0, b.length);
+    }
+
+    /**
+     * {@inheritDoc}
+     * When the stream is consumed, the database objects held by the instance are closed.
+     */
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (endOfStream) {
+            return -1;
+        }
+        openStream();
+        int c = in.read(b, off, len);
+        if (c == -1) {
+            endOfStream = true;
+            close();
+        }
+        return c;
+    }
+
+    /**
+     * {@inheritDoc}
+     * When the stream is consumed, the database objects held by the instance are closed.
+     */
+    public void close() throws IOException {
+        if (in != null) {
+            in.close();
+            in = null;
+            // some additional database objects
+            // may need to be closed
+            if (rs != null) {
+                DbUtility.close(rs);
+                rs = null;
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public long skip(long n) throws IOException {
+        if (endOfStream) {
+            return -1;
+        }
+        openStream();
+        return in.skip(n);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public int available() throws IOException {
+        if (endOfStream) {
+            return 0;
+        }
+        openStream();
+        return in.available();
+    }
+
+    /**
+     * This method does nothing.
+     */
+    public void mark(int readlimit) {
+        // do nothing
+    }
+
+    /**
+     * This method does nothing.
+     */
+    public void reset() throws IOException {
+        // do nothing
+    }
+
+    /**
+     * Check whether mark and reset are supported.
+     *
+     * @return false
+     */
+    public boolean markSupported() {
+        return false;
+    }
+
+    /**
+     * Set the result set of this input stream. This object must be closed once
+     * the stream is closed.
+     *
+     * @param rs the result set
+     */
+    void setResultSet(ResultSet rs) {
+        this.rs = rs;
+    }
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DerbyDataStore.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,53 @@
+/*
+ * 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.data.db;
+
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.apache.jackrabbit.core.data.DataStoreException;
+import org.apache.jackrabbit.core.util.db.ConnectionHelper;
+import org.apache.jackrabbit.core.util.db.DerbyConnectionHelper;
+
+/**
+ * The Derby data store closes the database when the data store is closed
+ * (embedded databases only).
+ */
+public class DerbyDataStore extends DbDataStore {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected ConnectionHelper createConnectionHelper(DataSource dataSrc) throws Exception {
+        return new DerbyConnectionHelper(dataSrc, false);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public synchronized void close() throws DataStoreException {
+        super.close();
+        try {
+            ((DerbyConnectionHelper) conHelper).shutDown(getDriver());
+        } catch (SQLException e) {
+            throw new DataStoreException(e);
+        }
+    }
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/TempFileInputStream.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,148 @@
+/*
+ * 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.data.db;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.AutoCloseInputStream;
+
+/**
+ * An input stream from a temporary file. The file is deleted when the stream is
+ * closed, fully read, or garbage collected.
+ * <p>
+ * This class does not support mark/reset. It is always to be wrapped
+ * using a BufferedInputStream.
+ */
+public class TempFileInputStream extends AutoCloseInputStream {
+
+    private final File file;
+    private boolean closed;
+    private boolean delayedResourceCleanup = true;
+
+    /**
+     * Copy the data to a file and close the input stream afterwards.
+     *
+     * @param in the input stream
+     * @param file the target file
+     * @return the size of the file
+     */
+    public static long writeToFileAndClose(InputStream in, File file) throws IOException {
+        OutputStream out = new FileOutputStream(file);
+        IOUtils.copy(in, out);
+        out.close();
+        in.close();
+        return file.length();
+    }
+
+    /**
+     * Construct a new temporary file input stream.
+     * The file is deleted if the input stream is closed or fully read and 
+     * delayedResourceCleanup was set to true. Otherwise you must call {@link #deleteFile()}.
+     * Deleting is only attempted once.
+     *
+     * @param file the temporary file
+     * @param delayedResourceCleanup
+     */
+    public TempFileInputStream(File file, boolean delayedResourceCleanup) throws FileNotFoundException {
+        super(new BufferedInputStream(new FileInputStream(file)));
+        this.file = file;
+        this.delayedResourceCleanup = delayedResourceCleanup;
+    }
+
+    public File getFile() {
+    	return file;
+    }
+    
+    public void deleteFile() {
+	    file.delete();
+	}
+
+	private int closeIfEOF(int read) throws IOException {
+        if (read < 0) {
+            close();
+        }
+        return read;
+    }
+
+    public void close() throws IOException {
+        if (!closed) {
+            in.close();
+            if (!delayedResourceCleanup) {
+            	deleteFile();
+            }
+            closed = true;
+        }
+    }
+
+    public int available() throws IOException {
+        return in.available();
+    }
+
+    /**
+     * This method does nothing.
+     */
+    public void mark(int readlimit) {
+        // do nothing
+    }
+
+    /**
+     * Check whether mark and reset are supported.
+     *
+     * @return false
+     */
+    public boolean markSupported() {
+        return false;
+    }
+
+    public long skip(long n) throws IOException {
+        return in.skip(n);
+    }
+
+    public void reset() throws IOException {
+        in.reset();
+    }
+
+    public int read(byte[] b, int off, int len) throws IOException {
+        if (closed) {
+            return -1;
+        }
+        return closeIfEOF(in.read(b, off, len));
+    }
+
+    public int read(byte[] b) throws IOException {
+        if (closed) {
+            return -1;
+        }
+        return closeIfEOF(in.read(b));
+    }
+
+    public int read() throws IOException {
+        if (closed) {
+            return -1;
+        }
+        return closeIfEOF(in.read());
+    }
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/BasedFileSystem.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,187 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+
+/**
+ * A <code>BasedFileSystem</code> represents a 'file system in a file system'.
+ */
+public class BasedFileSystem implements FileSystem {
+
+    protected final FileSystem fsBase;
+
+    protected final String basePath;
+
+    /**
+     * Creates a new <code>BasedFileSystem</code>
+     *
+     * @param fsBase      the <code>FileSystem</code> the new file system should be based on
+     * @param relRootPath the root path relative to <code>fsBase</code>'s root
+     */
+    public BasedFileSystem(FileSystem fsBase, String relRootPath) {
+        if (fsBase == null) {
+            throw new IllegalArgumentException("invalid file system argument");
+        }
+        this.fsBase = fsBase;
+
+        if (relRootPath == null) {
+            throw new IllegalArgumentException("invalid null path argument");
+        }
+        if (relRootPath.equals(SEPARATOR)) {
+            throw new IllegalArgumentException("invalid path argument");
+        }
+        if (!relRootPath.startsWith(SEPARATOR)) {
+            relRootPath = SEPARATOR + relRootPath;
+        }
+        if (relRootPath.endsWith(SEPARATOR)) {
+            relRootPath = relRootPath.substring(0, relRootPath.length() - 1);
+
+        }
+        this.basePath = relRootPath;
+    }
+
+    protected String buildBasePath(String path) {
+        if (path.startsWith(SEPARATOR)) {
+            if (path.length() == 1) {
+                return basePath;
+            } else {
+                return basePath + path;
+            }
+        } else {
+            return basePath + SEPARATOR + path;
+        }
+    }
+
+    //-----------------------------------------------------------< FileSystem >
+    /**
+     * {@inheritDoc}
+     */
+    public void init() throws FileSystemException {
+        // check base path
+        if (!fsBase.isFolder(basePath)) {
+            fsBase.createFolder(basePath);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void close() throws FileSystemException {
+        // do nothing; base file system should be closed explicitly
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void createFolder(String folderPath) throws FileSystemException {
+        fsBase.createFolder(buildBasePath(folderPath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void deleteFile(String filePath) throws FileSystemException {
+        fsBase.deleteFile(buildBasePath(filePath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void deleteFolder(String folderPath) throws FileSystemException {
+        fsBase.deleteFolder(buildBasePath(folderPath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean exists(String path) throws FileSystemException {
+        return fsBase.exists(buildBasePath(path));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public InputStream getInputStream(String filePath) throws FileSystemException {
+        return fsBase.getInputStream(buildBasePath(filePath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public OutputStream getOutputStream(String filePath) throws FileSystemException {
+        return fsBase.getOutputStream(buildBasePath(filePath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasChildren(String path) throws FileSystemException {
+        return fsBase.hasChildren(buildBasePath(path));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isFile(String path) throws FileSystemException {
+        return fsBase.isFile(buildBasePath(path));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isFolder(String path) throws FileSystemException {
+        return fsBase.isFolder(buildBasePath(path));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public long lastModified(String path) throws FileSystemException {
+        return fsBase.lastModified(buildBasePath(path));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public long length(String filePath) throws FileSystemException {
+        return fsBase.length(buildBasePath(filePath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String[] list(String folderPath) throws FileSystemException {
+        return fsBase.list(buildBasePath(folderPath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String[] listFiles(String folderPath) throws FileSystemException {
+        return fsBase.listFiles(buildBasePath(folderPath));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String[] listFolders(String folderPath) throws FileSystemException {
+        return fsBase.listFolders(buildBasePath(folderPath));
+    }
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystem.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,206 @@
+/*
+ * 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;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * The <code>FileSystem</code> interface is an abstraction of a virtual
+ * file system. The similarities of its method names with with the methods
+ * of the <code>java.io.File</code> class are intentional.
+ * <br>
+ * Implementations of this interface expose a file system-like resource.
+ * File system-like resources include WebDAV-enabled servers, local file systems,
+ * and so forth.
+ */
+public interface FileSystem {
+
+    /**
+     * File separator
+     */
+    String SEPARATOR = "/";
+
+    /**
+     * File separator character
+     */
+    char SEPARATOR_CHAR = '/';
+
+    /**
+     * Initialize the file system
+     *
+     * @throws FileSystemException if the file system initialization fails
+     */
+    void init() throws FileSystemException;
+
+    /**
+     * Close the file system. After calling this method, the file system is no
+     * longer accessible.
+     *
+     * @throws FileSystemException
+     */
+    void close() throws FileSystemException;
+
+    /**
+     * Returns an input stream of the contents of the file denoted by this path.
+     *
+     * @param filePath the path of the file.
+     * @return an input stream of the contents of the file.
+     * @throws FileSystemException if the file does not exist
+     *                             or if it cannot be read from
+     */
+    InputStream getInputStream(String filePath) throws FileSystemException;
+
+    /**
+     * Returns an output stream for writing bytes to the file denoted by this path.
+     * The file will be created if it doesn't exist. If the file exists, its contents
+     * will be overwritten.
+     *
+     * @param filePath the path of the file.
+     * @return an output stream for writing bytes to the file.
+     * @throws FileSystemException if the file cannot be written to or created
+     */
+    OutputStream getOutputStream(String filePath) throws FileSystemException;
+
+    /**
+     * Creates the folder named by this path, including any necessary but
+     * nonexistent parent folders. Note that if this operation fails it
+     * may have succeeded in creating some of the necessary parent folders.
+     *
+     * @param folderPath the path of the folder to be created.
+     * @throws FileSystemException if a file system entry denoted by path
+     *                             already exists or if another error occurs.
+     */
+    void createFolder(String folderPath) throws FileSystemException;
+
+    /**
+     * Tests whether the file system entry denoted by this path exists.
+     *
+     * @param path the path of a file system entry.
+     * @return true if the file system entry at path exists; false otherwise.
+     * @throws FileSystemException
+     */
+    boolean exists(String path) throws FileSystemException;
+
+    /**
+     * Tests whether the file system entry denoted by this path exists and
+     * is a file.
+     *
+     * @param path the path of a file system entry.
+     * @return true if the file system entry at path is a file; false otherwise.
+     * @throws FileSystemException
+     */
+    boolean isFile(String path) throws FileSystemException;
+
+    /**
+     * Tests whether the file system entry denoted by this path exists and
+     * is a folder.
+     *
+     * @param path the path of a file system entry.
+     * @return true if the file system entry at path is a folder; false otherwise.
+     * @throws FileSystemException
+     */
+    boolean isFolder(String path) throws FileSystemException;
+
+    /**
+     * Tests whether the file system entry denoted by this path has child entries.
+     *
+     * @param path the path of a file system entry.
+     * @return true if the file system entry at path has child entries; false otherwise.
+     * @throws FileSystemException
+     */
+    boolean hasChildren(String path) throws FileSystemException;
+
+    /**
+     * Returns the length of the file denoted by this path.
+     *
+     * @param filePath the path of the file.
+     * @return The length, in bytes, of the file denoted by this path,
+     *         or -1L if the length can't be determined.
+     * @throws FileSystemException if the path does not denote an existing file.
+     */
+    long length(String filePath) throws FileSystemException;
+
+    /**
+     * Returns the time that the file system entry denoted by this path
+     * was last modified.
+     *
+     * @param path the path of a file system entry.
+     * @return A long value representing the time the file system entry was
+     *         last modified, measured in milliseconds since the epoch
+     *         (00:00:00 GMT, January 1, 1970), or 0L if the modification
+     *         time can't be determined.
+     * @throws FileSystemException if the file system entry does not exist.
+     */
+    long lastModified(String path) throws FileSystemException;
+
+    /**
+     * Returns an array of strings naming the files and folders
+     * in the folder denoted by this path.
+     *
+     * @param folderPath the path of the folder whose contents is to be listed.
+     * @return an array of strings naming the files and folders
+     *         in the folder denoted by this path.
+     * @throws FileSystemException if this path does not denote a folder or if
+     *                             another error occurs.
+     */
+    String[] list(String folderPath) throws FileSystemException;
+
+    /**
+     * Returns an array of strings naming the files in the folder
+     * denoted by this path.
+     *
+     * @param folderPath the path of the folder whose contents is to be listed.
+     * @return an array of strings naming the files in the folder
+     *         denoted by this path.
+     * @throws FileSystemException if this path does not denote a folder or if
+     *                             another error occurs.
+     */
+    String[] listFiles(String folderPath) throws FileSystemException;
+
+    /**
+     * Returns an array of strings naming the folders in the folder
+     * denoted by this path.
+     *
+     * @param folderPath the path of the folder whose contents is to be listed.
+     * @return an array of strings naming the folders in the folder
+     *         denoted by this path.
+     * @throws FileSystemException if this path does not denote a folder or if
+     *                             another error occurs.
+     */
+    String[] listFolders(String folderPath) throws FileSystemException;
+
+    /**
+     * Deletes the file denoted by this path.
+     *
+     * @param filePath the path of the file to be deleted.
+     * @throws FileSystemException if this path does not denote a file or if
+     *                             another error occurs.
+     */
+    void deleteFile(String filePath) throws FileSystemException;
+
+    /**
+     * Deletes the folder denoted by this path. Any contents of this folder
+     * (folders and files) will be deleted recursively.
+     *
+     * @param folderPath the path of the folder to be deleted.
+     * @throws FileSystemException if this path does not denote a folder or if
+     *                             another error occurs.
+     */
+    void deleteFolder(String folderPath) throws FileSystemException;
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemException.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,49 @@
+/*
+ * 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;
+
+/**
+ * The <code>FileSystemException</code> signals an error within a file system
+ * operation. FileSystemExceptions are thrown by {@link FileSystem}
+ * implementations.
+ */
+public class FileSystemException extends Exception {
+
+    /**
+     * Constructs a new instance of this class with the specified detail
+     * message.
+     *
+     * @param message the detail message. The detail message is saved for
+     *                later retrieval by the {@link #getMessage()} method.
+     */
+    public FileSystemException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new instance of this class with the specified detail
+     * message and root cause.
+     *
+     * @param message   the detail message. The detail message is saved for
+     *                  later retrieval by the {@link #getMessage()} method.
+     * @param rootCause root failure cause
+     */
+    public FileSystemException(String message, Throwable rootCause) {
+        super(message, rootCause);
+    }
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemFactory.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+import javax.jcr.RepositoryException;
+
+
+/**
+ * Factory interface for creating {@link FileSystem} instances. Used
+ * to decouple the repository internals from the repository configuration
+ * mechanism.
+ */
+public interface FileSystemFactory {
+
+    /**
+     * Creates, initializes, and returns a {@link FileSystem} instance
+     * for use by the repository. Note that no information is passed from
+     * the client, so all required configuration information must be
+     * encapsulated in the factory.
+     *
+     * @return initialized file system
+     * @throws RepositoryException if the file system can not be created
+     */
+    FileSystem getFileSystem() throws RepositoryException;
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemPathUtil.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,229 @@
+/*
+ * 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;
+
+import java.io.ByteArrayOutputStream;
+import java.util.BitSet;
+
+
+/**
+ * Utility class for handling paths in a file system.
+ */
+public final class FileSystemPathUtil {
+
+    /**
+     * Array of lowercase hexadecimal characters used in creating hex escapes.
+     */
+    private static final char[] HEX_TABLE = "0123456789abcdef".toCharArray();
+
+    /**
+     * The escape character used to mark hex escape sequences.
+     */
+    private static final char ESCAPE_CHAR = '%';
+
+    /**
+     * The list of characters that are not encoded by the <code>escapeName(String)</code>
+     * and <code>unescape(String)</code> methods. They contains the characters
+     * which can safely be used in file names:
+     */
+    public static final BitSet SAFE_NAMECHARS;
+
+    /**
+     * The list of characters that are not encoded by the <code>escapePath(String)</code>
+     * and <code>unescape(String)</code> methods. They contains the characters
+     * which can safely be used in file paths:
+     */
+    public static final BitSet SAFE_PATHCHARS;
+
+    static {
+        // build list of valid name characters
+        SAFE_NAMECHARS = new BitSet(256);
+        int i;
+        for (i = 'a'; i <= 'z'; i++) {
+            SAFE_NAMECHARS.set(i);
+        }
+        for (i = 'A'; i <= 'Z'; i++) {
+            SAFE_NAMECHARS.set(i);
+        }
+        for (i = '0'; i <= '9'; i++) {
+            SAFE_NAMECHARS.set(i);
+        }
+        SAFE_NAMECHARS.set('-');
+        SAFE_NAMECHARS.set('_');
+        SAFE_NAMECHARS.set('.');
+
+        // build list of valid path characters (includes name characters)
+        SAFE_PATHCHARS = (BitSet) SAFE_NAMECHARS.clone();
+        SAFE_PATHCHARS.set(FileSystem.SEPARATOR_CHAR);
+    }
+
+    /**
+     * private constructor
+     */
+    private FileSystemPathUtil() {
+    }
+
+    /**
+     * Escapes the given string using URL encoding for all bytes not included
+     * in the given set of safe characters.
+     *
+     * @param s the string to escape
+     * @param safeChars set of safe characters (bytes)
+     * @return escaped string
+     */
+    private static String escape(String s, BitSet safeChars) {
+        byte[] bytes = s.getBytes();
+        StringBuilder out = new StringBuilder(bytes.length);
+        for (int i = 0; i < bytes.length; i++) {
+            int c = bytes[i] & 0xff;
+            if (safeChars.get(c) && c != ESCAPE_CHAR) {
+                out.append((char) c);
+            } else {
+                out.append(ESCAPE_CHAR);
+                out.append(HEX_TABLE[(c >> 4) & 0x0f]);
+                out.append(HEX_TABLE[(c) & 0x0f]);
+            }
+        }
+        return out.toString();
+    }
+
+    /**
+     * Encodes the specified <code>path</code>. Same as
+     * <code>{@link #escapeName(String)}</code> except that the separator
+     * character <b><code>/</code></b> is regarded as a legal path character
+     * that needs no escaping.
+     *
+     * @param path the path to encode.
+     * @return the escaped path
+     */
+    public static String escapePath(String path) {
+        return escape(path, SAFE_PATHCHARS);
+    }
+
+    /**
+     * Encodes the specified <code>name</code>. Same as
+     * <code>{@link #escapePath(String)}</code> except that the separator character
+     * <b><code>/</code></b> is regarded as an illegal character that needs
+     * escaping.
+     *
+     * @param name the name to encode.
+     * @return the escaped name
+     */
+    public static String escapeName(String name) {
+        return escape(name, SAFE_NAMECHARS);
+    }
+
+    /**
+     * Decodes the specified path/name.
+     *
+     * @param pathOrName the escaped path/name
+     * @return the unescaped path/name
+     */
+    public static String unescape(String pathOrName) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream(pathOrName.length());
+        for (int i = 0; i < pathOrName.length(); i++) {
+            char c = pathOrName.charAt(i);
+            if (c == ESCAPE_CHAR) {
+                try {
+                    out.write(Integer.parseInt(pathOrName.substring(i + 1, i + 3), 16));
+                } catch (NumberFormatException e) {
+                    IllegalArgumentException iae = new IllegalArgumentException("Failed to unescape escape sequence");
+                    iae.initCause(e);
+                    throw iae;
+                }
+                i += 2;
+            } else {
+                out.write(c);
+            }
+        }
+        return new String(out.toByteArray());
+    }
+
+    /**
+     * Tests whether the specified path represents the root path, i.e. "/".
+     *
+     * @param path path to test
+     * @return true if the specified path represents the root path; false otherwise.
+     */
+    public static boolean denotesRoot(String path) {
+        return path.equals(FileSystem.SEPARATOR);
+    }
+
+    /**
+     * Checks if <code>path</code> is a valid path.
+     *
+     * @param path the path to be checked
+     * @throws FileSystemException If <code>path</code> is not a valid path
+     */
+    public static void checkFormat(String path) throws FileSystemException {
+        if (path == null) {
+            throw new FileSystemException("null path");
+        }
+
+        // path must be absolute, i.e. starting with '/'
+        if (!path.startsWith(FileSystem.SEPARATOR)) {
+            throw new FileSystemException("not an absolute path: " + path);
+        }
+
+        // trailing '/' is not allowed (except for root path)
+        if (path.endsWith(FileSystem.SEPARATOR) && path.length() > 1) {
+            throw new FileSystemException("malformed path: " + path);
+        }
+
+        String[] names = path.split(FileSystem.SEPARATOR);
+        for (int i = 1; i < names.length; i++) {
+            // name must not be empty
+            if (names[i].length() == 0) {
+                throw new FileSystemException("empty name: " + path);
+            }
+            // leading/trailing whitespace is not allowed
+            String trimmed = names[i].trim();
+            if (!trimmed.equals(names[i])) {
+                throw new FileSystemException("illegal leading or trailing whitespace in name: " + path);
+            }
+        }
+    }
+
+    /**
+     * Returns the parent directory of the specified <code>path</code>.
+     *
+     * @param path a file system path denoting a directory or a file.
+     * @return the parent directory.
+     */
+    public static String getParentDir(String path) {
+        int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR);
+        if (pos > 0) {
+            return path.substring(0, pos);
+        }
+        return FileSystem.SEPARATOR;
+    }
+
+    /**
+     * Returns the name of the specified <code>path</code>.
+     *
+     * @param path a file system path denoting a directory or a file.
+     * @return the name.
+     */
+    public static String getName(String path) {
+        int pos = path.lastIndexOf(FileSystem.SEPARATOR_CHAR);
+        if (pos != -1) {
+            return path.substring(pos + 1);
+        }
+        return path;
+    }
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/fs/FileSystemResource.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,226 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.core.fs.FileSystemPathUtil;
+
+/**
+ * A <code>FileSystemResource</code> represents a resource (i.e. file) in a
+ * <code>FileSystem</code>.
+ */
+public class FileSystemResource {
+
+    protected final FileSystem fs;
+
+    protected final String path;
+
+    static {
+        // preload FileSystemPathUtil to prevent classloader issues during shutdown
+        FileSystemPathUtil.class.hashCode();
+    }
+
+    /**
+     * Creates a new <code>FileSystemResource</code>
+     *
+     * @param fs   the <code>FileSystem</code> where the resource is located
+     * @param path the path of the resource in the <code>FileSystem</code>
+     */
+    public FileSystemResource(FileSystem fs, String path) {
+        if (fs == null) {
+            throw new IllegalArgumentException("invalid file system argument");
+        }
+        this.fs = fs;
+
+        if (path == null) {
+            throw new IllegalArgumentException("invalid path argument");
+        }
+        this.path = path;
+    }
+
+    /**
+     * Returns the <code>FileSystem</code> where this resource is located.
+     *
+     * @return the <code>FileSystem</code> where this resource is located.
+     */
+    public FileSystem getFileSystem() {
+        return fs;
+    }
+
+    /**
+     * Returns the path of this resource.
+     *
+     * @return the path of this resource.
+     */
+    public String getPath() {
+        return path;
+    }
+
+    /**
+     * Returns the parent directory of this resource.
+     *
+     * @return the parent directory.
+     */
+    public String getParentDir() {
+        return FileSystemPathUtil.getParentDir(path);
+    }
+
+    /**
+     * Returns the name of this resource.
+     *
+     * @return the name.
+     */
+    public String getName() {
+        return FileSystemPathUtil.getName(path);
+    }
+
+    /**
+     * Creates the parent directory of this resource, including any necessary
+     * but nonexistent parent directories.
+     *
+     * @throws FileSystemException
+     */
+    public synchronized void makeParentDirs() throws FileSystemException {
+        String parentDir = getParentDir();
+        if (!fs.exists(parentDir)) {
+            fs.createFolder(parentDir);
+        }
+    }
+
+    /**
+     * Deletes this resource.
+     * Same as <code>{@link #delete(false)}</code>.
+     *
+     * @see FileSystem#deleteFile
+     */
+    public void delete() throws FileSystemException {
+        delete(false);
+    }
+
+    /**
+     * Deletes this resource.
+     *
+     * @param pruneEmptyParentDirs if <code>true</code>, empty parent folders will
+     *                             automatically be deleted
+     * @see FileSystem#deleteFile
+     */
+    public synchronized void delete(boolean pruneEmptyParentDirs) throws FileSystemException {
+        fs.deleteFile(path);
+        if (pruneEmptyParentDirs) {
+            // prune empty parent folders
+            String parentDir = FileSystemPathUtil.getParentDir(path);
+            while (!parentDir.equals(FileSystem.SEPARATOR)
+                    && fs.exists(parentDir)
+                    && !fs.hasChildren(parentDir)) {
+                fs.deleteFolder(parentDir);
+                parentDir = FileSystemPathUtil.getParentDir(parentDir);
+            }
+        }
+    }
+
+    /**
+     * @see FileSystem#exists
+     */
+    public boolean exists() throws FileSystemException {
+        return fs.exists(path);
+    }
+
+    /**
+     * @see FileSystem#getInputStream
+     */
+    public InputStream getInputStream() throws FileSystemException {
+        return fs.getInputStream(path);
+    }
+
+    /**
+     * Spools this resource to the given output stream.
+     *
+     * @param out output stream where to spool the resource
+     * @throws FileSystemException if the input stream for this resource could
+     *                             not be obtained
+     * @throws IOException         if an error occurs while while spooling
+     * @see FileSystem#getInputStream
+     */
+    public void spool(OutputStream out) throws FileSystemException, IOException {
+        InputStream in = fs.getInputStream(path);
+        try {
+            IOUtils.copy(in, out);
+        } finally {
+            IOUtils.closeQuietly(in);
+        }
+    }
+
+    /**
+     * @see FileSystem#getOutputStream
+     */
+    public OutputStream getOutputStream() throws FileSystemException {
+        return fs.getOutputStream(path);
+    }
+
+    /**
+     * @see FileSystem#lastModified
+     */
+    public long lastModified() throws FileSystemException {
+        return fs.lastModified(path);
+    }
+
+    /**
+     * @see FileSystem#length
+     */
+    public long length() throws FileSystemException {
+        return fs.length(path);
+    }
+
+    //-------------------------------------------< java.lang.Object overrides >
+    /**
+     * Returns the path string of this resource. This is just the
+     * string returned by the <code>{@link #getPath}</code> method.
+     *
+     * @return The path string of this resource
+     */
+    public String toString() {
+        return getPath();
+    }
+
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj instanceof FileSystemResource) {
+            FileSystemResource other = (FileSystemResource) obj;
+            return (path == null ? other.path == null : path.equals(other.path))
+                    && (fs == null ? other.fs == null : fs.equals(other.fs));
+        }
+        return false;
+    }
+
+    /**
+     * 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;
+    }
+
+}