/*
* @(#)JasenScanner.java 5/01/2005
*
* Copyright (c) 2004, 2005 jASEN.org
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the distribution.
*
* 3. The names of the authors may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* 4. Any modification or additions to the software must be contributed back
* to the project.
*
* 5. Any investigation or reverse engineering of source code or binary to
* enable emails to bypass the filters, and hence inflict spam and or viruses
* onto users who use or do not use jASEN could subject the perpetrator to
* criminal and or civil liability.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JASEN.ORG,
* OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
*/
package org.jasen;
import java.io.InputStream;
import java.util.List;
import javax.mail.internet.MimeMessage;
import org.apache.log4j.Logger;
import org.jasen.core.engine.Jasen;
import org.jasen.error.ErrorHandlerBroker;
import org.jasen.error.JasenException;
import org.jasen.event.JasenAutoUpdateListener;
import org.jasen.event.JasenScanListener;
import org.jasen.interfaces.JasenConfigurationLoader;
import org.jasen.interfaces.JasenMessage;
import org.jasen.interfaces.JasenScanResult;
import org.jasen.thread.ControlledThread;
import org.jasen.update.JasenAutoUpdateManager;
import org.jasen.update.JasenAutoUpdateParcel;
import org.jasen.update.JasenAutoUpdateParcelWrapper;
import org.jasen.update.JasenAutoUpdateReaper;
import org.jasen.update.JasenAutoUpdateReport;
import org.jasen.util.ReadOnlyList;
import org.jasen.util.ThreadUtils;
/**
* <p>
* JasenScanner is a singleton scanner class for safe scanning of email messages using the jASEN engine.
* </p>
* <p>
* All message scanning should be done through this class to guarantee thread safety and in particular to allow
* for correct behaviour during auto updates.
* </p>
* <p>
* It is not, however, mandatory that this class be used exclusively for scanning, merely recommended.
* <br/>
* For direct (non-singleton) access to the engine, use the org.jasen.core.engine.Jasen class
* </p>
* @see org.jasen.core.engine.Jasen
* @author Jason Polites
*/
public final class JasenScanner {
private static JasenScanner instance;
private static final Object instancelock = new Object();
private static final Object scanlock = new Object();
private boolean initialized = false;
private volatile boolean updating = false;
static Logger logger = Logger.getLogger(JasenScanner.class);
private Jasen jasen;
private Restarter restarter;
private boolean alive = false;
private boolean restartRequired = false;
private volatile int scansInProgress = 0;
private JasenScanListener scanListener;
private JasenAutoUpdateListener autoUpdateListener;
/**
* <p>
* The restarter polls for restart requests and restarts the engine when required
* </p>
*/
final class Restarter extends ControlledThread {
/**
*
*/
public Restarter() {
super();
}
/**
* @param name
*/
public Restarter(String name) {
super(name);
}
public void handleException(Exception e) {
jasen.getErrorHandler().handleException(e);
}
public void process() throws JasenException {
if(restartRequired) {
synchronized(scanlock) {
restartRequired = false;
try {
logger.debug("Restart required... Issueing engine restart...");
jasen.restart();
// if(autoUpdateListener != null) {
// autoUpdateListener.onAfterUpdate();
// }
}
finally {
scanlock.notifyAll();
}
}
}
}
}
/**
*
*/
private JasenScanner() {
super();
}
/**
* Gets the current instance of the JasenScanner
* @return The current (and only) JasenScanner instance
*/
public static final JasenScanner getInstance() {
if(instance == null) {
synchronized(instancelock) {
if(instance == null) {
instance = new JasenScanner();
}
instancelock.notifyAll();
}
}
return instance;
}
/**
* Gets the reference to the internal scan engine
* @return The single Jasen instance
*/
public Jasen getEngine() {
return this.jasen;
}
/**
* Initialises the engine with the default configuration
* @throws JasenException
* @see org.jasen.core.engine.Jasen#init()
*/
public void init() throws JasenException {
jasen = new Jasen();
jasen.init();
initInternal();
}
/**
* Initialises the engine using the configuration loader provided
* @param loader
* @throws JasenException
* @see org.jasen.core.engine.Jasen#init(JasenConfigurationLoader)
*/
public void init(JasenConfigurationLoader loader) throws JasenException {
jasen = new Jasen();
jasen.init(loader);
initInternal();
}
/**
* Initialises the engine with the configuration file specified
* @param config The absolute path to the configuration file
* @throws JasenException
* @see org.jasen.core.engine.Jasen#init(String)
* @deprecated
*/
public void init(String config) throws JasenException {
jasen = new Jasen();
jasen.init(config);
initInternal();
}
/**
* Initialises the engine with the configuration document passed as a stream
* @param in
* @throws JasenException
* @see org.jasen.core.engine.Jasen#init(InputStream)
* @deprecated
*/
public void init(InputStream in) throws JasenException {
jasen = new Jasen();
jasen.init(in);
initInternal();
}
private void initInternal() {
alive = true;
initialized = true;
logger.debug("Starting engine restarter thread");
restarter = new Restarter("Auto Update Restarter");
restarter.start();
// If we are supposed to update on start... force an update here
if(JasenAutoUpdateManager.getInstance().getConfiguration() != null && JasenAutoUpdateManager.getInstance().getConfiguration().isCheckOnStartup()) {
try {
forceUpdate();
} catch (JasenException e) {
ErrorHandlerBroker.getInstance().getErrorHandler().handleException(e);
}
}
}
/**
* Destroys the engine and all registered plugins
*/
public synchronized void destroy() {
if(alive) {
jasen.destroy();
logger.debug("Stopping engine restarter thread...");
if(!ThreadUtils.forceFinish(restarter, JasenAutoUpdateReaper.killTimeout)) {
logger.warn("Restarter thread in JasenScanner did not die and was killed");
}
alive = false;
}
}
/**
* Scans the given message.
* <p>
* All plugins will execute regardless of the probability (or computed total probability) discovered from any single plugin or combination thereof
* </p>
* @param mm The MimeMessage to be scanned
* @return The results of the scan as a JasenScanResult
* @throws JasenException
*/
public JasenScanResult scan(MimeMessage mm) throws JasenException {
return scan(mm, (String[])null);
}
/**
* Scans the given message.
* <p>
* The threshold value indicates the value at which we know the message is spam without continuing
* </p>
* If the engine computes this threshold prior to all plugins executing, tests are stopped and the result is
* returned immediately
* @param mm The MimeMessage to be scanned
* @param threshold The marker above which scanning ceases
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, JasenMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, float threshold) throws JasenException {
return scan(mm, threshold, null);
}
/**
* Scans the message without a threshold specified
* @param mm The MimeMessage to be scanned
* @param message A pre-parsed JasenMessage
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, JasenMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, JasenMessage message) throws JasenException {
return scan(mm, message, null);
}
/**
* Scans the given mime message using the already mime-parsed JasenMessage.
* <p>
* This implementation allows calling applications to implement their own JasenMessage by passing it to the scan engine.
* </p>
* @param mm The MimeMessage to be scanned
* @param message A pre-parsed JasenMessage
* @param threshold The thresholds. If any one plugin yields a result >= threshold, scanning is ceased and the result returned immediately
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, JasenMessage message, float threshold) throws JasenException {
return scan(mm, message, threshold, null);
}
/**
* Scans the given message.
* <p>
* All plugins will execute regardless of the probability (or computed total probability) discovered from any single plugin or combination thereof
* </p>
* @param mm The MimeMessage to be scanned
* @param ignored A list of plugin names which will be ignored during the scan. May be null
* @return The results of the scan as a JasenScanResult
* @throws JasenException
*/
public JasenScanResult scan(MimeMessage mm, String[] ignored) throws JasenException {
notifyScan();
try {
return jasen.scan(mm, ignored);
}
finally {
notifyScanComplete();
}
}
/**
* Scans the given message.
* <p>
* The threshold value indicates the value at which we know the message is spam without continuing
* </p>
* If the engine computes this threshold prior to all plugins executing, tests are stopped and the result is
* returned immediately
* @param mm The MimeMessage to be scanned
* @param threshold The marker above which scanning ceases
* @param ignored A list of plugin names which will be ignored during the scan. May be null
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, JasenMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, float threshold, String[] ignored) throws JasenException {
notifyScan();
try {
return jasen.scan(mm, threshold, ignored);
}
finally {
notifyScanComplete();
}
}
/**
* Scans the message without a threshold specified
* @param mm The MimeMessage to be scanned
* @param message A pre-parsed JasenMessage
* @param ignored A list of plugin names which will be ignored during the scan. May be null
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, JasenMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, JasenMessage message, String[] ignored) throws JasenException {
notifyScan();
try {
return jasen.scan(mm, message, ignored);
}
finally {
notifyScanComplete();
}
}
/**
* Scans the given mime message using the already mime-parsed JasenMessage.
* <p>
* This implementation allows calling applications to implement their own JasenMessage by passing it to the scan engine.
* </p>
* @param mm The MimeMessage to be scanned
* @param message A pre-parsed JasenMessage
* @param threshold The thresholds. If any one plugin yields a result >= threshold, scanning is ceased and the result returned immediately
* @param ignored A list of plugin names which will be ignored during the scan. May be null
* @return The results of the scan as a JasenScanResult
* @throws JasenException
* @see JasenScanner#scan(MimeMessage, JasenMessage, float)
*/
public JasenScanResult scan(MimeMessage mm, JasenMessage message, float threshold, String[] ignored) throws JasenException {
notifyScan();
try {
return jasen.scan(mm, message, threshold, ignored);
}
finally {
notifyScanComplete();
}
}
/**
* Records the occurrance of a scan
* @throws JasenException
*/
public void notifyScan() throws JasenException {
synchronized(scanlock) {
if(updating) {
try {
logger.debug("Waiting to execute a scan...");
scanlock.wait();
logger.debug("Stopped waiting executing scan...");
} catch (InterruptedException ignore) {}
if(!initialized) {
throw new JasenException("Cannot access the jASEN engine until it has been intialized!");
}
}
scansInProgress++;
scanlock.notifyAll();
}
if(scanListener != null) {
scanListener.onScanStart();
}
}
/**
* Records the completion of a scan
* @throws JasenException
*/
public void notifyScanComplete() throws JasenException {
synchronized(scanlock) {
scansInProgress--;
scanlock.notifyAll();
}
if(scanListener != null) {
scanListener.onScanEnd();
}
}
/**
* Called by the AutoUpdateManager if there is an update pending to signal a pause in scanning
*
*/
public boolean notifyPendingUpdate(JasenAutoUpdateParcel parcel) {
if((autoUpdateListener != null && autoUpdateListener.onBeforeUpdate())
|| autoUpdateListener == null) {
logger.debug("Scanner notified of pending update. Locking scan access...");
synchronized(scanlock) {
// Signal the intention to update
updating = true;
// Now wait for any current scans to complete...
while(scansInProgress > 0) {
try {
scanlock.wait();
} catch (InterruptedException ignore) {}
}
scanlock.notifyAll();
if(autoUpdateListener != null) {
return autoUpdateListener.onUpdateStart(new JasenAutoUpdateParcelWrapper(parcel));
}
else
{
return true;
}
}
}
else if(autoUpdateListener != null) {
logger.debug("Auto update listener aborted update");
}
return false;
}
/**
* Called by the AutoUpdateManager when the update has completed
* @param report A report of the results of an update
* @throws JasenException
*/
public void notifyUpdateComplete(JasenAutoUpdateReport report) throws JasenException {
synchronized(scanlock) {
boolean restart = report.isEngineRestart();
if(autoUpdateListener != null) {
if(autoUpdateListener.onUpdateEnd(report) && report.isEngineRestart()) {
restart = true;
}
}
if(restart) {
restartRequired = true;
logger.debug("Waking up the restarter");
restarter.wake();
}
updating = false;
logger.info("Auto update completed successfully" + ((report.isEngineRestart()) ? ", restart pending." : ""));
// Notify waiting listeners
if(autoUpdateListener != null) {
autoUpdateListener.onAfterUpdate();
}
scanlock.notifyAll();
}
}
public void notifyUpdateDownload(long bytes) {
if(autoUpdateListener != null) {
autoUpdateListener.onUpdateDownload(bytes);
}
}
/**
* Returns true IFF the engine is initialised and ready for scans.
* @return True if the engine is alive, false otherwise.
*/
public boolean isAlive() {
return alive;
}
/**
* Gets the listener registered for scan events if one has been set.
* @return The current listener, or null if no listener has been set
*/
public JasenScanListener getScanListener() {
return scanListener;
}
/**
* Sets the listener which will record scan events.
* @param scanListener
*/
public void setScanListener(JasenScanListener scanListener) {
this.scanListener = scanListener;
}
/**
* Gets the listener registered for auto update events if one has been set.
* @return The registered listener, or null if no listener has been set
*/
public JasenAutoUpdateListener getAutoUpdateListener() {
return autoUpdateListener;
}
/**
* Sets the listener which will record update completion events.
* @param autoUpdateListener
*/
public void setAutoUpdateListener(JasenAutoUpdateListener autoUpdateListener) {
this.autoUpdateListener = autoUpdateListener;
}
/**
* Forces the engine to run an update check
* @return If true, the force request was received and the engine is checking for
* updates. If false, the engine is either not running, or is already in the process of checking
* @throws JasenException
*/
public boolean forceUpdate() throws JasenException {
return JasenAutoUpdateManager.getInstance().forceUpdate();
}
/**
* Returns a read-only list of the plugins registered in the engine.
* <br/>
* The list does not allow modification. Any attempt to modify the list will
* result in an UnsupportedOperationException
* @return A list of PluginContainer objects containing the registered plugins
* @see org.jasen.core.PluginContainer
*/
public List getPlugins() {
return new ReadOnlyList(jasen.getPlugins());
}
}