/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2008
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package alma.acs.logging.archive.zoom;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.text.ParseException;
import java.util.Date;
import alma.acs.logging.engine.io.IOPorgressListener;
import alma.acs.util.IsoDateFormat;
import com.cosylab.logging.engine.ACS.ACSRemoteErrorListener;
import com.cosylab.logging.engine.ACS.ACSRemoteLogListener;
import com.cosylab.logging.engine.log.LogTypeHelper;
/**
* <code>FilesManager</code> organizes the files for the zooming:
* <UL>
* <LI>owns a list of files of logs
* <LI>return the right file(s) for zooming by date/time interval
* <LI>get the logs of the given time and level intervals
* </UL>
* <P>
* The folder to access files in this version is read from a java property.
* In future releases it could be read out of the CDB.
* <P>
* <I>Implementation note</i>: To reduce memory usage, objects of this class
* do not read the list of files in the folder until a request of loading
* is issued.
*
* <BR>
* File names have the following format: <code>logOutput<StartDate>--<EndDate>.xml</code>
*
* <P>
* ARCHIVE does not ensure that the logs in a file are only those having the timestamp
* between the start and the end date in the name of the file.
* This means that
* <UL>
* <LI>while zooming jlog should check not only in the files with the start and end date
* that are part of the select interval but also on other files near to the requested interval
* <BR>What does it mean <i>near</i> is something not clear at the present.
* <LI>a filter should be applied while reading files to discard logs that are
* out of the time interval
* </UL>
* Current version bases the selection of the files on the requested interval and the
* timestamp in the name of the files i.e. there is a chance to miss logs.
* <P>
* This has been agreed with ARCHIVE. They claim that there is a constant to tune in the
* CDB that should avoid this problem.
* See http://almasw.hq.eso.org/almasw/bin/view/Archive/ArchiveEightDotZero for further details.
*
*
* @author acaproni
*
*/
public class FilesManager {
/**
* A class to get events while loading logs from the files.
* <P>
* In this version, <code>ProgressListener</code> does nothing.
*
* @author acaproni
*
*/
private static class ProgressListener implements IOPorgressListener {
/**
* @see alma.acs.logging.engine.io.IOPorgressListener#bytesRead(long)
*/
@Override
public void bytesRead(long bytes) {}
/**
* @see alma.acs.logging.engine.io.IOPorgressListener#bytesWritten(long)
*/
@Override
public void bytesWritten(long bytes) {}
/**
* @see alma.acs.logging.engine.io.IOPorgressListener#logsRead(int)
*/
@Override
public void logsRead(int numOfLogs) {}
/**
* @see alma.acs.logging.engine.io.IOPorgressListener#logsWritten(int)
*/
@Override
public void logsWritten(int numOfLogs) {}
}
/**
* The filter for the file names.
* <P>
* The file names have the following format: <code>logOutput<StartDate>--<EndDate>.xml</code>.
* like for example <code>logOutput2008-09-19T11_21_50.115--2008-09-19T11_21_51.637.xml</code>.
* <P>
* The file names accepted by this filter are all the file names with the format described above
* and whose start and end dates are in a given time interval, defined in the constructor.
*
*
* @author acaproni
*
*/
public class FileNameFilter implements FilenameFilter {
/**
* The start name of each log file
*/
private static final String header="logOutput";
/**
* The start date in the file name
*/
private final long start;
/**
* The end date in the file name
*/
private final long end;
/**
* The start and end dates to accept in the file name
*
* @param start The start date (can be 0)
* @param end The end date
*/
public FileNameFilter(long start, long end) {
if (start<0 || end==0 || end<start) {
throw new IllegalArgumentException("Invalid time interval ["+start+", "+end+"]");
}
this.start=start;
this.end=end;
}
/**
* @see {@link FilenameFilter}
*/
@Override
public boolean accept(File dir, String name) {
if (name.indexOf(header)==-1) {
return false;
}
if (!name.toLowerCase().endsWith(".xml")) {
return false;
}
// Remove the header and the .xml
//
// For example if name was logOutput2008-09-19T11:21:49.670--2008-09-19T11:21:50.115.xml
// The following substring returns 2008-09-19T11:21:49.670--2008-09-19T11:21:50.115
name=name.substring(header.length(),name.length()-4);
// Get the start and end dates
String[] dates=name.split("--");
if (dates==null || dates.length!=2) {
return false;
}
dates[0]=dates[0].replaceAll("_", ":");
dates[1]=dates[1].replaceAll("_", ":");
Date startFileDate;
Date endFileDate;
try {
startFileDate=dateFormat.parse(dates[0]);
endFileDate=dateFormat.parse(dates[1]);
} catch (Throwable t) {
return false;
}
long startFile = startFileDate.getTime();
long endFile = endFileDate.getTime();
if (startFile>endFile) {
// Something wrong in the file name
return false;
}
return !(endFile<=start) && !(startFile>end);
}
}
/**
* The folder containing XML files of logs written by ARCHIVE
*/
public final String filesFolder;
/**
* The date format to read ISO dates
*/
private IsoDateFormat dateFormat = new IsoDateFormat();
/**
* Signal that the loading must be interrupted.
*/
private volatile boolean stopLoading;
/**
* The file where the manager is reading logs
*/
private FileHelper fileHelper=null;
/**
* Constructor
*
* @param folder The folder containing files of logs
* @throws ZoomException
*/
public FilesManager(String folder) throws ZoomException {
filesFolder=checkFolderOfFiles(folder);
}
/**
* Check if the folder where ARCHIVE writes files into is valid
* and points to a readable directory.
*
* @param folder The path of folder of log files
* @return The absolute path name of the folder of files;
*
* @throws ZoomException In case the folder is not found or not readable
*/
private String checkFolderOfFiles(String folder) throws ZoomException {
if (folder==null || folder.isEmpty()) {
throw new IllegalArgumentException("Invalid folder: "+folder);
}
// Check if the folder is readable
File f = new File(folder);
if (!f.isDirectory() || !f.canRead()) {
throw new ZoomException("Invalid folder of XML files: "+folder);
} else {
folder=f.getAbsolutePath();
}
return folder;
}
/**
* Get logs from files.
*
* @param startDate The start date of the loading (ISO format);
* if <code>null</code> the start date is not limited (i.e.
* the start date will be that of the first available log in the files)
* @param endDate The end date of the loading (ISO format);
* if <code>null</code> the current time is used
* @param logListener The listener to send logs to
* @param minLevel The min log level of logs to read from files (inclusive);
* if <code>null</code>, the lowest level (<code>TRACE</code>) is used
* @param maxLevel The max log level of logs to read from files (inclusive);
* if <code>null</code>, the highest log level (<code>EMERGENCY</code>) is used
* @param zoomListener The listener for monitoring the activity of the zoom engine.
* It can be <code>null</code>.
* @param errorListener The listener to be notified about logs that was not possible to parse
* @return <code>false</code> in case of errors, <code>false</code> otherwise.
*
* @see {@link ACSRemoteLogListener}
* @see {@link ZoomProgressListener}
*/
public boolean getLogs(
String startDate,
String endDate,
ACSRemoteLogListener logListener,
LogTypeHelper minLevel,
LogTypeHelper maxLevel,
ZoomProgressListener zoomListener,
ACSRemoteErrorListener errorListener) throws FileNotFoundException, ZoomException {
stopLoading=false;
long start;
long end;
// Get the start date
if (startDate==null) {
start=0L;
} else {
Date date;
try {
date= dateFormat.parse(startDate);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid start date: "+endDate,e);
}
start = date.getTime();
}
// Get the end date
if (endDate==null) {
end = System.currentTimeMillis();
} else {
Date date;
try {
date= dateFormat.parse(endDate);
} catch (ParseException e) {
throw new IllegalArgumentException("Invalid end date: "+endDate,e);
}
end=date.getTime();
}
if (logListener==null) {
throw new IllegalArgumentException("The listener can't be null");
}
if (minLevel==null) {
minLevel=LogTypeHelper.values()[0];
}
if (maxLevel==null) {
maxLevel=LogTypeHelper.values()[LogTypeHelper.values().length-1];
}
if (maxLevel.ordinal()<minLevel.ordinal()) {
throw new IllegalArgumentException("Invalid level range ["+minLevel+", "+maxLevel+"]");
}
if (zoomListener!=null) {
zoomListener.zoomReadingFile(0);
}
boolean ret=true;
// Get the files containing logs in the selected range
File[] files = getFileList(start, end);
if (zoomListener!=null) {
zoomListener.zoomTotalFileToRead(files.length);
}
// Read the files
int index=0;
for (File inFile: files) {
if (zoomListener!=null) {
zoomListener.zoomReadingFile(++index);
}
fileHelper = new FileHelper(inFile, start, end, minLevel, maxLevel);
if (stopLoading) {
return false;
}
ret = ret && fileHelper.loadLogs(logListener, new ProgressListener(), errorListener);
}
return ret;
}
/**
* Get the XML files of logs between the start and the end date
* (in msec as defined in {@link Date}).
*<P> TODO: use a better heuristic here instead of plain starts/end dates
*
* @param start The start date of the XML file (0 means unbounded)
* @param end The end date of the file
*
* @return A list of files with the given start and end dates
* in their names. The array can be empty if there are
* no files in the folder.
* A value of <code>null</code> means that an error happened during I/O
*/
public File[] getFileList(long start, long end) {
if (start>end) {
throw new IllegalArgumentException("Invalid time range");
}
if (filesFolder==null || filesFolder.isEmpty()) {
throw new IllegalStateException("Folder of files not initialized!");
}
FileNameFilter filter = new FileNameFilter(start,end);
File f = new File(filesFolder);
if (!f.isDirectory() || !f.canRead()) {
// This exception is an illegal state because the properties
// of the folder were already been checked in the constructor
// This exception means that something happened outside of jlog
// (for example someone deleted the folder)
throw new IllegalStateException(filesFolder+" unreadable/does not exist!");
}
return f.listFiles(filter);
}
/**
* Return <code>true</code> if the file manager is operative i.e. if:
* <UL>
* <LI>the folder of files exists and is readable
* <LI>the folder of files contains files of log suitable for zooming
* </UL>
* <P>
* The folder of files is checked when the object is created too.
* However someone from outside could have deleted, renamed
* or changed the accessing properties of the folder.
* <P>
* <I>Note</I>: this method could be slow if the folder contains
* a big number of files.
* @return <code>true</code> if the file manager is operative
*/
public boolean isOperational() {
try {
checkFolderOfFiles(filesFolder);
} catch (Exception e) {
return false;
}
File f = new File(filesFolder);
return getFileList(0, System.currentTimeMillis()).length>0;
}
/**
* Stop loading logs.
* <P>
* <code>stopLoading</code> does nothing if no load is in progress.
*/
public void stopLoading() {
stopLoading=true;
if (fileHelper!=null) {
fileHelper.stopLoading();
}
}
}