### Eclipse Workspace Patch 1.0 #P jackrabbit-core Index: src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java (revision 1393938) +++ src/main/java/org/apache/jackrabbit/core/data/db/DbDataStore.java (working copy) @@ -21,6 +21,7 @@ import org.apache.jackrabbit.core.data.DataRecord; import org.apache.jackrabbit.core.data.DataStore; import org.apache.jackrabbit.core.data.DataStoreException; +import org.apache.jackrabbit.core.data.MultiDataStoreAware; import org.apache.jackrabbit.core.util.db.CheckSchemaOperation; import org.apache.jackrabbit.core.util.db.ConnectionFactory; import org.apache.jackrabbit.core.util.db.ConnectionHelper; @@ -97,7 +98,7 @@ * The tablePrefix can be used to specify a schema and / or catalog name: * <param name="tablePrefix" value="ds."> */ -public class DbDataStore implements DataStore, DatabaseAware { +public class DbDataStore implements DataStore, DatabaseAware, MultiDataStoreAware { /** * The default value for the minimum object size. @@ -470,6 +471,14 @@ return temp; } + public synchronized void deleteRecord(DataIdentifier identifier) throws DataStoreException { + try { + conHelper.exec(deleteSQL, identifier.toString()); + } catch (Exception e) { + throw convert("Can not delete record", e); + } + } + public synchronized int deleteAllOlderThan(long min) throws DataStoreException { try { ArrayList touch = new ArrayList(); Index: src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java (revision 1241895) +++ src/main/java/org/apache/jackrabbit/core/data/FileDataStore.java (working copy) @@ -59,7 +59,7 @@ * This implementation relies on the underlying file system to support * atomic O(1) move operations with {@link File#renameTo(File)}. */ -public class FileDataStore implements DataStore { +public class FileDataStore implements DataStore, MultiDataStoreAware { /** * Logger instance @@ -292,6 +292,18 @@ minModifiedDate = before; } + public void deleteRecord(DataIdentifier identifier) + throws DataStoreException { + File file = getFile(identifier); + synchronized (this) { + if (file.exists()) { + if (!file.delete()) { + log.warn("Failed to delete file " + file.getAbsolutePath()); + } + } + } + } + public int deleteAllOlderThan(long min) { return deleteOlderRecursive(directory, min); } @@ -455,5 +467,4 @@ } } } - } Index: src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java (revision 1383976) +++ src/main/java/org/apache/jackrabbit/core/config/RepositoryConfigurationParser.java (working copy) @@ -20,6 +20,8 @@ import org.apache.jackrabbit.core.cluster.ClusterNode; import org.apache.jackrabbit.core.data.DataStore; import org.apache.jackrabbit.core.data.DataStoreFactory; +import org.apache.jackrabbit.core.data.MultiDataStore; +import org.apache.jackrabbit.core.data.MultiDataStoreAware; import org.apache.jackrabbit.core.fs.FileSystem; import org.apache.jackrabbit.core.fs.FileSystemException; import org.apache.jackrabbit.core.fs.FileSystemFactory; @@ -38,9 +40,15 @@ import org.apache.jackrabbit.core.util.RepositoryLockMechanismFactory; import org.apache.jackrabbit.core.util.db.ConnectionFactory; import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver; +import org.w3c.dom.Attr; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.w3c.dom.TypeInfo; +import org.w3c.dom.UserDataHandler; import org.xml.sax.InputSource; import java.io.File; @@ -180,6 +188,12 @@ /** Name of the clustered configuration attribute. */ public static final String CLUSTERED_ATTRIBUTE = "clustered"; + /** Name of the primary DataStore class attribute. */ + public static final String PRIMARY_DATASTORE_ATTRIBUTE = "primary"; + + /** Name of the archive DataStore class attribute. */ + public static final String ARCHIVE_DATASTORE_ATTRIBUTE = "archive"; + /** Default synchronization delay, in milliseconds. */ public static final String DEFAULT_SYNC_DELAY = "5000"; @@ -973,6 +987,19 @@ * ... * </DataStore> * + * Its also possible to configure a multi data store. The configuration uses following format: + *
+     *   <DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
+     *     <param name="primary" value="org.apache.jackrabbit.core.data.db.XXDataStore">
+     *         <param name="..." value="...">
+     *         ...
+     *     </param>
+     *     <param name="archive" value="org.apache.jackrabbit.core.data.db.XXDataStore">
+     *         <param name="..." value="...">
+     *         ...
+     *     </param>
+     *   </DataStore>
+     * 
*

