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>
+ * <DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
+ * <param name="{@link #setMaxAge(int) maxAge}" value="60"/>
+ * <param name="{@link #setMoveDataTaskSleep(int) moveDataTaskSleep}" value="604800"/>
+ * <param name="{@link #setMoveDataTaskFirstRunHourOfDay(int) moveDataTaskFirstRunHourOfDay}" value="1"/>
+ * <param name="{@link #setSleepBetweenRecords(long) sleepBetweenRecords}" value="100"/>
+ * <param name="{@link #setDelayedDelete(boolean) delayedDelete}" value="false"/>
+ * <param name="{@link #setDelayedDeleteSleep(long) delayedDeleteSleep}" value="86400"/>
+ * <param name="primary" value="org.apache.jackrabbit.core.data.db.DbDataStore">
+ * <param .../>
+ * </param>
+ * <param name="archive" value="org.apache.jackrabbit.core.data.FileDataStore">
+ * <param .../>
+ * </param>
+ * </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;
+ }
+}