/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2002
* Copyright by ESO (in the framework of the ALMA collaboration)
* and Cosylab 2002, All rights reserved
*
* 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.engine.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Collection;
import java.util.Iterator;
import java.util.zip.Deflater;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import alma.acs.logging.engine.parser.ACSLogParser;
import alma.acs.logging.engine.parser.ACSLogParserFactory;
import alma.acs.util.StopWatch;
import com.cosylab.logging.engine.LogMatcher;
import com.cosylab.logging.engine.ACS.ACSRemoteErrorListener;
import com.cosylab.logging.engine.ACS.ACSRemoteLogListener;
import com.cosylab.logging.engine.ACS.ACSRemoteRawLogListener;
import com.cosylab.logging.engine.log.ILogEntry;
/**
* An helper class to perform synchronous I/O operations like load and save.
* <P>
* Load and save methods are executed in a synchronous way i.e. they do not return
* until the I/O is terminated (or an exception occurs).
* <BR>
* Intermediate results useful to monitor the progress of the I/O are communicated
* to the listeners implementing the <code>IOProgressListener</code> interface.
* <P>
* The load and save methods of this class are <code>synchronized</code> but I would not say
* that this class is thread safe because it does not hold and lock the objects it receives
* as parameters like for example the <code>BufferReader</code> and the <code>BufferWriter</code>.
* So thread safety must be ensured by the owner of such objects.
* <P><B>Loading</B><BR>
* The loading is performed through one of the overloaded <code>loadLogs</code> methods.
* The bytes read and the number of the logs successfully read are sent to the listeners
* implementing the <code>IOProgressListener</code> interface.
* If there are filters, an audience or a discard level defined, then each log is checked
* before being sent to the listener.
*
* <P><B>Saving</B><BR>
* The saving of logs can be done by passing a <code>Collection </code> of logs or an <code>Iterator</code>to one of the
* overloaded <code>saveLogs</code> methods.
* Such methods communicates the progress to the listener implementing the
* <code>IOProgressListener</code> interface.
* <P>
* Saving logs by passing the name of the file does not require any extra steps.
* <BR>
* Saving logs by passing a <code>BufferedWriter</code> is always a three steps procedure:
* <OL>
* <LI>call the <code>prepareSaveFile</code> to write XML header
* <LI>save the logs by calling one of the <code>saveLogs</code> or the <code>saveLog</code>
* <LI>execute <code>Save</code> to add the closing XML tags, flush and close the buffer
* </OL>
* <P>
* Load and save can be very long operations. To stop an I/O, the <code>stopIO()</code>
* must be executed.
*
* @author acaproni
*
*/
public class IOHelper extends LogMatcher {
/**
* <code>GZipLogOutStream</code> subclass <code>GZIPOutputStream</code> to set the compression level.
*
* @author acaproni
*
*/
public class GZipLogOutStream extends GZIPOutputStream {
/**
* Constructor
*
* @param stream The stream for writing compressed logs into
* @param level The compression level
*
* @throws IOException
*
* @see {@link Deflater}
*/
public GZipLogOutStream(OutputStream stream, int level) throws IOException {
super(stream);
super.def.setLevel(level);
}
/**
* Constructor using the default compression level
*
* @param stream The stream for writing compressed logs into
* @throws IOException
*/
public GZipLogOutStream(OutputStream stream) throws IOException {
super(stream,DEFAULT_COMPRESSION_LEVEL);
}
}
/**
* Constructor
*
* @throws Exception In case of errors building the parser
*/
public IOHelper() throws Exception {
parser=ACSLogParserFactory.getParser();
}
/**
* The default compression level while saving files
*/
public static final int DEFAULT_COMPRESSION_LEVEL = 5;
/**
* Signal that a load or a save must be stopped
*/
protected volatile boolean stopped=false;
/**
* The parser
*/
private final ACSLogParser parser;
/**
* Inject the log into the engine
*
* @param logStr The string representation of the log
* @param logListener The listener i.e. the callback for each new log to add
*/
private void injectLog (
StringBuilder logStr,
ACSRemoteLogListener logListener,
ACSRemoteRawLogListener rawLogListener,
ACSRemoteErrorListener errorListener) {
if (errorListener==null || (logListener==null && rawLogListener==null)) {
throw new IllegalArgumentException("Listeners can't be null");
}
if (logListener!=null) {
ILogEntry log=null;
try {
log = parser.parse(logStr.toString().trim());
} catch (Exception e) {
errorListener.errorReceived(logStr.toString().trim());
System.err.println("Exception parsing a log: "+e.getMessage()+" ["+logStr+"]");
e.printStackTrace(System.err);
return;
}
if (match(log)) {
logListener.logEntryReceived(log);
}
}
if (rawLogListener!=null) {
rawLogListener.xmlEntryReceived(logStr.toString().trim());
}
}
/**
* Load the logs from the file with the given name.
* <P>
* The logs are sent to the <code>ACSRemoteLogListener</code> and /or
* to the <code>ACSRemoteRawLogListener</code>.
*
* @param fileName The name of the file to read logs from
* @param logListener The callback for each new log read from the IO
* @param rawLogListener The callback for each new XML log read from the IO
* @param errorListener The listener for errors
* @param progressListener The listener to be notified about the bytes read
* @param gzip If <code>true</code> the file to read is compressed in GZIP format
* @return The length of the file to read
* @throws IOException In case of an IO error while reading the file
* @throws Exception In case of error building the parser
*/
public synchronized long loadLogs(
String fileName,
ACSRemoteLogListener logListener,
ACSRemoteRawLogListener rawLogListener,
ACSRemoteErrorListener errorListener,
IOPorgressListener progressListener,
boolean gzip) throws IOException, Exception {
if (fileName==null || fileName.isEmpty()) {
throw new IllegalArgumentException("Invalid file name: "+fileName);
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
File f = new File(fileName);
InputStream inStream = new FileInputStream(f);
BufferedReader reader;
if (gzip) {
InputStreamReader inStreamReader= new InputStreamReader(new GZIPInputStream(inStream));
reader = new BufferedReader(inStreamReader);
} else {
reader = new BufferedReader(new InputStreamReader(inStream));
}
loadLogs(reader, logListener, rawLogListener, errorListener,progressListener);
return f.length();
}
/**
* Load the logs from the file with the given name.
* <P>
* The logs are sent to the <code>ACSRemoteLogListener</code> and /or
* to the <code>ACSRemoteRawLogListener</code>.
* <P>
* The file can be compressed (GZIP) or plain.
* Compressed file names must terminate with <I>.gz</I> while
* plain XML file names must end with <I>.xml</I>.
*
* @param fileName The name of the file to read logs from.
* <code>fileName</code> must terminate with .gz or .xml (case insensitive)
* @param logListener The callback for each new log read from the IO
* @param rawLogListener The callback for each new XML log read from the IO
* @param errorListener The listener for errors
* @param progressListener The listener to be notified about the bytes read
* @return The length of the file to read
* @throws IOException In case of an IO error while reading the file
* @throws Exception In case of error building the parser
*/
public synchronized long loadLogs(
String fileName,
ACSRemoteLogListener logListener,
ACSRemoteRawLogListener rawLogListener,
ACSRemoteErrorListener errorListener,
IOPorgressListener progressListener) throws IOException, Exception {
String name = fileName.toLowerCase();
if (!name.endsWith(".gz") && !name.endsWith(".xml")) {
throw new IllegalArgumentException("File name must end with .gz or .xml");
}
return loadLogs(fileName, logListener, rawLogListener, errorListener, progressListener,name.endsWith(".gz"));
}
/**
* Load the logs from the given <code>BufferedReader</code>.
* <P>
* The logs are sent to the <code>ACSRemoteLogListener</code> and /or
* to the <code>ACSRemoteRawLogListener</code>.
*
* @param reader The reader to read logs from
* @param logListener The callback for each new log read from the IO
* @param rawLogListener The callback for each new XML log read from the IO
* @param errorListener The listener for errors
* @param progressListener The listener to be notified about the bytes read
*
* @throws IOException In case of an IO error while reading the file
*/
public synchronized void loadLogs(
BufferedReader reader,
ACSRemoteLogListener logListener,
ACSRemoteRawLogListener rawLogListener,
ACSRemoteErrorListener errorListener,
IOPorgressListener progressListener) throws IOException {
if (reader==null || errorListener==null) {
throw new IllegalArgumentException("Parameters can't be null");
}
if (logListener==null && rawLogListener==null) {
throw new IllegalArgumentException("No log listeners defined");
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
stopped=false;
// The "clever" buffer
LogStringBuffer buffer = new LogStringBuffer();
// Read one char per iteration
int chRead;
// Count the bytes read
int bytesRead=0;
int logRecordsRead = 0;
// Here the buffer writes the XML of each log
StringBuilder xmlStr = new StringBuilder();
/**
* The size of the buffer
*/
final int size=16384;
/**
* The buffer of data read from the file
*/
char[] buf =new char[size];
/**
* The cursor to scan the buffer (circular)
*/
int actualPos=-1;
/**
* When it is 0, then we have to read another block from the file
*/
int bytesInBuffer=0;
StopWatch stopWatch = new StopWatch();
// This thread is very CPU demanding so we want to give other
// threads a chance to run too...
int yelder=0;
while (true && !stopped) {
// Read a block from the file if the buffer is empty
if (bytesInBuffer==0) {
bytesInBuffer = reader.read(buf,0,size);
}
if (bytesInBuffer<=0) { // EOF
break;
}
bytesInBuffer--;
actualPos=(actualPos+1)%size;
chRead=buf[actualPos];
bytesRead++;
buffer.append((char)chRead,xmlStr);
if (xmlStr.length()>0) {
// A new log has been found
injectLog(xmlStr,logListener, rawLogListener, errorListener);
logRecordsRead++;
xmlStr.delete(0, xmlStr.length());
progressListener.bytesRead(bytesRead);
if (logRecordsRead%25==0) {
progressListener.logsRead(logRecordsRead);
}
}
yelder=(yelder++)%5000;
if (yelder==0) {
Thread.yield();
}
}
System.out.println("XML log record import finished with " + logRecordsRead + " records in " +
stopWatch.getLapTimeMillis()/1000 + " seconds.");
}
/**
* Write the XML header in the buffered writer
*
* @param bw
* @throws IOException
*/
public synchronized void writeHeader(BufferedWriter wBuffer) throws IOException {
if (wBuffer==null) {
throw new IllegalArgumentException("The BufferedWriter can't be null");
}
String header = new String("<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\n<Log>\n<Header Name=\"NameForXmlDocument\" Type=\"LOGFILE\" />\n");
wBuffer.write(header, 0,header.length());
}
/**
* Terminate the saving.
* <P>
* This method must executed when the save terminates.
* It does the following:
* <UL>
* <LI>write the closing XML tags
* <LI>flush the output file
* <li>close the file
* </UL>
* @param outBuffer The file to close
* @param close If <code>true</code> the <code>BufferedWriter</code> is closed
* @throws IOException In case of an IO error
*/
public synchronized void terminateSave(BufferedWriter outBuffer, boolean close) throws IOException {
String str = new String("</Log>");
outBuffer.write(str,0,str.length());
outBuffer.flush();
if (close) {
outBuffer.close();
}
}
/**
* Save a log in the passed file
*
* @param outbuf The buffered writer where the logs have to be stored
* @param log The log to save
* @param progressListener The listener to be notified about the bytes written
* @throws IOException In case of an IO error while writing logs into the file
*/
public synchronized int saveLog(BufferedWriter outBuf, ILogEntry log) throws IOException {
if (outBuf==null) {
throw new IllegalArgumentException("BufferedWriter can't be null");
}
if (log==null) {
throw new IllegalArgumentException("The log can't be null");
}
String str = log.toXMLString()+"\n";
outBuf.write(str,0,str.length());
return str.length();
}
/**
* Save a collection of logs on disk
*
* @param fileName The name of the file to store logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file aredy exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP) with the default compression level
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(
String fileName,
Collection<ILogEntry> logs,
IOPorgressListener progressListener,
boolean append,
boolean gzip) throws IOException {
saveLogs(fileName, logs, progressListener,append,gzip,DEFAULT_COMPRESSION_LEVEL);
}
/**
* Save a collection of logs on disk
*
* @param fileName The name of the file to store logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file aredy exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP)
* @param compressionLevel The compressionLevel for GZIP compression (0..9);
* ignored if <code>gzip</code> is is <code>false>/code>
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(
String fileName,
Collection<ILogEntry> logs,
IOPorgressListener progressListener,
boolean append,
boolean gzip,
int compressionLevel) throws IOException {
if (logs==null || logs.isEmpty()) {
throw new IllegalArgumentException("No logs to save");
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
Iterator<ILogEntry> iterator = logs.iterator();
saveLogs(fileName, iterator, progressListener,append,gzip,compressionLevel);
}
/**
* Save a collection of logs on a <code>BufferedWriter</code>.
* <P>
* The buffer must be initialized and terminated i.e. the <code>prepareSaveFile</code>
* and the <code>terminateSave</code> are not executed by this method.
*
* @param outBuffer The writer to write logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @param gzip If <code>true</code> the file is compressed (GZIP)
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(BufferedWriter outBuffer, Collection<ILogEntry> logs, IOPorgressListener progressListener) throws IOException {
if (logs==null || logs.isEmpty()) {
throw new IllegalArgumentException("No logs to save");
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
Iterator<ILogEntry> iterator = logs.iterator();
saveLogs(outBuffer, iterator, progressListener);
}
/**
* Save the logs available through an <code>Iterator</code>.
*
* @param filename The name of the file to write logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file already exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP) with the default compression level
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(
String fileName,
Iterator<ILogEntry>iterator,
IOPorgressListener progressListener,
boolean append,
boolean gzip) throws IOException {
saveLogs(fileName, iterator, progressListener, append, gzip,DEFAULT_COMPRESSION_LEVEL);
}
/**
* Save the logs available through an <code>Iterator</code>.
*
* @param filename The name of the file to write logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file already exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP)
* @param compressionLevel The compressionLevel for GZIP compression (0..9);
* ignored if <code>gzip</code> is <code>false>/code>
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(
String fileName,
Iterator<ILogEntry>iterator,
IOPorgressListener progressListener,
boolean append,
boolean gzip,
int compressionLevel) throws IOException {
if (iterator==null || !iterator.hasNext()) {
throw new IllegalArgumentException("No logs to save");
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
BufferedWriter writer = getBufferedWriter(fileName, append,gzip, compressionLevel);
writeHeader(writer);
saveLogs(writer, iterator,progressListener);
terminateSave(writer,true);
}
/**
* Create a <code>BufferedWriter</code> to save logs into
*
* @param fileName The name of the file to write logs into
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file already exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP)
* @param compressionLevel The compressionLevel for GZIP compression (0..9);
* ignored if <code>gzip</code> is <code>false>/code>
*
* @return the <code>BufferedWriter</code> to save logs into the file of the given name
*/
public synchronized BufferedWriter getBufferedWriter(String fileName, boolean append, boolean gzip, int compressionLevel)
throws FileNotFoundException, IOException{
OutputStream outStream=new FileOutputStream(fileName,append);
BufferedWriter writer;
if (gzip) {
outStream = new GZipLogOutStream(outStream,compressionLevel);
}
writer = new BufferedWriter(new OutputStreamWriter(outStream));
return writer;
}
/**
* Create a <code>BufferedWriter</code> to save logs into.
* <P>
* In case of compressed files, the default compression level is used
*
* @param fileName The name of the file to write logs into
* @param append <UL><LI>if <code>true</code> if the logs in the collection must be appended to an existing file</LI>
* <LI>if <code>false</code> and the file already exists, it is deleted before writing
* </UL>
* @param gzip If <code>true</code> the file is compressed (GZIP) with the default level
*
* @return the <code>BufferedWriter</code> to save logs into the file of the given name
*/
public synchronized BufferedWriter getBufferedWriter(String fileName, boolean append, boolean gzip)
throws FileNotFoundException, IOException{
return getBufferedWriter(fileName, append, gzip,DEFAULT_COMPRESSION_LEVEL);
}
/**
* Return the reader to get logs from
* <P>
* The reader can be compressed (GZIP) or not depending on the
* extension of the file name (i.e. ".gz" or ".xml").
*
* @param fileName The name of the file to read
* @return the reader to get logs from
*/
public synchronized BufferedReader getBufferedReader(String fileName) throws FileNotFoundException, IOException {
String name = fileName.toLowerCase();
if (!name.endsWith(".gz") && !name.endsWith(".xml")) {
throw new IllegalArgumentException("File name must end with .gz or .xml");
}
File f = new File(fileName);
InputStream inStream = new FileInputStream(f);
if (name.endsWith(".gz")) {
InputStreamReader inStreamReader= new InputStreamReader(new GZIPInputStream(inStream));
return new BufferedReader(inStreamReader);
} else {
return new BufferedReader(new InputStreamReader(inStream));
}
}
/**
* Save a collection of logs on a <code>BufferedWriter</code>.
* <P>
* The buffer must be initialized and terminated i.e. the <code>prepareSaveFile</code>
* and the <code>terminateSave</code> are not executed by this method.
*
* @param outBuf The buffer to write logs into
* @param logs The non empty collection of logs to save
* @param progressListener The listener to be notified about the number of bytes written
* @throws IOException In case of error writing
*/
public synchronized void saveLogs(
BufferedWriter outBuf,
Iterator<ILogEntry> iterator,
IOPorgressListener progressListener) throws IOException {
if (iterator==null || !iterator.hasNext()) {
throw new IllegalArgumentException("No logs to save");
}
if (progressListener==null) {
throw new IllegalArgumentException("The progress listener can't be null");
}
stopped=false;
long len=0;
int logsWritten=0;
while (iterator.hasNext() && !stopped) {
ILogEntry log = iterator.next();
len+=saveLog(outBuf, log);
progressListener.bytesWritten(len);
progressListener.logsWritten(++logsWritten);
}
}
/**
* Call this method if you wish to interrupt a load or a save.
* <P>
* Load and save are executed synchronously so this method has to be called by a separate
* thread.
* A typical example is the "Abort" button of a dialog: when the user presses such a button, this method
* is invoked by the swing thread.
*/
public void stopIO() {
stopped=true;
}
}