/*
* This software is distributed under the terms of the FSF
* Gnu Lesser General Public License (see lgpl.txt).
*
* This program is distributed WITHOUT ANY WARRANTY. See the
* GNU General Public License for more details.
*/
package com.scooterframework.autoloader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import com.scooterframework.common.logging.LogUtil;
/**
* <p>
* FileMonitor is responsible for monitoring file changes. Changed files are
* automatically recompiled.
* </p>
*
* <p>
* The default monitor interval is 1000 milliseconds. This can be changed by
* updating the <tt>source_file_monitor_period</tt> property in
* <tt>autoloader.properties</tt> file.
* </p>
*
* @author (Fei) John Chen
*/
public class FileMonitor {
private LogUtil log = LogUtil.getLogger(this.getClass().getName());
private static long oneHundredDays = 8640000;
private static boolean started = false;
private Date oldDate = null;
private Timer timer = null;
private long period = 0L;
private String sourcePath = "";
private long lastScanTime = 0L;
private static ConcurrentMap<String, SourceFile> sourceMap = new ConcurrentHashMap<String, SourceFile>();
private ConcurrentMap<String, SourceFile> modifiedSources = new ConcurrentHashMap<String, SourceFile>();
private long latestChange = 0L;
private static FileMonitor fm;
public static boolean turnOff = false;
static {
fm = new FileMonitor();
}
private FileMonitor() {
sourcePath = AutoLoaderConfig.getInstance().getSourcePath();
AutoLoaderConfig.getInstance().registerFileMonitor(this);
}
public static FileMonitor getInstance() {
return fm;
}
public void start() {
if (turnOff || started) return;
oldDate = new Date();
oldDate.setTime(oldDate.getTime()-oneHundredDays);
timer = new Timer();
sourcePath = AutoLoaderConfig.getInstance().getSourcePath();
period = AutoLoaderConfig.getInstance().getPeriod();
if (period > 0) {
SourceFileTimerTask sourceTask =
new SourceFileTimerTask(sourcePath);
schedule(sourceTask, period);
started = true;
log.debug("Java source file change monitor started with an interval of " + period + " milliseconds.");
}
}
/**
* Terminates this loader, discarding any currently scheduled tasks.
*
* @see java.util.Timer#cancel()
*/
public void stop() {
if (!started) return;
if (timer != null) {
timer.cancel();
log.debug("Java source file change monitor stopped.");
}
started = false;
}
/**
* Updates the FileMonitor, restarts the timer if the period is changed.
*/
public void update() {
long newPeriod = AutoLoaderConfig.getInstance().getPeriod();
if (newPeriod != period) {
stop();
if (newPeriod > 0) {
start();
}
}
}
public static boolean isStarted() {
return started;
}
/**
* Only those classes that are under src directory are monitored.
*
* @param className
* @return true if the class is monitored.
*/
public static boolean isClassMonitored(String className) {
return sourceMap.containsKey(className);
}
public static SourceFile getSourceFile(String className) {
SourceFile sf = (SourceFile)sourceMap.get(className);
if (sf == null) {
sf = SourceFileHelper.getSourceFileFromClassName(className);
}
return sf;
}
public long getLastScanTime() {
return lastScanTime;
}
private void schedule(TimerTask task, long period) {
if (period > 0) {
timer.schedule(task, oldDate, period);
}
else {
timer.schedule(task, oldDate);
}
}
private void scanAllSources(String sourceLocation) {
if (sourceLocation == null) return;
try {
StringTokenizer st = new StringTokenizer(sourceLocation, File.pathSeparator);
while (st.hasMoreElements()) {
String sourceDirPath = (String) st.nextElement();
File base = new File(sourceDirPath);
scanFiles(base, sourceDirPath);
}
lastScanTime = (new Date()).getTime();
recompile();
} catch (Exception ex) {
log.error("Error in scanAllSources() for " + sourceLocation + ": " + ex);
}
}
private void scanFiles(File file, String sourceDirPath) throws IOException {
if (file.isDirectory()) {
File[] files = file.listFiles();
int length = files.length;
for (int i=0; i<length; i++) {
File f = files[i];
scanFiles(f, sourceDirPath);
}
}
else {
String fn = file.getName();
if (fn.endsWith("java") && (fn.indexOf(' ') == -1)) {
processJavaFile(file, sourceDirPath);
}
}
}
private void processJavaFile(File file, String sourceDirPath) throws IOException {
String filePath = file.getCanonicalPath();
String className = SourceFileHelper.getClassNameFromSourceFile(file, sourceDirPath);
if (sourceMap.containsKey(className)) {
SourceFile sf = (SourceFile)sourceMap.get(className);
if (sf.isUpdated(file) || sf.availableForRecompile()) {
modifiedSources.putIfAbsent(filePath, sf);
}
}
else {
SourceFile sf = new SourceFile(file, sourceDirPath);
sourceMap.put(className, sf);
if (sf.availableForRecompile()) modifiedSources.put(filePath, sf);
}
}
private void recompile() throws Exception {
if (modifiedSources.size() <= 0) return;
//1. check if there is any change in source files
List<File> files = new ArrayList<File>(modifiedSources.size());
long sumTime = 0L;
for (Map.Entry<String, SourceFile> entry : modifiedSources.entrySet()) {
SourceFile sf = entry.getValue();
if (sf.getSource().exists()) {
files.add(sf.getSource());
sumTime += sf.getLastSourceModifiedTime();
}
}
if (sumTime == latestChange) return;
//2. recompile
log.debug("recompile classes: " + files);
latestChange = sumTime;
String result = JavaCompiler.compile(files);
//3. transform
if (result == null || "".equals(result)) {
List<String> classNames = new ArrayList<String>();
for (Map.Entry<String, SourceFile> entry : modifiedSources.entrySet()) {
SourceFile sf = entry.getValue();
if (sf.getClassFile().exists()) {
String className = SourceFileHelper.getClassNameFromClassFile(sf.getClassFile());
classNames.add(className);
}
}
ClassWorkHelper.preloadClasses(classNames);
}
//4. cleanup
if (result == null || "".equals(result)) {
modifiedSources.clear();
}
}
/**
* SourceFileTimerTask is responsible for scanning files.
*/
public class SourceFileTimerTask extends TimerTask {
private String sourceLocation = "";
public SourceFileTimerTask(String sourceLocation) {
super();
this.sourceLocation = sourceLocation;
}
public void run() {
if (started) scanAllSources(sourceLocation);
}
}
}