mirror of
https://github.com/Alfresco/alfresco-community-repo.git
synced 2025-07-24 17:32:48 +00:00
Content URLs are now generated with an extra HOUR folder to handle high volume input in one day better
Added cleanup job for content stores - content is moved into (alf_data)/contentstore.deleted and mirrors the live content store - We'll make a call about disabling the trigger for the job, but currently it will fire at 4am git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@2422 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261
This commit is contained in:
@@ -8,6 +8,44 @@
|
||||
<value>${dir.contentstore}</value>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
|
||||
<!-- deleted content will get pushed into this store, where it can be cleaned up at will -->
|
||||
<bean id="deletedContentStore" class="org.alfresco.repo.content.filestore.FileContentStore">
|
||||
<constructor-arg>
|
||||
<value>${dir.contentstore.deleted}</value>
|
||||
</constructor-arg>
|
||||
</bean>
|
||||
<!-- bean to move deleted content into the the backup store -->
|
||||
<bean id="deletedContentBackupListener" class="org.alfresco.repo.content.cleanup.DeletedContentBackupCleanerListener" >
|
||||
<property name="store">
|
||||
<ref bean="deletedContentStore" />
|
||||
</property>
|
||||
</bean>
|
||||
<!-- Performs the content cleanup -->
|
||||
<bean id="contentStoreCleaner" class="org.alfresco.repo.content.cleanup.ContentStoreCleaner" >
|
||||
<property name="dictionaryService">
|
||||
<ref bean="dictionaryService" />
|
||||
</property>
|
||||
<property name="nodeDaoService" >
|
||||
<ref bean="nodeDaoService" />
|
||||
</property>
|
||||
<property name="transactionService" >
|
||||
<ref bean="transactionComponent" />
|
||||
</property>
|
||||
<property name="protectDays" >
|
||||
<value>14</value>
|
||||
</property>
|
||||
<property name="stores" >
|
||||
<list>
|
||||
<ref bean="fileContentStore" />
|
||||
</list>
|
||||
</property>
|
||||
<property name="listeners" >
|
||||
<list>
|
||||
<ref bean="deletedContentBackupListener" />
|
||||
</list>
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="contentService" class="org.alfresco.repo.content.RoutingContentService" init-method="init">
|
||||
<property name="transactionService">
|
||||
|
@@ -3,6 +3,7 @@
|
||||
dir.root=./alf_data
|
||||
|
||||
dir.contentstore=${dir.root}/contentstore
|
||||
dir.contentstore.deleted=${dir.root}/contentstore.deleted
|
||||
|
||||
# The location for lucene index files
|
||||
|
||||
|
@@ -52,32 +52,30 @@
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
<bean id="fileContentStoreCleanerTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
|
||||
<bean id="contentStoreCleanerTrigger" class="org.alfresco.util.TriggerBean">
|
||||
<property name="jobDetail">
|
||||
<bean id="fileContentStoreCleanerJobDetail" class="org.springframework.scheduling.quartz.JobDetailBean">
|
||||
<property name="jobClass">
|
||||
<value>org.alfresco.repo.content.ContentStoreCleanupJob</value>
|
||||
<value>org.alfresco.repo.content.cleanup.ContentStoreCleanupJob</value>
|
||||
</property>
|
||||
<property name="jobDataAsMap">
|
||||
<map>
|
||||
<entry key="contentStore">
|
||||
<ref bean="fileContentStore" />
|
||||
</entry>
|
||||
<entry key="searcher">
|
||||
<ref bean="searchService" />
|
||||
</entry>
|
||||
<entry key="protectHours">
|
||||
<value>24</value>
|
||||
<entry key="contentStoreCleaner">
|
||||
<ref bean="contentStoreCleaner" />
|
||||
</entry>
|
||||
</map>
|
||||
</property>
|
||||
</bean>
|
||||
</property>
|
||||
<property name="startDelay">
|
||||
<value>600000</value><!-- start after 10 minutes -->
|
||||
<!-- trigger at 4am -->
|
||||
<property name="hour">
|
||||
<value>04</value>
|
||||
</property>
|
||||
<property name="minute">
|
||||
<value>00</value>
|
||||
</property>
|
||||
<property name="repeatInterval">
|
||||
<value>3600000</value><!-- repeat every hour -->
|
||||
<value>86400000</value> <!-- repeat daily -->
|
||||
</property>
|
||||
</bean>
|
||||
|
||||
@@ -153,9 +151,7 @@
|
||||
<property name="triggers">
|
||||
<list>
|
||||
<ref bean="tempFileCleanerTrigger" />
|
||||
<!--
|
||||
<ref local="fileContentStoreCleanerTrigger"/>
|
||||
-->
|
||||
<ref local="contentStoreCleanerTrigger"/>
|
||||
<ref bean="ftsIndexerTrigger" />
|
||||
<ref bean="indexRecoveryTrigger" />
|
||||
<ref bean="indexBackupTrigger" />
|
||||
|
@@ -59,12 +59,14 @@ public abstract class AbstractContentStore implements ContentStore
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
int month = calendar.get(Calendar.MONTH) + 1; // 0-based
|
||||
int day = calendar.get(Calendar.DAY_OF_MONTH);
|
||||
int hour = calendar.get(Calendar.HOUR_OF_DAY);
|
||||
// create the URL
|
||||
StringBuilder sb = new StringBuilder(20);
|
||||
sb.append(STORE_PROTOCOL)
|
||||
.append(year).append('/')
|
||||
.append(month).append('/')
|
||||
.append(day).append('/')
|
||||
.append(hour).append('/')
|
||||
.append(GUID.generate()).append(".bin");
|
||||
String newContentUrl = sb.toString();
|
||||
// done
|
||||
|
@@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.search.SearchService;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/**
|
||||
* Removes all content form the store that is not referenced by any content node.
|
||||
* <p>
|
||||
* The following parameters are required:
|
||||
* <ul>
|
||||
* <li><b>contentStore</b>: The content store bean to clean up</li>
|
||||
* <li><b>searcher</b>: The index searcher that searches for content in the store</li>
|
||||
* <li><b>protectHours</b>: The number of hours to protect content that isn't referenced</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class ContentStoreCleanupJob implements Job
|
||||
{
|
||||
/**
|
||||
* Gets all content URLs from the store, checks if it is in use by any node
|
||||
* and deletes those that aren't.
|
||||
*/
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException
|
||||
{
|
||||
JobDataMap jobData = context.getJobDetail().getJobDataMap();
|
||||
// extract the content store to use
|
||||
Object contentStoreObj = jobData.get("contentStore");
|
||||
if (contentStoreObj == null || !(contentStoreObj instanceof ContentStore))
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"ContentStoreCleanupJob data must contain valid 'contentStore' reference");
|
||||
}
|
||||
ContentStore contentStore = (ContentStore) contentStoreObj;
|
||||
// extract the search to use
|
||||
Object searcherObj = jobData.get("searcher");
|
||||
if (searcherObj == null || !(searcherObj instanceof SearchService))
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"ContentStoreCleanupJob data must contain valid 'searcher' reference");
|
||||
}
|
||||
SearchService searcher = (SearchService) searcherObj;
|
||||
// get the number of hourse to protect content
|
||||
Object protectHoursObj = jobData.get("protectHours");
|
||||
if (protectHoursObj == null || !(protectHoursObj instanceof String))
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"ContentStoreCleanupJob data must contain valid 'protectHours' value");
|
||||
}
|
||||
long protectHours = 24L;
|
||||
try
|
||||
{
|
||||
protectHours = Long.parseLong((String) protectHoursObj);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"ContentStoreCleanupJob data 'protectHours' value is not a valid integer");
|
||||
}
|
||||
|
||||
long protectMillis = protectHours * 3600L * 1000L; // 3600s in an hour; 1000ms in a second
|
||||
long now = System.currentTimeMillis();
|
||||
long lastModifiedSafeTimeMs = (now - protectMillis); // able to remove anything modified before this
|
||||
|
||||
// get all URLs in the store
|
||||
Set<String> contentUrls = contentStore.getUrls();
|
||||
for (String contentUrl : contentUrls)
|
||||
{
|
||||
// TODO here we need to get hold of all the orphaned content in this store
|
||||
|
||||
// not found - it is not in the repo, but check that it is old enough to delete
|
||||
ContentReader reader = contentStore.getReader(contentUrl);
|
||||
if (reader == null || !reader.exists())
|
||||
{
|
||||
// gone missing in the meantime
|
||||
continue;
|
||||
}
|
||||
long lastModified = reader.getLastModified();
|
||||
if (lastModified >= lastModifiedSafeTimeMs)
|
||||
{
|
||||
// not old enough
|
||||
continue;
|
||||
}
|
||||
|
||||
// it is not in the repo and is old enough
|
||||
boolean result = contentStore.delete(contentUrl);
|
||||
System.out.println(contentUrl + ": " + Boolean.toString(result));
|
||||
}
|
||||
|
||||
// TODO for now throw this exception to ensure that this job does not get run until
|
||||
// the orphaned content can be correctly retrieved
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.repository.ContentWriter;
|
||||
import org.alfresco.util.BaseSpringTest;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.quartz.Scheduler;
|
||||
import org.quartz.SchedulerFactory;
|
||||
import org.quartz.impl.StdSchedulerFactory;
|
||||
import org.quartz.impl.calendar.BaseCalendar;
|
||||
import org.quartz.spi.TriggerFiredBundle;
|
||||
import org.springframework.scheduling.quartz.SimpleTriggerBean;
|
||||
|
||||
/**
|
||||
* Content store cleanup job unit test
|
||||
*
|
||||
* @author Roy Wetherall
|
||||
*/
|
||||
public class ContentStoreCleanupJobTest extends BaseSpringTest
|
||||
{
|
||||
private SimpleTriggerBean simpleTriggerBean;
|
||||
private JobExecutionContext jobExecutionContext;
|
||||
private ContentStoreCleanupJob job;
|
||||
|
||||
private ContentStore contentStore;
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* This can be removed once the class being tested actually has a remote
|
||||
* chance of working.
|
||||
*/
|
||||
public static TestSuite suite()
|
||||
{
|
||||
return new TestSuite();
|
||||
}
|
||||
|
||||
/**
|
||||
* On setup in transaction
|
||||
*/
|
||||
@Override
|
||||
protected void onSetUpInTransaction() throws Exception
|
||||
{
|
||||
this.contentStore = (ContentStore)this.applicationContext.getBean("fileContentStore");
|
||||
this.simpleTriggerBean = (SimpleTriggerBean)this.applicationContext.getBean("fileContentStoreCleanerTrigger");
|
||||
|
||||
SchedulerFactory factory = new StdSchedulerFactory();
|
||||
Scheduler scheduler = factory.getScheduler();
|
||||
|
||||
// Set the protect hours to 0 for the purpose of this test
|
||||
JobDataMap jobDataMap = this.simpleTriggerBean.getJobDetail().getJobDataMap();
|
||||
jobDataMap.put("protectHours", "0");
|
||||
this.simpleTriggerBean.getJobDetail().setJobDataMap(jobDataMap);
|
||||
|
||||
this.job = new ContentStoreCleanupJob();
|
||||
TriggerFiredBundle triggerFiredBundle = new TriggerFiredBundle(
|
||||
this.simpleTriggerBean.getJobDetail(),
|
||||
this.simpleTriggerBean,
|
||||
new BaseCalendar(),
|
||||
false,
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date(),
|
||||
new Date());
|
||||
|
||||
this.jobExecutionContext = new JobExecutionContext(scheduler, triggerFiredBundle, job);
|
||||
|
||||
ContentWriter contentWriter = this.contentStore.getWriter(null, null);
|
||||
contentWriter.putContent("This is some content that I am going to delete.");
|
||||
this.url = contentWriter.getContentUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test execute method
|
||||
*/
|
||||
public void testExecute()
|
||||
{
|
||||
try
|
||||
{
|
||||
ContentReader before = this.contentStore.getReader(this.url);
|
||||
assertTrue(before.exists());
|
||||
|
||||
this.job.execute(this.jobExecutionContext);
|
||||
|
||||
ContentReader after = this.contentStore.getReader(this.url);
|
||||
assertFalse(after.exists());
|
||||
}
|
||||
catch (JobExecutionException exception)
|
||||
{
|
||||
fail("Exception raised!");
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,218 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content.cleanup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.alfresco.repo.content.ContentStore;
|
||||
import org.alfresco.repo.node.db.NodeDaoService;
|
||||
import org.alfresco.repo.transaction.TransactionUtil;
|
||||
import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
|
||||
import org.alfresco.service.cmr.dictionary.DictionaryService;
|
||||
import org.alfresco.service.cmr.repository.ContentData;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.transaction.TransactionService;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* This component is responsible for finding orphaned content in a given
|
||||
* content store or stores. Deletion handlers can be provided to ensure
|
||||
* that the content is moved to another location prior to being removed
|
||||
* from the store(s) being cleaned.
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class ContentStoreCleaner
|
||||
{
|
||||
private static Log logger = LogFactory.getLog(ContentStoreCleaner.class);
|
||||
|
||||
private DictionaryService dictionaryService;
|
||||
private NodeDaoService nodeDaoService;
|
||||
private TransactionService transactionService;
|
||||
private List<ContentStore> stores;
|
||||
private List<ContentStoreCleanerListener> listeners;
|
||||
private int protectDays;
|
||||
|
||||
public ContentStoreCleaner()
|
||||
{
|
||||
this.stores = new ArrayList<ContentStore>(0);
|
||||
this.listeners = new ArrayList<ContentStoreCleanerListener>(0);
|
||||
this.protectDays = 7;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dictionaryService used to determine which properties are content properties
|
||||
*/
|
||||
public void setDictionaryService(DictionaryService dictionaryService)
|
||||
{
|
||||
this.dictionaryService = dictionaryService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param nodeDaoService used to get the property values
|
||||
*/
|
||||
public void setNodeDaoService(NodeDaoService nodeDaoService)
|
||||
{
|
||||
this.nodeDaoService = nodeDaoService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transactionService the component to ensure proper transactional wrapping
|
||||
*/
|
||||
public void setTransactionService(TransactionService transactionService)
|
||||
{
|
||||
this.transactionService = transactionService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param stores the content stores to clean
|
||||
*/
|
||||
public void setStores(List<ContentStore> stores)
|
||||
{
|
||||
this.stores = stores;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param listeners the listeners that can react to deletions
|
||||
*/
|
||||
public void setListeners(List<ContentStoreCleanerListener> listeners)
|
||||
{
|
||||
this.listeners = listeners;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the minimum number of days old that orphaned content must be
|
||||
* before deletion is possible. The default is 7 days.
|
||||
*
|
||||
* @param protectDays minimum age (in days) of deleted content
|
||||
*/
|
||||
public void setProtectDays(int protectDays)
|
||||
{
|
||||
this.protectDays = protectDays;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform basic checks to ensure that the necessary dependencies were injected.
|
||||
*/
|
||||
private void checkProperties()
|
||||
{
|
||||
if (dictionaryService == null)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Property 'dictionaryService' not set");
|
||||
}
|
||||
if (nodeDaoService == null)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Property 'nodeDaoService' not set");
|
||||
}
|
||||
if (transactionService == null)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Property 'transactionService' not set");
|
||||
}
|
||||
if (stores == null || stores.size() == 0)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Property 'stores' not set");
|
||||
}
|
||||
if (listeners == null)
|
||||
{
|
||||
throw new AlfrescoRuntimeException("Property 'listeners' not set");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> getValidUrls()
|
||||
{
|
||||
// wrap to make the request in a transaction
|
||||
TransactionWork<List<String>> getUrlsWork = new TransactionWork<List<String>>()
|
||||
{
|
||||
public List<String> doWork() throws Exception
|
||||
{
|
||||
return nodeDaoService.getContentDataStrings();
|
||||
};
|
||||
};
|
||||
// execute in READ-ONLY txn
|
||||
List<String> contentDataStrings = TransactionUtil.executeInUserTransaction(
|
||||
transactionService,
|
||||
getUrlsWork,
|
||||
true);
|
||||
|
||||
// get all valid URLs
|
||||
Set<String> validUrls = new HashSet<String>(contentDataStrings.size());
|
||||
// convert the strings to objects and extract the URL
|
||||
for (String contentDataString : contentDataStrings)
|
||||
{
|
||||
ContentData contentData = ContentData.createContentProperty(contentDataString);
|
||||
if (contentData.getContentUrl() != null)
|
||||
{
|
||||
// a URL was present
|
||||
validUrls.add(contentData.getContentUrl());
|
||||
}
|
||||
}
|
||||
// done
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("Found " + validUrls.size() + " valid URLs in metadata");
|
||||
}
|
||||
return validUrls;
|
||||
}
|
||||
|
||||
public void execute()
|
||||
{
|
||||
checkProperties();
|
||||
Set<String> validUrls = getValidUrls();
|
||||
// now clean each store in turn
|
||||
for (ContentStore store : stores)
|
||||
{
|
||||
clean(validUrls, store);
|
||||
}
|
||||
}
|
||||
|
||||
private void clean(Set<String> validUrls, ContentStore store)
|
||||
{
|
||||
Date checkAllBeforeDate = new Date(System.currentTimeMillis() - (long) protectDays * 3600L * 1000L * 24L);
|
||||
// get the store's URLs
|
||||
Set<String> storeUrls = store.getUrls(null, checkAllBeforeDate);
|
||||
// remove all URLs that occur in the validUrls
|
||||
storeUrls.removeAll(validUrls);
|
||||
// now clean the store
|
||||
for (String url : storeUrls)
|
||||
{
|
||||
ContentReader sourceReader = store.getReader(url);
|
||||
// announce this to the listeners
|
||||
for (ContentStoreCleanerListener listener : listeners)
|
||||
{
|
||||
// get a fresh reader
|
||||
ContentReader listenerReader = sourceReader.getReader();
|
||||
// call it
|
||||
listener.beforeDelete(listenerReader);
|
||||
}
|
||||
// delete it
|
||||
store.delete(url);
|
||||
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("Removed URL from store: \n" +
|
||||
" Store: " + store + "\n" +
|
||||
" URL: " + url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content.cleanup;
|
||||
|
||||
import org.alfresco.service.cmr.repository.ContentIOException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
|
||||
/**
|
||||
* A listener that can be plugged into a
|
||||
* {@link org.alfresco.repo.content.cleanup.ContentStoreCleaner cleaner} to
|
||||
* move soon-to-be-deleted content to a new location.
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public interface ContentStoreCleanerListener
|
||||
{
|
||||
public void beforeDelete(ContentReader reader) throws ContentIOException;
|
||||
}
|
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content.cleanup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.alfresco.repo.content.ContentStore;
|
||||
import org.alfresco.repo.content.filestore.FileContentStore;
|
||||
import org.alfresco.repo.node.db.NodeDaoService;
|
||||
import org.alfresco.service.ServiceRegistry;
|
||||
import org.alfresco.service.cmr.dictionary.DictionaryService;
|
||||
import org.alfresco.service.cmr.repository.ContentIOException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.repository.ContentWriter;
|
||||
import org.alfresco.service.transaction.TransactionService;
|
||||
import org.alfresco.util.ApplicationContextHelper;
|
||||
import org.alfresco.util.TempFileProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
/**
|
||||
* @see org.alfresco.repo.content.cleanup.ContentStoreCleaner
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class ContentStoreCleanerTest extends TestCase
|
||||
{
|
||||
private static ApplicationContext ctx = ApplicationContextHelper.getApplicationContext();
|
||||
|
||||
private ContentStoreCleaner cleaner;
|
||||
private ContentStore store;
|
||||
private ContentStoreCleanerListener listener;
|
||||
private List<String> deletedUrls;
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception
|
||||
{
|
||||
ServiceRegistry serviceRegistry = (ServiceRegistry) ctx.getBean("ServiceRegistry");
|
||||
TransactionService transactionService = serviceRegistry.getTransactionService();
|
||||
DictionaryService dictionaryService = serviceRegistry.getDictionaryService();
|
||||
NodeDaoService nodeDaoService = (NodeDaoService) ctx.getBean("nodeDaoService");
|
||||
|
||||
// we need a store
|
||||
store = new FileContentStore(TempFileProvider.getTempDir().getAbsolutePath());
|
||||
// and a listener
|
||||
listener = new DummyCleanerListener();
|
||||
// initialise record of deleted URLs
|
||||
deletedUrls = new ArrayList<String>(5);
|
||||
|
||||
// construct the test cleaner
|
||||
cleaner = new ContentStoreCleaner();
|
||||
cleaner.setTransactionService(transactionService);
|
||||
cleaner.setDictionaryService(dictionaryService);
|
||||
cleaner.setNodeDaoService(nodeDaoService);
|
||||
cleaner.setStores(Collections.singletonList(store));
|
||||
cleaner.setListeners(Collections.singletonList(listener));
|
||||
}
|
||||
|
||||
public void testImmediateRemoval() throws Exception
|
||||
{
|
||||
cleaner.setProtectDays(0);
|
||||
// add some content to the store
|
||||
ContentWriter writer = store.getWriter(null, null);
|
||||
writer.putContent("ABC");
|
||||
String contentUrl = writer.getContentUrl();
|
||||
|
||||
// fire the cleaner
|
||||
cleaner.execute();
|
||||
|
||||
// the content should have disappeared as it is not in the database
|
||||
assertFalse("Unprotected content was not deleted", store.exists(contentUrl));
|
||||
assertTrue("Content listener was not called with deletion", deletedUrls.contains(contentUrl));
|
||||
}
|
||||
|
||||
public void testProtectedRemoval() throws Exception
|
||||
{
|
||||
cleaner.setProtectDays(1);
|
||||
// add some content to the store
|
||||
ContentWriter writer = store.getWriter(null, null);
|
||||
writer.putContent("ABC");
|
||||
String contentUrl = writer.getContentUrl();
|
||||
|
||||
// fire the cleaner
|
||||
cleaner.execute();
|
||||
|
||||
// the content should have disappeared as it is not in the database
|
||||
assertTrue("Protected content was deleted", store.exists(contentUrl));
|
||||
assertFalse("Content listener was called with deletion of protected URL", deletedUrls.contains(contentUrl));
|
||||
}
|
||||
|
||||
private class DummyCleanerListener implements ContentStoreCleanerListener
|
||||
{
|
||||
public void beforeDelete(ContentReader reader) throws ContentIOException
|
||||
{
|
||||
deletedUrls.add(reader.getContentUrl());
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content.cleanup;
|
||||
|
||||
import org.alfresco.error.AlfrescoRuntimeException;
|
||||
import org.quartz.Job;
|
||||
import org.quartz.JobDataMap;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
|
||||
/**
|
||||
* Triggers the deletion of unused content using a
|
||||
* {@link org.alfresco.repo.content.cleanup.ContentStoreCleaner}.
|
||||
* <p>
|
||||
* The following parameters are required:
|
||||
* <ul>
|
||||
* <li><b>contentStoreCleaner</b>: The content store cleaner bean</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class ContentStoreCleanupJob implements Job
|
||||
{
|
||||
public ContentStoreCleanupJob()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the cleaner to do its work
|
||||
*/
|
||||
public void execute(JobExecutionContext context) throws JobExecutionException
|
||||
{
|
||||
JobDataMap jobData = context.getJobDetail().getJobDataMap();
|
||||
// extract the content cleaner to use
|
||||
Object contentStoreCleanerObj = jobData.get("contentStoreCleaner");
|
||||
if (contentStoreCleanerObj == null || !(contentStoreCleanerObj instanceof ContentStoreCleaner))
|
||||
{
|
||||
throw new AlfrescoRuntimeException(
|
||||
"ContentStoreCleanupJob data must contain valid 'contentStoreCleaner' reference");
|
||||
}
|
||||
ContentStoreCleaner contentStoreCleaner = (ContentStoreCleaner) contentStoreCleanerObj;
|
||||
contentStoreCleaner.execute();
|
||||
}
|
||||
}
|
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2005 Alfresco, Inc.
|
||||
*
|
||||
* Licensed under the Mozilla Public License version 1.1
|
||||
* with a permitted attribution clause. You may obtain a
|
||||
* copy of the License at
|
||||
*
|
||||
* http://www.alfresco.org/legal/license.txt
|
||||
*
|
||||
* 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.alfresco.repo.content.cleanup;
|
||||
|
||||
import org.alfresco.repo.content.ContentStore;
|
||||
import org.alfresco.service.cmr.repository.ContentIOException;
|
||||
import org.alfresco.service.cmr.repository.ContentReader;
|
||||
import org.alfresco.service.cmr.repository.ContentWriter;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
* Listens for content that is about to be deleted and moves it into the store
|
||||
* configured as the backup store.
|
||||
*
|
||||
* @author Derek Hulley
|
||||
*/
|
||||
public class DeletedContentBackupCleanerListener implements ContentStoreCleanerListener
|
||||
{
|
||||
private static Log logger = LogFactory.getLog(DeletedContentBackupCleanerListener.class);
|
||||
|
||||
private ContentStore store;
|
||||
|
||||
public DeletedContentBackupCleanerListener()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the store to copy soon-to-be-deleted content into
|
||||
*
|
||||
* @param store the deleted content backup store
|
||||
*/
|
||||
public void setStore(ContentStore store)
|
||||
{
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
public void beforeDelete(ContentReader reader) throws ContentIOException
|
||||
{
|
||||
// write the content into the target store
|
||||
ContentWriter writer = store.getWriter(null, reader.getContentUrl());
|
||||
// copy across
|
||||
writer.putContent(reader);
|
||||
// done
|
||||
if (logger.isDebugEnabled())
|
||||
{
|
||||
logger.debug("Moved content before deletion: \n" +
|
||||
" URL: " + reader.getContentUrl() + "\n" +
|
||||
" Store: " + store);
|
||||
}
|
||||
}
|
||||
}
|
@@ -330,6 +330,17 @@
|
||||
status.changeTxnId = :changeTxnId
|
||||
</query>
|
||||
|
||||
<query name="node.GetContentDataStrings">
|
||||
select distinct
|
||||
props.stringValue
|
||||
from
|
||||
org.alfresco.repo.domain.hibernate.NodeImpl as node
|
||||
join
|
||||
node.properties props
|
||||
where
|
||||
props.stringValue like 'contentUrl%'
|
||||
</query>
|
||||
|
||||
<query name="node.patch.GetNodesWithPersistedSerializableProperties">
|
||||
select distinct
|
||||
node
|
||||
|
@@ -24,12 +24,14 @@ import java.util.Map;
|
||||
import javax.transaction.UserTransaction;
|
||||
|
||||
import org.alfresco.model.ContentModel;
|
||||
import org.alfresco.repo.content.MimetypeMap;
|
||||
import org.alfresco.repo.domain.NodeStatus;
|
||||
import org.alfresco.repo.node.BaseNodeServiceTest;
|
||||
import org.alfresco.repo.transaction.AlfrescoTransactionSupport;
|
||||
import org.alfresco.repo.transaction.TransactionUtil;
|
||||
import org.alfresco.repo.transaction.TransactionUtil.TransactionWork;
|
||||
import org.alfresco.service.cmr.repository.ChildAssociationRef;
|
||||
import org.alfresco.service.cmr.repository.ContentData;
|
||||
import org.alfresco.service.cmr.repository.NodeRef;
|
||||
import org.alfresco.service.cmr.repository.NodeService;
|
||||
import org.alfresco.service.namespace.QName;
|
||||
@@ -257,4 +259,22 @@ public class DbNodeServiceImplTest extends BaseNodeServiceTest
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that the string_value retrieval against a property type is working
|
||||
*/
|
||||
public void testGetContentDataStringValues() throws Exception
|
||||
{
|
||||
ContentData contentData = new ContentData("abc", MimetypeMap.MIMETYPE_TEXT_PLAIN, 0L, null);
|
||||
// put this in as a random property
|
||||
nodeService.setProperty(
|
||||
rootNodeRef,
|
||||
QName.createQName(NAMESPACE, "random"),
|
||||
contentData);
|
||||
// get a list of all content values
|
||||
List<String> contentDataStrings = nodeDaoService.getContentDataStrings();
|
||||
assertNotNull(contentDataStrings);
|
||||
assertTrue("ContentData not represented as a String in results",
|
||||
contentDataStrings.contains(contentData.toString()));
|
||||
}
|
||||
}
|
||||
|
@@ -176,4 +176,12 @@ public interface NodeDaoService
|
||||
* returns <code>null</code>.
|
||||
*/
|
||||
public NodeStatus getNodeStatus(String protocol, String identifier, String id);
|
||||
|
||||
/**
|
||||
* Fetch all content data strings. These are all string values that begin
|
||||
* with <b>contentUrl=</b>.
|
||||
*
|
||||
* @return Returns the string values for content data
|
||||
*/
|
||||
public List<String> getContentDataStrings();
|
||||
}
|
||||
|
@@ -54,11 +54,11 @@ import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
|
||||
*/
|
||||
public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements NodeDaoService
|
||||
{
|
||||
public static final String QUERY_GET_ALL_STORES = "store.GetAllStores";
|
||||
public static final String QUERY_GET_CHILD_ASSOC = "node.GetChildAssoc";
|
||||
public static final String QUERY_GET_NODE_ASSOC = "node.GetNodeAssoc";
|
||||
public static final String QUERY_GET_NODE_ASSOC_TARGETS = "node.GetNodeAssocTargets";
|
||||
public static final String QUERY_GET_NODE_ASSOC_SOURCES = "node.GetNodeAssocSources";
|
||||
private static final String QUERY_GET_ALL_STORES = "store.GetAllStores";
|
||||
private static final String QUERY_GET_NODE_ASSOC = "node.GetNodeAssoc";
|
||||
private static final String QUERY_GET_NODE_ASSOC_TARGETS = "node.GetNodeAssocTargets";
|
||||
private static final String QUERY_GET_NODE_ASSOC_SOURCES = "node.GetNodeAssocSources";
|
||||
private static final String QUERY_GET_CONTENT_DATA_STRINGS = "node.GetContentDataStrings";
|
||||
|
||||
/** a uuid identifying this unique instance */
|
||||
private String uuid;
|
||||
@@ -504,4 +504,39 @@ public class HibernateNodeDaoServiceImpl extends HibernateDaoSupport implements
|
||||
// remove instance
|
||||
getHibernateTemplate().delete(assoc);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<String> getContentDataStrings()
|
||||
{
|
||||
HibernateCallback callback = new HibernateCallback()
|
||||
{
|
||||
public Object doInHibernate(Session session)
|
||||
{
|
||||
Query query = session.getNamedQuery(HibernateNodeDaoServiceImpl.QUERY_GET_CONTENT_DATA_STRINGS);
|
||||
return query.list();
|
||||
}
|
||||
};
|
||||
List<String> queryResults = (List) getHibernateTemplate().execute(callback);
|
||||
return queryResults;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user