net.sf.eBusx.io.EFileWatcher Maven / Gradle / Ivy
//
// Copyright 2013, 2014, 2019 Charles W. Rapp
//
// 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 net.sf.eBusx.io;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.eBus.client.EFeed;
import net.sf.eBus.client.EFeed.FeedScope;
import net.sf.eBus.client.EFeedState;
import net.sf.eBus.client.EPublishFeed;
import net.sf.eBus.client.EPublisher;
import net.sf.eBus.client.ERequestFeed;
import net.sf.eBus.client.ERequestFeed.ERequest;
import net.sf.eBus.client.ERequestor;
import net.sf.eBus.client.IEPublishFeed;
import net.sf.eBus.client.IERequestFeed;
import net.sf.eBus.messages.EMessageKey;
import net.sf.eBus.messages.EReplyMessage;
import net.sf.eBus.messages.InvalidMessageException;
import net.sf.eBus.messages.type.DataType;
import net.sf.eBusx.util.ETimer;
import net.sf.eBusx.util.TimerRequest;
/**
* This class provides an eBus file watcher service, allowing
* asynchronous notification when a file or directory is created,
* modified or deleted. This service only supports JVM-local
* subscribers. This publisher advertises the
* {@link EFileNotification} with the file name as subject. The
* subject pattern is ".+" which supports any file name.
*
* The file watcher service starts its timer task with the first
* subscriber and stops the timer task when there are no more
* subscribers. This means the timer is running only when there
* are watched files.
*
* @author Charles Rapp
*/
public final class EFileWatcher
implements EPublisher,
ERequestor
{
//---------------------------------------------------------------
// Member data.
//
//-----------------------------------------------------------
// Constants.
//
/**
* The default watch rate is one minute. Value is in
* milliseconds.
*/
public static final long DEFAULT_WATCH_RATE = 60000L;
/**
* The minimum watch rate is one half a second. Value is in
* milliseconds.
*/
public static final long MIN_WATCH_RATE = 500L;
/**
* The maximum watch rate is one hour. Value is in
* milliseconds.
*/
public static final long MAX_WATCH_RATE = 3600000L;
/**
* The thread name is "EFileWatcher".
*/
public static final String TIMER_NAME = "EFileWatcher";
//-----------------------------------------------------------
// Statics.
//
/**
* Maps the subject to its file watcher instance.
*/
private static final Map sWatchers =
new HashMap<>();
/**
* The singleton lock protecting access to
* {@link #sWatchers}.
*/
private static final Lock sLock = new ReentrantLock();
/**
* Logging subsystem interface.
*/
private static final Logger sLogger =
Logger.getLogger(EFileWatcher.class.getName());
// Static initialization block.
static
{
ETimer.startETimer(TIMER_NAME);
try
{
DataType.findType(EFileNotification.class);
}
catch (IllegalArgumentException |
InvalidMessageException jex)
{
if (sLogger.isLoggable(Level.WARNING))
{
sLogger.log(Level.WARNING,
"Cannot file EFileNotification",
jex);
}
}
} // end of static initialization block.
//-----------------------------------------------------------
// Locals.
//
/**
* Watch this file to see if it is created, modified, or
* deleted.
*/
private final File mFile;
/**
* Message key based on the the {@link EFileNotification}
* message class and {@link #mFile} absolute name.
*/
private final EMessageKey mKey;
/**
* Check for file changes at this millisecond rate.
*/
private final long mWatchRate;
/**
* Set to {@code true} if {@link #mFile} exists and
* {@code false} otherwise.
*/
private boolean mExistsFlag;
/**
* {@link #mFile} current last modify time.
*/
private long mModifyTime;
/**
* {@link #mFile} current size.
*/
private long mFileSize;
/**
* {@link ETimer} request feed.
*/
private ERequestFeed sTimerFeed;
/**
* The file watch timer request.
*/
private volatile ERequest mTimerTask;
/**
* The {@link EPublishFeed file watcher publisher feed}.
*/
private EPublishFeed mFeed;
//---------------------------------------------------------------
// Member methods.
//
//-----------------------------------------------------------
// Constructors.
//
/**
* Creates a new EFileWatcher instance for the given file or
* directory name and watch rate.
* @param file the file or directory to watch.
* @param key message key based on {@code file}.
* @param rate watch for changes at this millisecond rate.
*/
private EFileWatcher(final File file,
final EMessageKey key,
final long rate)
{
mFile = file;
mKey = key;
mWatchRate = rate;
} // end of EFileWatcher(String, long)
//
// end of Constructors.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EObject Interface Implementation.
//
/**
* Returns absolute file name as the eBus object name.
* @return eBus object name.
*/
@Override
public String name()
{
return (mKey.subject());
} // end of name()
/**
* Advertises the file watcher service to the local JVM only.
* The timer task is started with the first subscriber and
* stopped when there are no more subscribers.
*/
@Override
public void startup()
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.fine(
String.format(
"EFileWatcher: advertising %s.", mKey));
}
// Advertise this service - but only within
// this JVM. Do not send it to remote eBus apps.
mFeed =
(EPublishFeed.builder()).target(this)
.messageKey(mKey)
.scope(FeedScope.LOCAL_ONLY)
.build();
mFeed.advertise();
// Subscribe to the ETimer request feed.
// Note: the timer task is started when the first watch
// entry is created.
sTimerFeed =
(ERequestFeed.builder()).target(this)
.messageKey(ETimer.TIMER_KEY)
.scope(FeedScope.LOCAL_ONLY)
.build();
sTimerFeed.subscribe();
} // end of startup()
/**
* Shuts down the file watcher thread by stopping the watch
* timer task, retracting the notification advertisement, and
* clearing the file watchers collection.
*/
@Override
public void shutdown()
{
// Firstly, stop the timer task.
if (mTimerTask != null)
{
mTimerTask.close();
mTimerTask = null;
}
// Secondly, stop the timer feed.
if (sTimerFeed != null)
{
sTimerFeed.close();
sTimerFeed = null;
}
// Thirdly, retract the file watch ad.
if (mFeed != null)
{
mFeed.close();
mFeed = null;
}
// Thirdly, clear the watcher map.
sWatchers.clear();
} // end of shutdown()
//
// end of EObject Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// EPublisher Interface Implementation.
//
/**
* Starts or stops a file watcher entry depending on
* {@code upFlag}. The notification subject is the file
* name.
* @param fs the f state is either up or down.
* @param f the f state applies to this publish f.
*/
@Override
public void publishStatus(final EFeedState fs,
final IEPublishFeed f)
{
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
String.format(
"EFileWatcher: %s watch is %s.",
mFile.getAbsolutePath(),
fs));
}
// Is this f being started?
if (fs == EFeedState.UP)
{
// Yes, start the timer task.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
"EFileWatcher: starting timer task.");
}
mTimerTask =
sTimerFeed.request(
TimerRequest.builder()
.timerName(mKey.toString())
.delay(mWatchRate)
.period(mWatchRate)
.build());
}
// No, the feed is being stopped.
else if (mTimerTask != null)
{
// Stop the timer task.
if (sLogger.isLoggable(Level.FINER))
{
sLogger.finer(
"EFileWatcher: stopping timer task.");
}
mTimerTask.close();
mTimerTask = null;
}
f.updateFeedState(fs);
} // end of publishStatus(EFeedState, IEPublishFeed)
//
// end of EPublisher Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// ERequestor Interface Implementation.
//
/**
* Logs the {@link ETimer} feed state.
* @param feedState the new eBus timer feed state.
* @param feed the eBus timer request feed.
*/
@Override
public void feedStatus(final EFeedState feedState,
final IERequestFeed feed)
{
if (sLogger.isLoggable(Level.FINE))
{
sLogger.log(Level.FINE,
"{0} is {1}.",
new Object[]
{
ETimer.TIMER_KEY, feedState
});
}
} // end of feedStatus(EFeedState, ERequestFeed)
/**
* Checks if any of the subscribed files have changed since
* the last timeout.
* @param remaining number of replies remaining in this
* request. Should be infinite.
* @param reply the timer reply message.
* @param request the timer request.
*/
@Override
public void reply(final int remaining,
final EReplyMessage reply,
final ERequest request)
{
final boolean existsFlag = mFile.exists();
final long modifyTime= mFile.lastModified();
final long fileSize = mFile.length();
EFileNotification.EventType eventType = null;
if (sLogger.isLoggable(Level.FINEST))
{
sLogger.finest(
String.format(
"%s: checking for changes.",
mFile.getPath()));
}
// Is this watcher still running?
if (mFeed == null)
{
// No, so do nothing and leave eventType null.
}
// Has the file been deleted?
else if (mExistsFlag && !existsFlag)
{
// Yes, the file is now gone.
eventType = EFileNotification.EventType.DELETE;
}
// No, not deleted.
// Has the file been created?
else if (!mExistsFlag && existsFlag)
{
// Yes, the file now exists.
eventType = EFileNotification.EventType.CREATE;
}
// No, the file was and still is in existance.
// Has the file been modified?
// Note: while the file size may be > or <
// the previous size, the modify time may only be
// > previous time.
else if (modifyTime > mModifyTime ||
fileSize != mFileSize)
{
// Yes, the file was modified.
eventType = EFileNotification.EventType.MODIFY;
}
// Did the file change in any way?
// Is the feed still up?
if (eventType != null && mFeed.isFeedUp())
{
// Yes, update the data members and tell the
// subscribers.
mExistsFlag = true;
mModifyTime = modifyTime;
mFileSize = fileSize;
mFeed.publish(
EFileNotification.builder()
.subject((mFeed.key()).subject())
.file(mFile)
.eventType(eventType)
.timestamp(mModifyTime)
.length(fileSize)
.build());
}
} // end of reply(int, EReplyMessage, ERequest)
//
// end of ERequestor Interface Implementation.
//-----------------------------------------------------------
//-----------------------------------------------------------
// Get Methods.
//
/**
* Returns {@code true} if the file watcher service is
* running for the given subject and {@code false} otherwise.
* @param file check if this file or directory has a watcher.
* @return {@code true} if the file watcher service is
* running.
*/
public static boolean isFileWatcherRunning(
final File file)
{
boolean retcode;
sLock.lock();
try
{
retcode = sWatchers.containsKey(file);
}
finally
{
sLock.unlock();
}
return (retcode);
} // end of isFileWatcherRunning(File)
//
// end of Get Methods.
//-----------------------------------------------------------
/**
* Creates the eBus file watcher service using the
* {@link #DEFAULT_WATCH_RATE default watch rate}.
* @param file watch this given file or directory.
* @throws IllegalStateException
* if the file watcher service is already running.
*/
public static void startFileWatcher(final File file)
{
startFileWatcher(file, DEFAULT_WATCH_RATE);
} // end of startFileWatcher(File)
/**
* Creates an eBus file watcher service for the specified
* file or directory and checking at a given watch rate.
* Returns the {@link EMessageKey message key} for this
* watcher. Use the returned key to
* {@link net.sf.eBus.client.ESubscribeFeed#open(net.sf.eBus.client.ESubscriber, net.sf.eBus.messages.EMessageKey, net.sf.eBus.client.EFeed.FeedScope, net.sf.eBus.client.ECondition) open}
* a subscription to the newly created file watcher.
* @param file watch this given file or directory.
* @param rate check for file updates at this millisecond
* rate.
* @return the file watcher message key used to subscribe to
* this file watcher.
* @throws IllegalArgumentException
* If {@code file} is {@code null} or if {@code rate} is <
* {@link #MIN_WATCH_RATE} or > {@link #MAX_WATCH_RATE}.
* @throws IllegalStateException
* if the file watcher service is already running but at a
* different watch rate.
*/
public static EMessageKey startFileWatcher(final File file,
final long rate)
{
EMessageKey retval = null;
if (file == null)
{
throw (new IllegalArgumentException("file is null"));
}
else if (rate < MIN_WATCH_RATE ||
rate > MAX_WATCH_RATE)
{
throw (
new IllegalArgumentException(
String.format(
"invalid watchRate %,d", rate)));
}
sLock.lock();
try
{
EFileWatcher watcher = sWatchers.get(file);
// Is this watcher already in existence?
if (watcher != null)
{
// Yes. At the same rate?
if (watcher.mWatchRate != rate)
{
// No.
throw (
new IllegalStateException(
"file watcher already started"));
}
// Ignore this redundant file watcher.
else
{
retval = watcher.mKey;
}
}
else
{
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info(
String.format(
"EFileWatcher: starting with %,d millisecond watch rate.",
rate));
}
// Create the singleton and connect it to the
// timer.
try
{
retval =
new EMessageKey(EFileNotification.class,
file.getAbsolutePath());
watcher =
new EFileWatcher(file, retval, rate);
sWatchers.put(file, watcher);
EFeed.register(watcher);
EFeed.startup(watcher);
}
catch (IllegalArgumentException jex)
{
// If the start up fails, then re-throws the
// exception inside an illegal state.
throw (
new IllegalStateException(
"file watcher startup failed", jex));
}
}
}
finally
{
sLock.unlock();
}
return (retval);
} // end of startFileWatcher(File, long)
/**
* Stops the watcher thread. This retracts the
* {@link EFileNotification} advertisement and discards all
* subscribed file watchers.
* @param file the file or directory. Must be the
* same as passed to {@link #startFileWatcher(File)}
*
* @see #startFileWatcher(File)
* @see #startFileWatcher(File, long)
*/
public static void stopFileWatcher(final File file)
{
sLock.lock();
try
{
final EFileWatcher watcher = sWatchers.remove(file);
if (watcher != null)
{
if (sLogger.isLoggable(Level.INFO))
{
sLogger.info("EFileWatcher: stopping.");
}
watcher.shutdown();
}
}
finally
{
sLock.unlock();
}
} // end of stopFileWatcher(File)
/**
* Returns the {@code EFileWatcher} message key for the given
* file or directory.
* @param file generate the message key for this file.
* @return file watcher message key for the given file.
*/
public static EMessageKey key(final File file)
{
if (file == null)
{
throw (new IllegalArgumentException("file is null"));
}
return (new EMessageKey(EFileNotification.class,
file.getAbsolutePath()));
} // end of key(File)
} // end of class EFileWatcher