* DataStore is a {@link #parseBeanConfig(Element,String) bean configuration} * element. @@ -992,9 +1019,50 @@ Node child = children.item(i); if (child.getNodeType() == Node.ELEMENT_NODE && DATA_STORE_ELEMENT.equals(child.getNodeName())) { - BeanConfig bc = - parseBeanConfig(parent, DATA_STORE_ELEMENT); + BeanConfig bc = + parseBeanConfig(parent, DATA_STORE_ELEMENT); + bc.setValidate(false); DataStore store = bc.newInstance(DataStore.class); + if (store instanceof MultiDataStore) { + DataStore primary = null; + DataStore archive = null; + NodeList subParamNodes = child.getChildNodes(); + for (int x = 0; x < subParamNodes.getLength(); x++) { + Node paramNode = subParamNodes.item(x); + if (paramNode.getNodeType() == Node.ELEMENT_NODE + && (PRIMARY_DATASTORE_ATTRIBUTE.equals(paramNode.getAttributes().getNamedItem("name").getNodeValue()) + || ARCHIVE_DATASTORE_ATTRIBUTE.equals(paramNode.getAttributes().getNamedItem("name").getNodeValue()))) { + final ElementImpl datastoreElement = new ElementImpl(DATA_STORE_ELEMENT, Node.ELEMENT_NODE, paramNode.getAttributes(), paramNode.getChildNodes()); + ElementImpl parent = new ElementImpl("parent", Node.ELEMENT_NODE, null, new NodeList() { + + @Override + public Node item(int index) { + return datastoreElement; + } + + @Override + public int getLength() { + return 1; + } + }); + DataStore subDataStore = getDataStoreFactory(parent, directory).getDataStore(); + if (!MultiDataStoreAware.class.isAssignableFrom(subDataStore.getClass())) { + throw new ConfigurationException("Only MultiDataStoreAware datastore's can be used within a MultiDataStore."); + } + String type = getAttribute((Element) paramNode, NAME_ATTRIBUTE); + if (PRIMARY_DATASTORE_ATTRIBUTE.equals(type)) { + primary = subDataStore; + } else if (ARCHIVE_DATASTORE_ATTRIBUTE.equals(type)) { + archive = subDataStore; + } + } + } + if (primary == null || archive == null) { + throw new ConfigurationException("A MultiDataStore must have configured a primary and archive datastore"); + } + ((MultiDataStore) store).setPrimaryDataStore(primary); + ((MultiDataStore) store).setArchiveDataStore(archive); + } store.init(directory); return store; } @@ -1100,4 +1168,305 @@ public void setConfigVisitor(BeanConfigVisitor configVisitor) { this.configVisitor = configVisitor; } + + private class ElementImpl implements org.w3c.dom.Element { + + private String nodeName; + private short nodeType; + private NodeList childNodes; + private NamedNodeMap params; + + public ElementImpl(String nodeName, short nodeType, NamedNodeMap params, NodeList nodeList) { + this.nodeName = nodeName; + this.nodeType = nodeType; + this.childNodes = nodeList; + this.params = params; + } + + @Override + public Node appendChild(Node newChild) throws DOMException { + return null; + } + + @Override + public Node cloneNode(boolean deep) { + return null; + } + + @Override + public short compareDocumentPosition(Node other) throws DOMException { + return 0; + } + + @Override + public NamedNodeMap getAttributes() { + return null; + } + + @Override + public String getBaseURI() { + return null; + } + + @Override + public NodeList getChildNodes() { + return childNodes; + } + + @Override + public Object getFeature(String feature, String version) { + return null; + } + + @Override + public Node getFirstChild() { + return null; + } + + @Override + public Node getLastChild() { + return null; + } + + @Override + public String getLocalName() { + return null; + } + + @Override + public String getNamespaceURI() { + return null; + } + + @Override + public Node getNextSibling() { + return null; + } + + @Override + public String getNodeName() { + return nodeName; + } + + @Override + public short getNodeType() { + return nodeType; + } + + @Override + public String getNodeValue() throws DOMException { + return null; + } + + @Override + public Document getOwnerDocument() { + return null; + } + + @Override + public Node getParentNode() { + return null; + } + + @Override + public String getPrefix() { + return null; + } + + @Override + public Node getPreviousSibling() { + return null; + } + + @Override + public String getTextContent() throws DOMException { + return null; + } + + @Override + public Object getUserData(String key) { + return null; + } + + @Override + public boolean hasAttributes() { + return false; + } + + @Override + public boolean hasChildNodes() { + return false; + } + + @Override + public Node insertBefore(Node newChild, Node refChild) + throws DOMException { + return null; + } + + @Override + public boolean isDefaultNamespace(String namespaceURI) { + return false; + } + + @Override + public boolean isEqualNode(Node arg) { + return false; + } + + @Override + public boolean isSameNode(Node other) { + return false; + } + + @Override + public boolean isSupported(String feature, String version) { + return false; + } + + @Override + public String lookupNamespaceURI(String prefix) { + return null; + } + + @Override + public String lookupPrefix(String namespaceURI) { + return null; + } + + @Override + public void normalize() { + } + + @Override + public Node removeChild(Node oldChild) throws DOMException { + return null; + } + + @Override + public Node replaceChild(Node newChild, Node oldChild) + throws DOMException { + return null; + } + + @Override + public void setNodeValue(String nodeValue) throws DOMException { + } + + @Override + public void setPrefix(String prefix) throws DOMException { + } + + @Override + public void setTextContent(String textContent) throws DOMException { + } + + @Override + public Object setUserData(String key, Object data, + UserDataHandler handler) { + return null; + } + + @Override + public String getAttribute(String name) { + return null; + } + + @Override + public String getAttributeNS(String namespaceURI, String localName) + throws DOMException { + return null; + } + + @Override + public Attr getAttributeNode(String name) { + return (Attr) params.getNamedItem(VALUE_ATTRIBUTE); + } + + @Override + public Attr getAttributeNodeNS(String namespaceURI, String localName) + throws DOMException { + return null; + } + + @Override + public NodeList getElementsByTagName(String name) { + return null; + } + + @Override + public NodeList getElementsByTagNameNS(String namespaceURI, + String localName) throws DOMException { + return null; + } + + @Override + public TypeInfo getSchemaTypeInfo() { + return null; + } + + @Override + public String getTagName() { + return null; + } + + @Override + public boolean hasAttribute(String name) { + return false; + } + + @Override + public boolean hasAttributeNS(String namespaceURI, String localName) + throws DOMException { + return false; + } + + @Override + public void removeAttribute(String name) throws DOMException { + } + + @Override + public void removeAttributeNS(String namespaceURI, String localName) + throws DOMException { + } + + @Override + public Attr removeAttributeNode(Attr oldAttr) throws DOMException { + return null; + } + + @Override + public void setAttribute(String name, String value) throws DOMException { + } + + @Override + public void setAttributeNS(String namespaceURI, String qualifiedName, + String value) throws DOMException { + } + + @Override + public Attr setAttributeNode(Attr newAttr) throws DOMException { + return null; + } + + @Override + public Attr setAttributeNodeNS(Attr newAttr) throws DOMException { + return null; + } + + @Override + public void setIdAttribute(String name, boolean isId) + throws DOMException { + } + + @Override + public void setIdAttributeNS(String namespaceURI, String localName, + boolean isId) throws DOMException { + } + + @Override + public void setIdAttributeNode(Attr idAttr, boolean isId) + throws DOMException { + } + } } Index: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd =================================================================== --- src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd (revision 0) +++ src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd (working copy) @@ -0,0 +1,64 @@ + + + +%repository-elements; + + + Index: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd =================================================================== --- src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd (revision 0) +++ src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd (working copy) Property changes on: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6.dtd ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd =================================================================== --- src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd (revision 0) +++ src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd (working copy) @@ -0,0 +1,254 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Index: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd =================================================================== --- src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd (revision 0) +++ src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd (working copy) Property changes on: src/main/resources/org/apache/jackrabbit/core/config/repository-2.6-elements.dtd ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java (revision 0) +++ src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java (working copy) @@ -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; + +import org.apache.jackrabbit.core.data.MultiDataStore.MultiDataStoreJanitor; + +/** + * 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 MultiDataStoreJanitor}. + * + * @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; + +} Index: src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java (revision 0) +++ src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java (working copy) Property changes on: src/main/java/org/apache/jackrabbit/core/data/MultiDataStoreAware.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java (revision 0) +++ src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java (working copy) @@ -0,0 +1,606 @@ +/* + * 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.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. + *

+ * Attention: You will lost the global single instance mechanism ! + *

+ * 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. To split up the data between the two DataStores use the + * MultiDataStoreJanitor. The GarabageCollector will only remove files from the + * archive DataStore. + *

+ * The internal MultiDataStoreJanitor will be started automatically and could be + * configured with the following properties. + *

+ * The Configuration: + * + *

+ * <DataStore class="org.apache.jackrabbit.core.data.MultiDataStore">
+ *     <param name="{@link #setMaxAge(int) maxAge}" value="60"/>
+ *     <param name="{@link #setJanitorSleep(int) janitorSleep}" value="604800"/>
+ *     <param name="{@link #setJanitorFirstRunHourOfDay(int) janitorFirstRunHourOfDay}" value="1"/>
+ *     <param name="{@link #setSleepBetweenRecords(long) sleepBetweenRecords}" value="100"/>
+ *     <param name="{@link #setDelayDelete(boolean) delayDelete}" value="false"/>
+ *     <param name="{@link #setDelayDeleteSleep(long) delayDeleteSleep}" 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>
+ * 
+ * + * + */ +public class MultiDataStore implements DataStore { + + /** + * Logger instance + */ + private static Logger log = LoggerFactory.getLogger(MultiDataStore.class); + + private DataStore primaryDataStore; + private DataStore archiveDataStore; + + /** + * ReentrantLock that is used while the MultiDataStoreJanitor is running. + */ + private ReentrantLock janitorLock = new ReentrantLock(); + + /** + * Max Age in days. + */ + private int maxAge = 60; + private boolean janitorRunning = false; + + /** + * Its possible to delay the remove operation on the primary data store + * while janitor is running. The delete will be executed after defined + * delayDeleteSleep. + */ + private boolean delayDelete = false; + + /** + * The sleep time in seconds to delay remove operation + * on the primary data store, 1 day default. + */ + private long delayDeleteSleep = 60 * 60 * 24; + + /** + * Name of the file which holds the withhold data identifiers + */ + private final String WITHHOLD_FILE_KEY = "withholdIdentifiers"; + + /** + * File that holds the data identifiers if delayDelete is enabled. + */ + private FileSystemResource withholdFile = null; + + private Thread janitorThread; + private Thread withholdJanitorThread; + + /** + * The sleep time in seconds of the MultiDataStoreJanitor, 7 day default. + */ + private int janitorSleep = 60 * 60 * 24 * 7; + + /** + * The time in milliseconds the janitor will sleep between operating on records, + * 100 default. + */ + private long sleepBetweenRecords = 100; + + /** + * Indicates when the next run of the janitor is scheduled. The first run is + * scheduled by default at 01:00 hours. + */ + private Calendar janitorNextRun = Calendar.getInstance(); + { + if (janitorNextRun.get(Calendar.HOUR_OF_DAY) >= 1) { + janitorNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + janitorNextRun.set(Calendar.HOUR_OF_DAY, 1); + janitorNextRun.set(Calendar.MINUTE, 0); + janitorNextRun.set(Calendar.SECOND, 0); + janitorNextRun.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 (janitorRunning) { + janitorLock.lock(); + } + try { + DataRecord dataRecord = primaryDataStore + .getRecordIfStored(identifier); + if (dataRecord == null) { + dataRecord = archiveDataStore.getRecordIfStored(identifier); + } + return dataRecord; + } finally { + if (janitorRunning) { + janitorLock.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 (janitorRunning) { + janitorLock.lock(); + } + try { + return primaryDataStore.getRecord(identifier); + } catch (DataStoreException e) { + return archiveDataStore.getRecord(identifier); + } finally { + if (janitorRunning) { + janitorLock.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. + *

+ * The given stream is consumed and not closed by this + * method. It is the responsibility of the caller to close the stream. A + * typical call pattern would be: + * + *

+	 *     InputStream stream = ...;
+	 *     try {
+	 *         record = store.addRecord(stream);
+	 *     } finally {
+	 *         stream.close();
+	 *     }
+	 * 
+ * + * @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 getAllIdentifiers() + throws DataStoreException { + return archiveDataStore.getAllIdentifiers(); + } + + public void init(String homeDir) throws RepositoryException { + janitorThread = new Thread(new MultiDataStoreJanitor(), "Jackrabbit-MultiDataStoreJanitor"); + janitorThread.setDaemon(true); + janitorThread.start(); + log.info("MultiDataStore Janitor thread started; first run scheduled at " + janitorNextRun.getTime()); + if (delayDelete) { + LocalFileSystem fileSystem = new LocalFileSystem(); + fileSystem.setRoot(new File(homeDir)); + withholdFile = new FileSystemResource(fileSystem, FileSystem.SEPARATOR + WITHHOLD_FILE_KEY); + try { + if (withholdFile.exists() && (withholdFile.lastModified() + (delayDeleteSleep * 1000)) < System.currentTimeMillis()) { + withholdJanitorThread = new Thread(new WithholdJanitor(), "Jackrabbit-MultiDataStore-WithholdJanitor"); + withholdJanitorThread.setDaemon(true); + withholdJanitorThread.start(); + log.info("Old entries in the " + WITHHOLD_FILE_KEY + " File found. WithholdJanitor-Thread started now."); + } + } catch (FileSystemException e) { + throw new RepositoryException("I/O error while reading from '" + + withholdFile.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(); + } + + 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 data store janitor is running interrupt it + try { + if (janitorRunning) { + janitorThread.interrupt(); + } + } catch (Exception e) { + if (lastException != null) { + lastException = new DataStoreException(lastException); + } + } + // 4. if withhold janitor is running interrupt it + try { + if (withholdJanitorThread != null && withholdJanitorThread.isAlive()) { + withholdJanitorThread.interrupt(); + } + } catch (Exception e) { + if (lastException != null) { + lastException = new DataStoreException(lastException); + } + } + if (lastException != null) { + throw lastException; + } + } + + public void clearInUse() { + archiveDataStore.clearInUse(); + } + + public int getMaxAge() { + return maxAge; + } + + public void setMaxAge(int maxAge) { + this.maxAge = maxAge; + } + + public int getJanitorSleep() { + return janitorSleep; + } + + public int getJanitorFirstRunHourOfDay() { + return janitorNextRun.get(Calendar.HOUR_OF_DAY); + } + + public void setJanitorSleep(int sleep) { + this.janitorSleep = sleep; + } + + public void setJanitorFirstRunHourOfDay(int hourOfDay) { + janitorNextRun = Calendar.getInstance(); + if (janitorNextRun.get(Calendar.HOUR_OF_DAY) >= hourOfDay) { + janitorNextRun.add(Calendar.DAY_OF_MONTH, 1); + } + janitorNextRun.set(Calendar.HOUR_OF_DAY, hourOfDay); + janitorNextRun.set(Calendar.MINUTE, 0); + janitorNextRun.set(Calendar.SECOND, 0); + janitorNextRun.set(Calendar.MILLISECOND, 0); + } + + public void setSleepBetweenRecords(long millis) { + this.sleepBetweenRecords = millis; + } + + public long getSleepBetweenRecords() { + return sleepBetweenRecords; + } + + public boolean isDelayDelete() { + return delayDelete; + } + + public void setDelayDelete(boolean delayDelete) { + this.delayDelete = delayDelete; + } + + public long getDelayDeleteSleep() { + return delayDeleteSleep; + } + + public void setDelayDeleteSleep(long delayDeleteSleep) { + this.delayDeleteSleep = delayDeleteSleep; + } + + /** + * Writes the given DataIdentifier to the withhold File. + * + * @param identifier + * @return boolean true if it was successful otherwise false + */ + private boolean writeWithholdDataIdentifier(DataIdentifier identifier) { + BufferedWriter writer = null; + try { + writer = new BufferedWriter( + new OutputStreamWriter(withholdFile.getOutputStream())); + writer.write(identifier.toString()); + writer.newLine(); + return true; + } catch (Exception e) { + log.warn("I/O error while saving DataIdentifier to '" + + withholdFile.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 MultiDataStoreJanitor implements Runnable { + + /** + * {@inheritDoc} + */ + public void run() { + while (!Thread.currentThread().isInterrupted()) { + try { + log.info("Next move-data task run scheduled at " + janitorNextRun.getTime()); + long sleepTime = janitorNextRun.getTimeInMillis() - System.currentTimeMillis(); + if (sleepTime > 0) { + Thread.sleep(sleepTime); + } + janitorRunning = true; + moveOutdatedData(); + janitorRunning = false; + janitorNextRun.add(Calendar.SECOND, janitorSleep); + if (delayDelete) { + if (withholdJanitorThread != null && withholdJanitorThread.isAlive()) { + log.warn("The WithholdJanitor-Thread is already running."); + } else { + withholdJanitorThread = new Thread(new WithholdJanitor(), "Jackrabbit-MultiDataStore-WithholdJanitor"); + withholdJanitorThread.setDaemon(true); + withholdJanitorThread.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 = 1000 * 60 * 60 * 24 * maxAge; + Iterator allIdentifiers = primaryDataStore.getAllIdentifiers(); + int moved = 0; + while (allIdentifiers.hasNext()) { + DataIdentifier identifier = allIdentifiers.next(); + DataRecord dataRecord = primaryDataStore.getRecord(identifier); + if ((dataRecord.getLastModified() + maxAgeMilli) < now) { + try { + janitorLock.lock(); + if (delayDelete) { + // first write it to the file and then add it to the archive data store ... + if (writeWithholdDataIdentifier(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++; + } + } catch (DataStoreException e) { + log.error("Failed to move DataRecord. DataIdentifier: " + identifier, e); + } finally { + janitorLock.unlock(); + } + } + // Give other threads time to use the MultiDataStore while + // janitor is running.. + Thread.sleep(sleepBetweenRecords); + } + if (delayDelete) { + log.info("Moved " + moved + " DataRecords to the archive data store. The DataRecords in the primary data store will be removed in " + delayDeleteSleep +" 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 withhold DataRecords from the primary data store. + */ + public class WithholdJanitor implements Runnable { + + @Override + public void run() { + while (!Thread.currentThread().isInterrupted()) { + BufferedReader reader = null; + ArrayList problemIdentifiers = new ArrayList(); + try { + int deleted = 0; + reader = new BufferedReader( + new InputStreamReader(withholdFile.getInputStream())); + while (true) { + String s = reader.readLine(); + if (s == null || s.equals("")) { + break; + } + DataIdentifier identifier = new DataIdentifier(s); + try { + janitorLock.lock(); + ((MultiDataStoreAware) primaryDataStore).deleteRecord(identifier); + deleted++; + } catch (DataStoreException e) { + log.error("Failed to delete DataRecord. DataIdentifier: " + identifier, e); + problemIdentifiers.add(identifier); + } finally { + janitorLock.unlock(); + } + // Give other threads time to use the MultiDataStore while + // janitor is running.. + Thread.sleep(sleepBetweenRecords); + } + log.info("Deleted " + deleted+ " DataRecords from the primary data store."); + if (problemIdentifiers.isEmpty()) { + try { + withholdFile.delete(); + } catch (FileSystemException e) { + log.error("Unable to delete the " + WITHHOLD_FILE_KEY + " File."); + } + } else { + for (int x = 0; x < problemIdentifiers.size(); x++) { + writeWithholdDataIdentifier(problemIdentifiers.get(x)); + } + } + } catch (InterruptedException e) { + log.warn("Interrupted: stopping delay-delete task."); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.warn("Failed to run delay-delete task.", e); + } finally { + IOUtils.closeQuietly(reader); + } + } + } + } +} Index: src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java =================================================================== --- src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java (revision 0) +++ src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java (working copy) Property changes on: src/main/java/org/apache/jackrabbit/core/data/MultiDataStore.java ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property