/*
 * 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.axiom.attachments;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;

import java.io.File;

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 Map<String,Long> files = new HashMap<String,Long>();

    // Delete detection is batched
    private Long priorDeleteMillis = getTime();

    private Timer timer = null;

    private static AttachmentCacheMonitor _singleton = null;


    /**
     * Get or Create an AttachmentCacheMonitor singleton
     * @return the singleton instance
     */
    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 = 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<String> it = files.keySet().iterator();
            while (it.hasNext()) {
                String fileName = it.next();
                Long lastAccess = 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 ) {
        return AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
                @Override
                public Boolean run() {
                    return _deleteFile(fileName);
                }
            });
    }

    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 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
         */
        @Override
        public void run() {
            checkForAgedFiles();
        }
    }
}
