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 [3/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/LocalCache.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/LocalCache.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,535 @@
+/*
+ * 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;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.jackrabbit.core.data.LazyFileInputStream;
+import org.apache.jackrabbit.util.TransientFileFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class implements a LRU cache used by {@link CachingDataStore}. If cache
+ * size exceeds limit, this cache goes in purge mode. In purge mode any
+ * operation to cache is no-op. After purge cache size would be less than
+ * cachePurgeResizeFactor * maximum size.
+ */
+public class LocalCache {
+
+    /**
+     * Logger instance.
+     */
+    static final Logger LOG = LoggerFactory.getLogger(LocalCache.class);
+
+    /**
+     * The file names of the files that need to be deleted.
+     */
+    final Set<String> toBeDeleted = new HashSet<String>();
+
+    /**
+     * The filename Vs file size LRU cache.
+     */
+    LRUCache cache;
+
+    /**
+     * The directory where the files are created.
+     */
+    private final File directory;
+
+    /**
+     * The directory where tmp files are created.
+     */
+    private final File tmp;
+
+    /**
+     * The maximum size of cache in bytes.
+     */
+    private long maxSize;
+
+    /**
+     * If true cache is in purgeMode and not available. All operation would be
+     * no-op.
+     */
+    private volatile boolean purgeMode;
+
+    /**
+     * Build LRU cache of files located at 'path'. It uses lastModified property
+     * of file to build LRU cache. If cache size exceeds limit size, this cache
+     * goes in purge mode. In purge mode any operation to cache is no-op.
+     * 
+     * @param path file system path
+     * @param tmpPath temporary directory used by cache.
+     * @param maxSize maximum size of cache.
+     * @param cachePurgeTrigFactor factor which triggers cache to purge mode.
+     * That is if current size exceed (cachePurgeTrigFactor * maxSize), the
+     * cache will go in auto-purge mode.
+     * @param cachePurgeResizeFactor after cache purge size of cache will be
+     * just less (cachePurgeResizeFactor * maxSize).
+     * @throws RepositoryException
+     */
+    public LocalCache(final String path, final String tmpPath,
+            final long maxSize, final double cachePurgeTrigFactor,
+            final double cachePurgeResizeFactor) throws RepositoryException {
+        this.maxSize = maxSize;
+        directory = new File(path);
+        tmp = new File(tmpPath);
+        cache = new LRUCache(maxSize, cachePurgeTrigFactor,
+            cachePurgeResizeFactor);
+        ArrayList<File> allFiles = new ArrayList<File>();
+
+        Iterator<File> it = FileUtils.iterateFiles(directory, null, true);
+        while (it.hasNext()) {
+            File f = it.next();
+            allFiles.add(f);
+        }
+        Collections.sort(allFiles, new Comparator<File>() {
+            @Override
+            public int compare(final File o1, final File o2) {
+                long l1 = o1.lastModified(), l2 = o2.lastModified();
+                return l1 < l2 ? -1 : l1 > l2 ? 1 : 0;
+            }
+        });
+        String dataStorePath = directory.getAbsolutePath();
+        long time = System.currentTimeMillis();
+        int count = 0;
+        int deletecount = 0;
+        for (File f : allFiles) {
+            if (f.exists()) {
+                long length = f.length();
+                String name = f.getPath();
+                if (name.startsWith(dataStorePath)) {
+                    name = name.substring(dataStorePath.length());
+                }
+                // convert to java path format
+                name = name.replace("\\", "/");
+                if (name.startsWith("/") || name.startsWith("\\")) {
+                    name = name.substring(1);
+                }
+                if ((cache.currentSizeInBytes + length) < cache.maxSizeInBytes) {
+                    count++;
+                    cache.put(name, length);
+                } else {
+                    if (tryDelete(name)) {
+                        deletecount++;
+                    }
+                }
+                long now = System.currentTimeMillis();
+                if (now > time + 5000) {
+                    LOG.info("Processed {" + (count + deletecount) + "}/{"
+                        + allFiles.size() + "}");
+                    time = now;
+                }
+            }
+        }
+        LOG.info("Cached {" + count + "}/{" + allFiles.size()
+            + "} , currentSizeInBytes = " + cache.currentSizeInBytes);
+        LOG.info("Deleted {" + deletecount + "}/{" + allFiles.size()
+            + "} files .");
+    }
+
+    /**
+     * Store an item in the cache and return the input stream. If cache is in
+     * purgeMode or file doesn't exists, inputstream from a
+     * {@link TransientFileFactory#createTransientFile(String, String, File)} is
+     * returned. Otherwise inputStream from cached file is returned. This method
+     * doesn't close the incoming inputstream.
+     * 
+     * @param fileName the key of cache.
+     * @param in the inputstream.
+     * @return the (new) input stream.
+     */
+    public synchronized InputStream store(String fileName, final InputStream in)
+            throws IOException {
+        fileName = fileName.replace("\\", "/");
+        File f = getFile(fileName);
+        long length = 0;
+        if (!f.exists() || isInPurgeMode()) {
+            OutputStream out = null;
+            File transFile = null;
+            try {
+                TransientFileFactory tff = TransientFileFactory.getInstance();
+                transFile = tff.createTransientFile("s3-", "tmp", tmp);
+                out = new BufferedOutputStream(new FileOutputStream(transFile));
+                length = IOUtils.copyLarge(in, out);
+            } finally {
+                IOUtils.closeQuietly(out);
+            }
+            // rename the file to local fs cache
+            if (canAdmitFile(length)
+                && (f.getParentFile().exists() || f.getParentFile().mkdirs())
+                && transFile.renameTo(f) && f.exists()) {
+                if (transFile.exists() && transFile.delete()) {
+                    LOG.warn("tmp file = " + transFile.getAbsolutePath()
+                        + " not deleted successfully");
+                }
+                transFile = null;
+                toBeDeleted.remove(fileName);
+                if (cache.get(fileName) == null) {
+                    cache.put(fileName, f.length());
+                }
+            } else {
+                f = transFile;
+            }
+        } else {
+            // f.exists and not in purge mode
+            f.setLastModified(System.currentTimeMillis());
+            toBeDeleted.remove(fileName);
+            if (cache.get(fileName) == null) {
+                cache.put(fileName, f.length());
+            }
+        }
+        cache.tryPurge();
+        return new LazyFileInputStream(f);
+    }
+
+    /**
+     * Store an item along with file in cache. Cache size is increased by
+     * {@link File#length()} If file already exists in cache,
+     * {@link File#setLastModified(long)} is updated with current time.
+     * 
+     * @param fileName the key of cache.
+     * @param src file to be added to cache.
+     * @throws IOException
+     */
+    public synchronized void store(String fileName, final File src)
+            throws IOException {
+        fileName = fileName.replace("\\", "/");
+        File dest = getFile(fileName);
+        File parent = dest.getParentFile();
+        if (src.exists() && !dest.exists() && !src.equals(dest)
+            && canAdmitFile(src.length())
+            && (parent.exists() || parent.mkdirs()) && (src.renameTo(dest))) {
+            toBeDeleted.remove(fileName);
+            if (cache.get(fileName) == null) {
+                cache.put(fileName, dest.length());
+            }
+
+        } else if (dest.exists()) {
+            dest.setLastModified(System.currentTimeMillis());
+            toBeDeleted.remove(fileName);
+            if (cache.get(fileName) == null) {
+                cache.put(fileName, dest.length());
+            }
+        }
+        cache.tryPurge();
+    }
+
+    /**
+     * Return the inputstream from from cache, or null if not in the cache.
+     * 
+     * @param fileName name of file.
+     * @return  stream or null.
+     */
+    public InputStream getIfStored(String fileName) throws IOException {
+
+        fileName = fileName.replace("\\", "/");
+        File f = getFile(fileName);
+        synchronized (this) {
+            if (!f.exists() || isInPurgeMode()) {
+                log("purgeMode true or file doesn't exists: getIfStored returned");
+                return null;
+            }
+            f.setLastModified(System.currentTimeMillis());
+            return new LazyFileInputStream(f);
+        }
+    }
+
+    /**
+     * Delete file from cache. Size of cache is reduced by file length. The
+     * method is no-op if file doesn't exist in cache.
+     * 
+     * @param fileName file name that need to be removed from cache.
+     */
+    public synchronized void delete(String fileName) {
+        if (isInPurgeMode()) {
+            log("purgeMode true :delete returned");
+            return;
+        }
+        fileName = fileName.replace("\\", "/");
+        cache.remove(fileName);
+    }
+
+    /**
+     * Returns length of file if exists in cache else returns null.
+     * @param fileName name of the file.
+     */
+    public Long getFileLength(String fileName) {
+        fileName = fileName.replace("\\", "/");
+        File f = getFile(fileName);
+        synchronized (this) {
+            if (!f.exists() || isInPurgeMode()) {
+                log("purgeMode true or file doesn't exists: getFileLength returned");
+                return null;
+            }
+            f.setLastModified(System.currentTimeMillis());
+            return f.length();
+        }
+    }
+
+    /**
+     * Close the cache. Cache maintain set of files which it was not able to
+     * delete successfully. This method will an attempt to delete all
+     * unsuccessful delete files.
+     */
+    public void close() {
+        log("close");
+        deleteOldFiles();
+    }
+
+    /**
+     * Check if cache can admit file of given length.
+     * @param length of the file.
+     * @return true if yes else return false.
+     */
+    private synchronized boolean canAdmitFile(final long length) {
+        // order is important here
+        boolean value = !isInPurgeMode() && cache.canAdmitFile(length);
+        if (!value) {
+            log("cannot admit file of length=" + length
+                + " and currentSizeInBytes=" + cache.currentSizeInBytes);
+        }
+        return value;
+    }
+
+    /**
+     * Return true if cache is in purge mode else return false.
+     */
+    synchronized boolean isInPurgeMode() {
+        return purgeMode || maxSize == 0;
+    }
+
+    /**
+     * Set purge mode. If set to true all cache operation will be no-op. If set
+     * to false, all operations to cache are available.
+     * 
+     * @param purgeMode purge mode
+     */
+    synchronized void setPurgeMode(final boolean purgeMode) {
+        this.purgeMode = purgeMode;
+    }
+
+    File getFile(final String fileName) {
+        return new File(directory, fileName);
+    }
+
+    private void deleteOldFiles() {
+        int initialSize = toBeDeleted.size();
+        int count = 0;
+        for (String n : new ArrayList<String>(toBeDeleted)) {
+            if (tryDelete(n)) {
+                count++;
+            }
+        }
+        LOG.info("deleted [" + count + "]/[" + initialSize + "] files");
+    }
+
+    /**
+     * This method tries to delete a file. If it is not able to delete file due
+     * to any reason, it add it toBeDeleted list.
+     * 
+     * @param fileName name of the file which will be deleted.
+     * @return true if this method deletes file successfuly else return false.
+     */
+    boolean tryDelete(final String fileName) {
+        log("cache delete " + fileName);
+        File f = getFile(fileName);
+        if (f.exists() && f.delete()) {
+            log(fileName + "  deleted successfully");
+            toBeDeleted.remove(fileName);
+            while (true) {
+                f = f.getParentFile();
+                if (f.equals(directory) || f.list().length > 0) {
+                    break;
+                }
+                // delete empty parent folders (except the main directory)
+                f.delete();
+            }
+            return true;
+        } else if (f.exists()) {
+            LOG.info("not able to delete file = " + f.getAbsolutePath());
+            toBeDeleted.add(fileName);
+            return false;
+        }
+        return true;
+    }
+
+    static int maxSizeElements(final long bytes) {
+        // after a CQ installation, the average item in
+        // the data store is about 52 KB
+        int count = (int) (bytes / 65535);
+        count = Math.max(1024, count);
+        count = Math.min(64 * 1024, count);
+        return count;
+    }
+
+    static void log(final String s) {
+        LOG.debug(s);
+    }
+
+    /**
+     * A LRU based extension {@link LinkedHashMap}. The key is file name and
+     * value is length of file.
+     */
+    private class LRUCache extends LinkedHashMap<String, Long> {
+        private static final long serialVersionUID = 1L;
+
+        volatile long currentSizeInBytes;
+
+        final long maxSizeInBytes;
+
+        long cachePurgeResize;
+        
+        private long cachePurgeTrigSize;
+
+        public LRUCache(final long maxSizeInBytes,
+                final double cachePurgeTrigFactor,
+                final double cachePurgeResizeFactor) {
+            super(maxSizeElements(maxSizeInBytes), (float) 0.75, true);
+            this.maxSizeInBytes = maxSizeInBytes;
+            this.cachePurgeTrigSize = new Double(cachePurgeTrigFactor
+                * maxSizeInBytes).longValue();
+            this.cachePurgeResize = new Double(cachePurgeResizeFactor
+                * maxSizeInBytes).longValue();
+        }
+
+        /**
+         * Overridden {@link Map#remove(Object)} to delete corresponding file
+         * from file system.
+         */
+        @Override
+        public synchronized Long remove(final Object key) {
+            String fileName = (String) key;
+            fileName = fileName.replace("\\", "/");
+            Long flength = null;
+            if (tryDelete(fileName)) {
+                flength = super.remove(key);
+                if (flength != null) {
+                    log("cache entry { " + fileName + "} with size {" + flength
+                        + "} removed.");
+                    currentSizeInBytes -= flength.longValue();
+                }
+            } else if (!getFile(fileName).exists()) {
+                // second attempt. remove from cache if file doesn't exists
+                flength = super.remove(key);
+                if (flength != null) {
+                    log(" file not exists. cache entry { " + fileName
+                        + "} with size {" + flength + "} removed.");
+                    currentSizeInBytes -= flength.longValue();
+                }
+            }
+            return flength;
+        }
+
+        @Override
+        public synchronized Long put(final String key, final Long value) {
+            long flength = value.longValue();
+            currentSizeInBytes += flength;
+            return super.put(key.replace("\\", "/"), value);
+        }
+
+        /**
+         * This method tries purging of local cache. It checks if local cache
+         * has exceeded the defined limit then it triggers purge cache job in a
+         * seperate thread.
+         */
+        synchronized void tryPurge() {
+            if (currentSizeInBytes > cachePurgeTrigSize && !isInPurgeMode()) {
+                setPurgeMode(true);
+                LOG.info("currentSizeInBytes[" + cache.currentSizeInBytes
+                    + "] exceeds (cachePurgeTrigSize)["
+                    + cache.cachePurgeTrigSize + "]");
+                new Thread(new PurgeJob()).start();
+            }
+        }
+        /**
+         * This method check if cache can admit file of given length. 
+         * @param length length of file.
+         * @return true if cache size + length is less than maxSize.
+         */
+        synchronized boolean canAdmitFile(final long length) {
+            return cache.currentSizeInBytes + length < cache.maxSizeInBytes;
+        }
+    }
+
+    /**
+     * This class performs purging of local cache. It implements
+     * {@link Runnable} and should be invoked in a separate thread.
+     */
+    private class PurgeJob implements Runnable {
+        public PurgeJob() {
+            // TODO Auto-generated constructor stub
+        }
+
+        /**
+         * This method purges local cache till its size is less than
+         * cacheResizefactor * maxSize
+         */
+        @Override
+        public void run() {
+            try {
+                synchronized (cache) {
+                    LOG.info(" cache purge job started");
+                    // first try to delete toBeDeleted files
+                    int initialSize = cache.size();
+                    for (String fileName : new ArrayList<String>(toBeDeleted)) {
+                        cache.remove(fileName);
+                    }
+                    Iterator<Map.Entry<String, Long>> itr = cache.entrySet().iterator();
+                    while (itr.hasNext()) {
+                        Map.Entry<String, Long> entry = itr.next();
+                        if (entry.getKey() != null) {
+                            if (cache.currentSizeInBytes > cache.cachePurgeResize) {
+                                itr.remove();
+
+                            } else {
+                                break;
+                            }
+                        }
+
+                    }
+                    LOG.info(" cache purge job completed: cleaned ["
+                        + (initialSize - cache.size())
+                        + "] files and currentSizeInBytes = [ "
+                        + cache.currentSizeInBytes + "]");
+                }
+            } catch (Exception e) {
+                LOG.error("error in purge jobs:", e);
+            } finally {
+                setPurgeMode(false);
+            }
+        }
+    }
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,722 @@
+/*
+ * 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;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Iterator;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.jcr.RepositoryException;
+
+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.FileSystemResource;
+import org.apache.jackrabbit.core.fs.local.LocalFileSystem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A MultiDataStore can handle two independent DataStores.
+ * <p>
+ * <b>Attention:</b> You will lost the global single instance mechanism !
+ * </p>
+ * It can be used if you have two storage systems. One for fast access and a
+ * other one like a archive DataStore on a slower storage system. All Files will
+ * be added to the primary DataStore. On read operations first the primary
+ * dataStore will be used and if no Record is found the archive DataStore will
+ * be used. The GarabageCollector will only remove files from the archive
+ * DataStore.
+ * <p>
+ * The internal MoveDataTask will be started automatically and could be
+ * configured with the following properties.
+ * <p/>
+ * The Configuration:
+ * 
+ * <pre>
+ * &lt;DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
+ *     &lt;param name="{@link #setMaxAge(int) maxAge}" value="60"/>
+ *     &lt;param name="{@link #setMoveDataTaskSleep(int) moveDataTaskSleep}" value="604800"/>
+ *     &lt;param name="{@link #setMoveDataTaskFirstRunHourOfDay(int) moveDataTaskFirstRunHourOfDay}" value="1"/>
+ *     &lt;param name="{@link #setSleepBetweenRecords(long) sleepBetweenRecords}" value="100"/>
+ *     &lt;param name="{@link #setDelayedDelete(boolean) delayedDelete}" value="false"/>
+ *     &lt;param name="{@link #setDelayedDeleteSleep(long) delayedDeleteSleep}" value="86400"/>
+ *     &lt;param name="primary" value="org.apache.jackrabbit.core.data.db.DbDataStore">
+ *        &lt;param .../>
+ *     &lt;/param>
+ *     &lt;param name="archive" value="org.apache.jackrabbit.core.data.FileDataStore">
+ *        &lt;param .../>
+ *     &lt;/param>
+ * &lt/DataStore>
+ * </pre>
+ * 
+ * <ul>
+ * <li><code>maxAge</code>: defines how many days the content will reside in the
+ * primary data store. DataRecords that have been added before this time span
+ * will be moved to the archive data store. (default = <code>60</code>)</li>
+ * <li><code>moveDataTaskSleep</code>: specifies the sleep time of the
+ * moveDataTaskThread in seconds. (default = 60 * 60 * 24 * 7, which equals 7
+ * days)</li>
+ * <li><code>moveDataTaskNextRunHourOfDay</code>: specifies the hour at which
+ * the moveDataTaskThread initiates its first run (default = <code>1</code>
+ * which means 01:00 at night)</li>
+ * <li><code>sleepBetweenRecords</code>: specifies the delay in milliseconds
+ * between scanning data records (default = <code>100</code>)</li>
+ * <li><code>delayedDelete</code>: its possible to delay the delete operation on
+ * the primary data store. The DataIdentifiers will be written to a temporary
+ * file. The file will be processed after a defined sleep (see
+ * <code>delayedDeleteSleep</code>) It's useful if you like to create a snapshot
+ * of the primary data store backend in the meantime before the data will be
+ * deleted. (default = <code>false</code>)</li>
+ * <li><code>delayedDeleteSleep</code>: specifies the sleep time of the
+ * delayedDeleteTaskThread in seconds. (default = 60 * 60 * 24, which equals 1
+ * day). This means the delayed delete from the primary data store will be
+ * processed after one day.</li>
+ * </ul>
+ */
+public class MultiDataStore implements DataStore {
+
+    /**
+     * Logger instance
+     */
+    private static Logger log = LoggerFactory.getLogger(MultiDataStore.class);
+
+    private DataStore primaryDataStore;
+    private DataStore archiveDataStore;
+
+    /**
+     * Max Age in days.
+     */
+    private int maxAge = 60;
+
+    /**
+     * ReentrantLock that is used while the MoveDataTask is running.
+     */
+    private ReentrantLock moveDataTaskLock = new ReentrantLock();
+    private boolean moveDataTaskRunning = false;
+    private Thread moveDataTaskThread;
+
+    /**
+     * The sleep time in seconds of the MoveDataTask, 7 day default.
+     */
+    private int moveDataTaskSleep = 60 * 60 * 24 * 7;
+
+    /**
+     * Indicates when the next run of the move task is scheduled. The first run
+     * is scheduled by default at 01:00 hours.
+     */
+    private Calendar moveDataTaskNextRun = Calendar.getInstance();
+
+    /**
+     * Its possible to delay the delete operation on the primary data store
+     * while move task is running. The delete will be executed after defined
+     * delayDeleteSleep.
+     */
+    private boolean delayedDelete = false;
+
+    /**
+     * The sleep time in seconds to delay remove operation on the primary data
+     * store, 1 day default.
+     */
+    private long delayedDeleteSleep = 60 * 60 * 24;
+
+    /**
+     * File that holds the data identifiers if delayDelete is enabled.
+     */
+    private FileSystemResource identifiersToDeleteFile = null;
+
+    private Thread deleteDelayedIdentifiersTaskThread;
+
+    /**
+     * Name of the file which holds the identifiers if deleayed delete is
+     * enabled
+     */
+    private final String IDENTIFIERS_TO_DELETE_FILE_KEY = "identifiersToDelete";
+
+    /**
+     * The delay time in milliseconds between scanning data records, 100
+     * default.
+     */
+    private long sleepBetweenRecords = 100;
+
+    {
+        if (moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY) >= 1) {
+            moveDataTaskNextRun.add(Calendar.DAY_OF_MONTH, 1);
+        }
+        moveDataTaskNextRun.set(Calendar.HOUR_OF_DAY, 1);
+        moveDataTaskNextRun.set(Calendar.MINUTE, 0);
+        moveDataTaskNextRun.set(Calendar.SECOND, 0);
+        moveDataTaskNextRun.set(Calendar.MILLISECOND, 0);
+    }
+
+    /**
+     * Setter for the primary dataStore
+     * 
+     * @param dataStore
+     */
+    public void setPrimaryDataStore(DataStore dataStore) {
+        this.primaryDataStore = dataStore;
+    }
+
+    /**
+     * Setter for the archive dataStore
+     * 
+     * @param dataStore
+     */
+    public void setArchiveDataStore(DataStore dataStore) {
+        this.archiveDataStore = dataStore;
+    }
+
+    /**
+     * Check if a record for the given identifier exists in the primary data
+     * store. If not found there it will be returned from the archive data
+     * store. If no record exists, this method returns null.
+     * 
+     * @param identifier
+     *            data identifier
+     * @return the record if found, and null if not
+     */
+    public DataRecord getRecordIfStored(DataIdentifier identifier) throws DataStoreException {
+        if (moveDataTaskRunning) {
+            moveDataTaskLock.lock();
+        }
+        try {
+            DataRecord dataRecord = primaryDataStore.getRecordIfStored(identifier);
+            if (dataRecord == null) {
+                dataRecord = archiveDataStore.getRecordIfStored(identifier);
+            }
+            return dataRecord;
+        } finally {
+            if (moveDataTaskRunning) {
+                moveDataTaskLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Returns the identified data record from the primary data store. If not
+     * found there it will be returned from the archive data store. The given
+     * identifier should be the identifier of a previously saved data record.
+     * Since records are never removed, there should never be cases where the
+     * identified record is not found. Abnormal cases like that are treated as
+     * errors and handled by throwing an exception.
+     * 
+     * @param identifier
+     *            data identifier
+     * @return identified data record
+     * @throws DataStoreException
+     *             if the data store could not be accessed, or if the given
+     *             identifier is invalid
+     */
+    public DataRecord getRecord(DataIdentifier identifier) throws DataStoreException {
+        if (moveDataTaskRunning) {
+            moveDataTaskLock.lock();
+        }
+        try {
+            return primaryDataStore.getRecord(identifier);
+        } catch (DataStoreException e) {
+            return archiveDataStore.getRecord(identifier);
+        } finally {
+            if (moveDataTaskRunning) {
+                moveDataTaskLock.unlock();
+            }
+        }
+    }
+
+    /**
+     * Creates a new data record in the primary data store. The given binary
+     * stream is consumed and a binary record containing the consumed stream is
+     * created and returned. If the same stream already exists in another
+     * record, then that record is returned instead of creating a new one.
+     * <p>
+     * The given stream is consumed and <strong>not closed</strong> by this
+     * method. It is the responsibility of the caller to close the stream. A
+     * typical call pattern would be:
+     * 
+     * <pre>
+     *     InputStream stream = ...;
+     *     try {
+     *         record = store.addRecord(stream);
+     *     } finally {
+     *         stream.close();
+     *     }
+     * </pre>
+     * 
+     * @param stream
+     *            binary stream
+     * @return data record that contains the given stream
+     * @throws DataStoreException
+     *             if the data store could not be accessed
+     */
+    public DataRecord addRecord(InputStream stream) throws DataStoreException {
+        return primaryDataStore.addRecord(stream);
+    }
+
+    /**
+     * From now on, update the modified date of an object even when accessing it
+     * in the archive data store. Usually, the modified date is only updated
+     * when creating a new object, or when a new link is added to an existing
+     * object. When this setting is enabled, even getLength() will update the
+     * modified date.
+     * 
+     * @param before
+     *            - update the modified date to the current time if it is older
+     *            than this value
+     */
+    public void updateModifiedDateOnAccess(long before) {
+        archiveDataStore.updateModifiedDateOnAccess(before);
+    }
+
+    /**
+     * Delete objects that have a modified date older than the specified date
+     * from the archive data store.
+     * 
+     * @param min
+     *            the minimum time
+     * @return the number of data records deleted
+     * @throws DataStoreException
+     */
+    public int deleteAllOlderThan(long min) throws DataStoreException {
+        return archiveDataStore.deleteAllOlderThan(min);
+    }
+
+    /**
+     * Get all identifiers from the archive data store.
+     * 
+     * @return an iterator over all DataIdentifier objects
+     * @throws DataStoreException
+     *             if the list could not be read
+     */
+    public Iterator<DataIdentifier> getAllIdentifiers() throws DataStoreException {
+        return archiveDataStore.getAllIdentifiers();
+    }
+
+    public DataRecord getRecordFromReference(String reference)
+            throws DataStoreException {
+        DataRecord record = primaryDataStore.getRecordFromReference(reference);
+        if (record == null) {
+            record = archiveDataStore.getRecordFromReference(reference);
+        }
+        return record;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void init(String homeDir) throws RepositoryException {
+        if (delayedDelete) {
+            // First initialize the identifiersToDeleteFile
+            LocalFileSystem fileSystem = new LocalFileSystem();
+            fileSystem.setRoot(new File(homeDir));
+            identifiersToDeleteFile = new FileSystemResource(fileSystem, FileSystem.SEPARATOR
+                    + IDENTIFIERS_TO_DELETE_FILE_KEY);
+        }
+        moveDataTaskThread = new Thread(new MoveDataTask(),
+                "Jackrabbit-MulitDataStore-MoveDataTaskThread");
+        moveDataTaskThread.setDaemon(true);
+        moveDataTaskThread.start();
+        log.info("MultiDataStore-MoveDataTask thread started; first run scheduled at "
+                + moveDataTaskNextRun.getTime());
+        if (delayedDelete) {
+            try {
+                // Run on startup the DeleteDelayedIdentifiersTask only if the
+                // file exists and modify date is older than the
+                // delayedDeleteSleep timeout ...
+                if (identifiersToDeleteFile != null
+                        && identifiersToDeleteFile.exists()
+                        && (identifiersToDeleteFile.lastModified() + (delayedDeleteSleep * 1000)) < System
+                                .currentTimeMillis()) {
+                    deleteDelayedIdentifiersTaskThread = new Thread(
+                            //Start immediately ...
+                            new DeleteDelayedIdentifiersTask(0L),
+                            "Jackrabbit-MultiDataStore-DeleteDelayedIdentifiersTaskThread");
+                    deleteDelayedIdentifiersTaskThread.setDaemon(true);
+                    deleteDelayedIdentifiersTaskThread.start();
+                    log.info("Old entries in the " + IDENTIFIERS_TO_DELETE_FILE_KEY
+                            + " File found. DeleteDelayedIdentifiersTask-Thread started now.");
+                }
+            } catch (FileSystemException e) {
+                throw new RepositoryException("I/O error while reading from '"
+                        + identifiersToDeleteFile.getPath() + "'", e);
+            }
+        }
+    }
+
+    /**
+     * Get the minimum size of an object that should be stored in the primary
+     * data store.
+     * 
+     * @return the minimum size in bytes
+     */
+    public int getMinRecordLength() {
+        return primaryDataStore.getMinRecordLength();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void close() throws DataStoreException {
+        DataStoreException lastException = null;
+        // 1. close the primary data store
+        try {
+            primaryDataStore.close();
+        } catch (DataStoreException e) {
+            lastException = e;
+        }
+        // 2. close the archive data store
+        try {
+            archiveDataStore.close();
+        } catch (DataStoreException e) {
+            if (lastException != null) {
+                lastException = new DataStoreException(lastException);
+            }
+        }
+        // 3. if moveDataTaskThread is running interrupt it
+        try {
+            if (moveDataTaskRunning) {
+                moveDataTaskThread.interrupt();
+            }
+        } catch (Exception e) {
+            if (lastException != null) {
+                lastException = new DataStoreException(lastException);
+            }
+        }
+        // 4. if deleteDelayedIdentifiersTaskThread is running interrupt it
+        try {
+            if (deleteDelayedIdentifiersTaskThread != null
+                    && deleteDelayedIdentifiersTaskThread.isAlive()) {
+                deleteDelayedIdentifiersTaskThread.interrupt();
+            }
+        } catch (Exception e) {
+            if (lastException != null) {
+                lastException = new DataStoreException(lastException);
+            }
+        }
+        if (lastException != null) {
+            throw lastException;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void clearInUse() {
+        archiveDataStore.clearInUse();
+    }
+
+    public int getMaxAge() {
+        return maxAge;
+    }
+
+    public void setMaxAge(int maxAge) {
+        this.maxAge = maxAge;
+    }
+
+    public int getMoveDataTaskSleep() {
+        return moveDataTaskSleep;
+    }
+
+    public int getMoveDataTaskFirstRunHourOfDay() {
+        return moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY);
+    }
+
+    public void setMoveDataTaskSleep(int sleep) {
+        this.moveDataTaskSleep = sleep;
+    }
+
+    public void setMoveDataTaskFirstRunHourOfDay(int hourOfDay) {
+        moveDataTaskNextRun = Calendar.getInstance();
+        if (moveDataTaskNextRun.get(Calendar.HOUR_OF_DAY) >= hourOfDay) {
+            moveDataTaskNextRun.add(Calendar.DAY_OF_MONTH, 1);
+        }
+        moveDataTaskNextRun.set(Calendar.HOUR_OF_DAY, hourOfDay);
+        moveDataTaskNextRun.set(Calendar.MINUTE, 0);
+        moveDataTaskNextRun.set(Calendar.SECOND, 0);
+        moveDataTaskNextRun.set(Calendar.MILLISECOND, 0);
+    }
+
+    public void setSleepBetweenRecords(long millis) {
+        this.sleepBetweenRecords = millis;
+    }
+
+    public long getSleepBetweenRecords() {
+        return sleepBetweenRecords;
+    }
+
+    public boolean isDelayedDelete() {
+        return delayedDelete;
+    }
+
+    public void setDelayedDelete(boolean delayedDelete) {
+        this.delayedDelete = delayedDelete;
+    }
+
+    public long getDelayedDeleteSleep() {
+        return delayedDeleteSleep;
+    }
+
+    public void setDelayedDeleteSleep(long delayedDeleteSleep) {
+        this.delayedDeleteSleep = delayedDeleteSleep;
+    }
+
+    /**
+     * Writes the given DataIdentifier to the delayedDeletedFile.
+     * 
+     * @param identifier
+     * @return boolean true if it was successful otherwise false
+     */
+    private boolean writeDelayedDataIdentifier(DataIdentifier identifier) {
+        BufferedWriter writer = null;
+        try {
+            File identifierFile = new File(
+                    ((LocalFileSystem) identifiersToDeleteFile.getFileSystem()).getPath(),
+                    identifiersToDeleteFile.getPath());
+            writer = new BufferedWriter(new FileWriter(identifierFile, true));
+            writer.write(identifier.toString());
+            return true;
+        } catch (Exception e) {
+            log.warn("I/O error while saving DataIdentifier (stacktrace on DEBUG log level) to '"
+                    + identifiersToDeleteFile.getPath() + "': " + e.getMessage());
+            log.debug("Root cause: ", e);
+            return false;
+        } finally {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    /**
+     * Purges the delayedDeletedFile.
+     * 
+     * @return boolean true if it was successful otherwise false
+     */
+    private boolean purgeDelayedDeleteFile() {
+        BufferedWriter writer = null;
+        try {
+            writer = new BufferedWriter(new OutputStreamWriter(
+                    identifiersToDeleteFile.getOutputStream()));
+            writer.write("");
+            return true;
+        } catch (Exception e) {
+            log.warn("I/O error while purging (stacktrace on DEBUG log level) the "
+                    + IDENTIFIERS_TO_DELETE_FILE_KEY + " file '"
+                    + identifiersToDeleteFile.getPath() + "': " + e.getMessage());
+            log.debug("Root cause: ", e);
+            return false;
+        } finally {
+            IOUtils.closeQuietly(writer);
+        }
+    }
+
+    /**
+     * Class for maintaining the MultiDataStore. It will be used to move the
+     * content of the primary data store to the archive data store.
+     */
+    public class MoveDataTask implements Runnable {
+
+        /**
+         * {@inheritDoc}
+         */
+        public void run() {
+            while (!Thread.currentThread().isInterrupted()) {
+                try {
+                    log.info("Next move-data task run scheduled at "
+                            + moveDataTaskNextRun.getTime());
+                    long sleepTime = moveDataTaskNextRun.getTimeInMillis()
+                            - System.currentTimeMillis();
+                    if (sleepTime > 0) {
+                        Thread.sleep(sleepTime);
+                    }
+                    moveDataTaskRunning = true;
+                    moveOutdatedData();
+                    moveDataTaskRunning = false;
+                    moveDataTaskNextRun.add(Calendar.SECOND, moveDataTaskSleep);
+                    if (delayedDelete) {
+                        if (deleteDelayedIdentifiersTaskThread != null
+                                && deleteDelayedIdentifiersTaskThread.isAlive()) {
+                            log.warn("The DeleteDelayedIdentifiersTask-Thread is already running.");
+                        } else {
+                            deleteDelayedIdentifiersTaskThread = new Thread(
+                                    new DeleteDelayedIdentifiersTask(delayedDeleteSleep),
+                                    "Jackrabbit-MultiDataStore-DeleteDelayedIdentifiersTaskThread");
+                            deleteDelayedIdentifiersTaskThread.setDaemon(true);
+                            deleteDelayedIdentifiersTaskThread.start();
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+            }
+            log.warn("Interrupted: stopping move-data task.");
+        }
+
+        /**
+         * Moves outdated data from primary to archive data store
+         */
+        protected void moveOutdatedData() {
+            try {
+                long now = System.currentTimeMillis();
+                long maxAgeMilli = 1000L * 60 * 60 * 24 * maxAge;
+                log.debug("Collecting all Identifiers from PrimaryDataStore...");
+                Iterator<DataIdentifier> allIdentifiers = primaryDataStore.getAllIdentifiers();
+                int moved = 0;
+                while (allIdentifiers.hasNext()) {
+                    DataIdentifier identifier = allIdentifiers.next();
+                    DataRecord dataRecord = primaryDataStore.getRecord(identifier);
+                    if ((dataRecord.getLastModified() + maxAgeMilli) < now) {
+                        try {
+                            moveDataTaskLock.lock();
+                            if (delayedDelete) {
+                                // first write it to the file and then add it to
+                                // the archive data store ...
+                                if (writeDelayedDataIdentifier(identifier)) {
+                                    archiveDataStore.addRecord(dataRecord.getStream());
+                                    moved++;
+                                }
+                            } else {
+                                // first add it and then delete it .. not really
+                                // atomic ...
+                                archiveDataStore.addRecord(dataRecord.getStream());
+                                ((MultiDataStoreAware) primaryDataStore).deleteRecord(identifier);
+                                moved++;
+                            }
+                            if (moved % 100 == 0) {
+                                log.debug("Moving DataRecord's... ({})", moved);
+                            }
+                        } catch (DataStoreException e) {
+                            log.error("Failed to move DataRecord. DataIdentifier: " + identifier, e);
+                        } finally {
+                            moveDataTaskLock.unlock();
+                        }
+                    }
+                    // Give other threads time to use the MultiDataStore while
+                    // MoveDataTask is running..
+                    Thread.sleep(sleepBetweenRecords);
+                }
+                if (delayedDelete) {
+                    log.info("Moved "
+                            + moved
+                            + " DataRecords to the archive data store. The DataRecords in the primary data store will be removed in "
+                            + delayedDeleteSleep + " seconds.");
+                } else {
+                    log.info("Moved " + moved + " DataRecords to the archive data store.");
+                }
+            } catch (Exception e) {
+                log.warn("Failed to run move-data task.", e);
+            }
+        }
+    }
+
+    /**
+     * Class to clean up the delayed DataRecords from the primary data store.
+     */
+    public class DeleteDelayedIdentifiersTask implements Runnable {
+
+        boolean run = true;
+        private long sleepTime = 0L;
+        
+        /**
+         * Constructor
+         * @param sleep how long this DeleteDelayedIdentifiersTask should sleep in seconds.
+         */
+        public DeleteDelayedIdentifiersTask(long sleep) {
+            this.sleepTime = (sleep * 1000L);
+        }
+
+        @Override
+        public void run() {
+            if (moveDataTaskRunning) {
+                log.warn("It's not supported to run the DeleteDelayedIdentifiersTask while the MoveDataTask is running.");
+                return;
+            }
+            while (run && !Thread.currentThread().isInterrupted()) {
+                if (sleepTime > 0) {
+                    try {
+                        Thread.sleep(sleepTime);
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                    }
+                }
+                log.info("Start to delete DataRecords from the primary data store.");
+                BufferedReader reader = null;
+                ArrayList<DataIdentifier> problemIdentifiers = new ArrayList<DataIdentifier>();
+                try {
+                    int deleted = 0;
+                    reader = new BufferedReader(new InputStreamReader(
+                            identifiersToDeleteFile.getInputStream()));
+                    while (true) {
+                        String s = reader.readLine();
+                        if (s == null || s.equals("")) {
+                            break;
+                        }
+                        DataIdentifier identifier = new DataIdentifier(s);
+                        try {
+                            moveDataTaskLock.lock();
+                            ((MultiDataStoreAware) primaryDataStore).deleteRecord(identifier);
+                            deleted++;
+                        } catch (DataStoreException e) {
+                            log.error("Failed to delete DataRecord. DataIdentifier: " + identifier,
+                                    e);
+                            problemIdentifiers.add(identifier);
+                        } finally {
+                            moveDataTaskLock.unlock();
+                        }
+                        // Give other threads time to use the MultiDataStore
+                        // while
+                        // DeleteDelayedIdentifiersTask is running..
+                        Thread.sleep(sleepBetweenRecords);
+                    }
+                    log.info("Deleted " + deleted + " DataRecords from the primary data store.");
+                    if (problemIdentifiers.isEmpty()) {
+                        try {
+                            identifiersToDeleteFile.delete();
+                        } catch (FileSystemException e) {
+                            log.warn("Unable to delete the " + IDENTIFIERS_TO_DELETE_FILE_KEY
+                                    + " File.");
+                            if (!purgeDelayedDeleteFile()) {
+                                log.error("Unable to purge the " + IDENTIFIERS_TO_DELETE_FILE_KEY
+                                        + " File.");
+                            }
+                        }
+                    } else {
+                        if (purgeDelayedDeleteFile()) {
+                            for (int x = 0; x < problemIdentifiers.size(); x++) {
+                                writeDelayedDataIdentifier(problemIdentifiers.get(x));
+                            }
+                        }
+                    }
+                } catch (InterruptedException e) {
+                    log.warn("Interrupted: stopping delayed-delete task.");
+                    Thread.currentThread().interrupt();
+                } catch (Exception e) {
+                    log.warn("Failed to run delayed-delete task.", e);
+                } finally {
+                    IOUtils.closeQuietly(reader);
+                    run = false;
+                }
+            }
+        }
+    }
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,38 @@
+/*
+ * 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;
+
+/**
+ * To use a DataStore within a MultiDataStore it must implement this
+ * MultiDataStoreAware Interface. It extends a DataStore to delete a single
+ * DataRecord.
+ */
+public interface MultiDataStoreAware {
+
+    /**
+     * Deletes a single DataRecord based on the given identifier. Delete will
+     * only be used by the {@link MoveDataTask}.
+     * 
+     * @param identifier
+     *            data identifier
+     * @throws DataStoreException
+     *             if the data store could not be accessed, or if the given
+     *             identifier is invalid
+     */
+    void deleteRecord(DataIdentifier identifier) throws DataStoreException;
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/ScanEventListener.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,26 @@
+/*
+ * 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;
+
+import org.apache.jackrabbit.api.management.MarkEventListener;
+
+/**
+ * The listener interface for receiving garbage collection scan events.
+ */
+public interface ScanEventListener extends MarkEventListener {
+
+}

Added: jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java
URL: http://svn.apache.org/viewvc/jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java?rev=1564687&view=auto
==============================================================================
--- jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java (added)
+++ jackrabbit/trunk/jackrabbit-data/src/main/java/org/apache/jackrabbit/core/data/db/DbDataRecord.java Wed Feb  5 09:27:20 2014
@@ -0,0 +1,71 @@
+/*
+ * 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.jackrabbit.core.data.AbstractDataRecord;
+import org.apache.jackrabbit.core.data.DataIdentifier;
+import org.apache.jackrabbit.core.data.DataStoreException;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+
+/**
+ * Data record that is stored in a database
+ */
+public class DbDataRecord extends AbstractDataRecord {
+
+    protected final DbDataStore store;
+    protected final long length;
+    protected long lastModified;
+
+    /**
+     * Creates a data record based on the given identifier and length.
+     *
+     * @param identifier data identifier
+     * @param length the length
+     * @param lastModified
+     */
+    public DbDataRecord(DbDataStore store, DataIdentifier identifier, long length, long lastModified) {
+        super(store, identifier);
+        this.store = store;
+        this.length = length;
+        this.lastModified = lastModified;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public long getLength() throws DataStoreException {
+        lastModified = store.touch(getIdentifier(), lastModified);
+        return length;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public InputStream getStream() throws DataStoreException {
+        lastModified = store.touch(getIdentifier(), lastModified);
+        return new BufferedInputStream(new DbInputStream(store, getIdentifier()));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public long getLastModified() {
+        return lastModified;
+    }
+}