Author: scheu Date: Tue Oct 13 21:17:42 2009 New Revision: 824930 URL: http://svn.apache.org/viewvc?rev=824930&view=rev Log: WSCOMMONS-506 Contributor: Wendy Raschke Added a property to ensure that attachment files are deleted. Added a validation test.
Added: webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/AttachmentCacheMonitor.java Modified: webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/CachedFileDataSource.java webservices/commons/trunk/modules/axiom/modules/axiom-tests/src/test/java/org/apache/axiom/attachments/AttachmentsTest.java Added: webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/AttachmentCacheMonitor.java URL: http://svn.apache.org/viewvc/webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/AttachmentCacheMonitor.java?rev=824930&view=auto ============================================================================== --- webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/AttachmentCacheMonitor.java (added) +++ webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/AttachmentCacheMonitor.java Tue Oct 13 21:17:42 2009 @@ -0,0 +1,303 @@ +/* + * Copyright 2004, 2009 The Apache Software Foundation. + * + * Licensed 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.axiom.attachments; + +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Timer; +import java.util.TimerTask; + +import java.io.File; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedAction; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * The CacheMonitor is responsible for deleting temporary attachment files + * after a timeout period has expired. + * + * The register method is invoked when the attachment file is created. + * The access method is invoked whenever the attachment file is accessed. + * The checkForAgedFiles method is invoked whenever the monitor should look for + * files to cleanup (delete). + * + */ +public final class AttachmentCacheMonitor { + + static Log log = + LogFactory.getLog(AttachmentCacheMonitor.class.getName()); + + // Setting this property puts a limit on the lifetime of a cache file + // The default is "0", which is interpreted as forever + // The suggested value is 300 seconds + private int attachmentTimeoutSeconds = 0; // Default is 0 (forever) + private int refreshSeconds = 0; + public static final String ATTACHMENT_TIMEOUT_PROPERTY = "org.apache.axiom.attachments.tempfile.expiration"; + + // HashMap + // Key String = Absolute file name + // Value Long = Last Access Time + private HashMap files = new HashMap(); + + // Delete detection is batched + private Long priorDeleteMillis = getTime(); + + private Timer timer = null; + + private static AttachmentCacheMonitor _singleton = null; + + + /** + * Get or Create an AttachmentCacheMonitor singleton + * @return + */ + public static synchronized AttachmentCacheMonitor getAttachmentCacheMonitor() { + if (_singleton == null) { + _singleton = new AttachmentCacheMonitor(); + } + return _singleton; + } + + /** + * Constructor + * Intentionally private. Callers should use getAttachmentCacheMonitor + * @see getAttachmentCacheMonitor + */ + private AttachmentCacheMonitor() { + String value = ""; + try { + value = System.getProperty(ATTACHMENT_TIMEOUT_PROPERTY, "0"); + attachmentTimeoutSeconds = Integer.valueOf(value).intValue(); + } catch (Throwable t) { + // Swallow exception and use default, but log a warning message + if (log.isDebugEnabled()) { + log.debug("The value of " + value + " was not valid. The default " + + attachmentTimeoutSeconds + " will be used instead."); + } + } + refreshSeconds = attachmentTimeoutSeconds / 2; + + if (log.isDebugEnabled()) { + log.debug("Custom Property Key = " + ATTACHMENT_TIMEOUT_PROPERTY); + log.debug(" Value = " + attachmentTimeoutSeconds); + } + + if (refreshSeconds > 0) { + timer = new Timer( true ); + timer.schedule( new CleanupFilesTask(), + refreshSeconds * 1000, + refreshSeconds * 1000 ); + } + } + + /** + * @return timeout value in seconds + */ + public synchronized int getTimeout() { + return attachmentTimeoutSeconds; + } + + /** + * This method should + * Set a new timeout value + * @param timeout new timeout value in seconds + */ + public synchronized void setTimeout(int timeout) { + // If the setting to the same value, simply return + if (timeout == attachmentTimeoutSeconds) { + return; + } + + attachmentTimeoutSeconds = timeout; + + // Reset the refresh + refreshSeconds = attachmentTimeoutSeconds / 2; + + // Make sure to cancel the prior timer + if (timer != null) { + timer.cancel(); // Remove scheduled tasks from the prior timer + timer = null; + } + + // Make a new timer if necessary + if (refreshSeconds > 0) { + timer = new Timer( true ); + timer.schedule( new CleanupFilesTask(), + refreshSeconds * 1000, + refreshSeconds * 1000 ); + } + + if (log.isDebugEnabled()) { + log.debug("New timeout = " + attachmentTimeoutSeconds); + log.debug("New refresh = " + refreshSeconds); + } + } + + /** + * Register a file name with the monitor. + * This will allow the Monitor to remove the file after + * the timeout period. + * @param fileName + */ + public void register(String fileName) { + if (attachmentTimeoutSeconds > 0) { + _register(fileName); + _checkForAgedFiles(); + } + } + + /** + * Indicates that the file was accessed. + * @param fileName + */ + public void access(String fileName) { + if (attachmentTimeoutSeconds > 0) { + _access(fileName); + _checkForAgedFiles(); + } + } + + /** + * Check for aged files and remove the aged ones. + */ + public void checkForAgedFiles() { + if (attachmentTimeoutSeconds > 0) { + _checkForAgedFiles(); + } + } + + private synchronized void _register(String fileName) { + Long currentTime = getTime(); + if (log.isDebugEnabled()) { + log.debug("Register file " + fileName); + log.debug("Time = " + currentTime); + } + files.put(fileName, currentTime); + } + + private synchronized void _access(String fileName) { + Long currentTime = getTime(); + Long priorTime = (Long) files.get(fileName); + if (priorTime != null) { + files.put(fileName, currentTime); + if (log.isDebugEnabled()) { + log.debug("Access file " + fileName); + log.debug("Old Time = " + priorTime); + log.debug("New Time = " + currentTime); + } + } else { + if (log.isDebugEnabled()) { + log.debug("The following file was already deleted and is no longer available: " + + fileName); + log.debug("The value of " + ATTACHMENT_TIMEOUT_PROPERTY + + " is " + attachmentTimeoutSeconds); + } + } + } + + private synchronized void _checkForAgedFiles() { + Long currentTime = getTime(); + // Don't keep checking the map, only trigger + // the checking if it is plausible that + // files will need to be deleted. + // I chose a value of ATTACHMENTT_TIMEOUT_SECONDS/4 + if (isExpired(priorDeleteMillis, + currentTime, + refreshSeconds)) { + Iterator it = files.keySet().iterator(); + while (it.hasNext()) { + String fileName = (String) it.next(); + Long lastAccess = (Long) files.get(fileName); + if (isExpired(lastAccess, + currentTime, + attachmentTimeoutSeconds)) { + + if (log.isDebugEnabled()) { + log.debug("Expired file " + fileName); + log.debug("Old Time = " + lastAccess); + log.debug("New Time = " + currentTime); + log.debug("Elapsed Time (ms) = " + + (currentTime.longValue() - lastAccess.longValue())); + } + + deleteFile(fileName); + // Use the iterator to remove this + // file from the map (this avoids + // the dreaded ConcurrentModificationException + it.remove(); + } + } + + // Reset the prior delete time + priorDeleteMillis = currentTime; + } + } + + private boolean deleteFile(final String fileName ) { + Boolean privRet = (Boolean) AccessController.doPrivileged(new PrivilegedAction() { + public Object run() { + return _deleteFile(fileName); + } + }); + return privRet.booleanValue(); + } + + private Boolean _deleteFile(String fileName) { + boolean ret = false; + File file = new File(fileName); + if (file.exists()) { + ret = file.delete(); + if (log.isDebugEnabled()) { + log.debug("Deletion Successful ? " + ret); + } + } else { + if (log.isDebugEnabled()) { + log.debug("This file no longer exists = " + fileName); + } + } + return new Boolean(ret); + } + + + private Long getTime() { + return new Long(System.currentTimeMillis()); + } + + private boolean isExpired (Long oldTimeMillis, + Long newTimeMillis, + int thresholdSecs) { + long elapse = newTimeMillis.longValue() - + oldTimeMillis.longValue(); + return (elapse > (thresholdSecs*1000)); + } + + + private class CleanupFilesTask extends TimerTask { + + /** + * Trigger a checkForAgedFiles event + */ + public void run() { + checkForAgedFiles(); + } + } +} Modified: webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/CachedFileDataSource.java URL: http://svn.apache.org/viewvc/webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/CachedFileDataSource.java?rev=824930&r1=824929&r2=824930&view=diff ============================================================================== --- webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/CachedFileDataSource.java (original) +++ webservices/commons/trunk/modules/axiom/modules/axiom-api/src/main/java/org/apache/axiom/attachments/CachedFileDataSource.java Tue Oct 13 21:17:42 2009 @@ -22,12 +22,45 @@ import javax.activation.FileDataSource; import java.io.File; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + + public class CachedFileDataSource extends FileDataSource { String contentType = null; + + protected static Log log = LogFactory.getLog(CachedFileDataSource.class); + + // The AttachmentCacheMonitor is used to delete expired copies of attachment files. + private static AttachmentCacheMonitor acm = + AttachmentCacheMonitor.getAttachmentCacheMonitor(); + + // Represents the absolute pathname of cached attachment file + private String cachedFileName = null; public CachedFileDataSource(File arg0) { super(arg0); + if (log.isDebugEnabled()) { + log.debug("Enter CachedFileDataSource ctor"); + } + if (arg0 != null) { + try { + cachedFileName = arg0.getCanonicalPath(); + } catch (java.io.IOException e) { + log.error("IOException caught: " + e); + } + } + if (cachedFileName != null) { + if (log.isDebugEnabled()) { + log.debug("Cached file: " + cachedFileName); + log.debug("Registering the file with AttachmentCacheMonitor and also marked it as being accessed"); + } + // Tell the monitor that the file is being accessed. + acm.access(cachedFileName); + // Register the file with the AttachmentCacheMonitor + acm.register(cachedFileName); + } } public String getContentType() { Modified: webservices/commons/trunk/modules/axiom/modules/axiom-tests/src/test/java/org/apache/axiom/attachments/AttachmentsTest.java URL: http://svn.apache.org/viewvc/webservices/commons/trunk/modules/axiom/modules/axiom-tests/src/test/java/org/apache/axiom/attachments/AttachmentsTest.java?rev=824930&r1=824929&r2=824930&view=diff ============================================================================== --- webservices/commons/trunk/modules/axiom/modules/axiom-tests/src/test/java/org/apache/axiom/attachments/AttachmentsTest.java (original) +++ webservices/commons/trunk/modules/axiom/modules/axiom-tests/src/test/java/org/apache/axiom/attachments/AttachmentsTest.java Tue Oct 13 21:17:42 2009 @@ -19,6 +19,7 @@ package org.apache.axiom.attachments; +import org.apache.axiom.attachments.AttachmentCacheMonitor; import org.apache.axiom.attachments.utils.IOUtils; import org.apache.axiom.om.AbstractTestCase; import org.apache.axiom.om.OMElement; @@ -36,6 +37,7 @@ import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -380,6 +382,78 @@ assertTrue("Expected MessageContent Length of " + fileSize + " but received " + length, length == fileSize); } + + public void testCachedFilesExpired() throws Exception { + + // Set file expiration to 10 seconds + long INTERVAL = 5 * 1000; // 5 seconds for Thread to sleep + Thread t = Thread.currentThread(); + + + // Get the AttachmentCacheMonitor and force it to remove files after + // 10 seconds. + AttachmentCacheMonitor acm = AttachmentCacheMonitor.getAttachmentCacheMonitor(); + int previousTime = acm.getTimeout(); + + try { + acm.setTimeout(10); + + + File aFile = new File("A"); + aFile.createNewFile(); + String aFileName = aFile.getCanonicalPath(); + acm.register(aFileName); + + t.sleep(INTERVAL); + + File bFile = new File("B"); + bFile.createNewFile(); + String bFileName = bFile.getCanonicalPath(); + acm.register(bFileName); + + t.sleep(INTERVAL); + + acm.access(aFileName); + + // time since file A registration <= cached file expiration + assertTrue("File A should still exist", aFile.exists()); + + t.sleep(INTERVAL); + + acm.access(bFileName); + + // time since file B registration <= cached file expiration + assertTrue("File B should still exist", bFile.exists()); + + t.sleep(INTERVAL); + + File cFile = new File("C"); + cFile.createNewFile(); + String cFileName = cFile.getCanonicalPath(); + acm.register(cFileName); + acm.access(bFileName); + + t.sleep(INTERVAL); + + acm.checkForAgedFiles(); + + // time since file C registration <= cached file expiration + assertTrue("File C should still exist", cFile.exists()); + + t.sleep(10* INTERVAL); // Give task loop time to delete aged files + + + // All files should be gone by now + assertFalse("File A should no longer exist", aFile.exists()); + assertFalse("File B should no longer exist", bFile.exists()); + assertFalse("File C should no longer exist", cFile.exists()); + } finally { + + // Reset the timeout to the previous value so that no + // other tests are affected + acm.setTimeout(previousTime); + } + } /** * Returns the contents of the input stream as byte array.