/*
* ALMA - Atacama Large Millimiter Array
* (c) European Southern Observatory, 2002
* Copyright by ESO (in the framework of the ALMA collaboration)
*
* 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.table;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import javax.swing.JOptionPane;
import javax.swing.table.AbstractTableModel;
import alma.acs.gui.util.threadsupport.EDTExecutor;
import alma.acs.logging.table.reduction.LogProcessor;
import com.cosylab.logging.LoggingClient;
import com.cosylab.logging.client.cache.LogCache;
import com.cosylab.logging.client.cache.LogCacheException;
import com.cosylab.logging.engine.audience.Audience.AudienceInfo;
import com.cosylab.logging.engine.log.ILogEntry;
import com.cosylab.logging.engine.log.LogField;
/**
* The table model with basic functionalities like add logs and remove all the logs.
* The model can be bounded to a max number of logs (by default unlimited).
* <P>
* Each log in the model is identified by a integer key; the association
* between keys and logs is done by the {@link #allLogs}.
* {@link #rows} represents a entry in the table i.e. a log identified by its key.
* So to get a log in a row, we must first get its key and then get the log from the cache.
* <BR> This indirection allows to avoid keeping all the logs in memory
* The list of keys ({@link #rows}) and the map ({@link #allLogs}) must be consistent
* in the sense that the map must always contain the keys of all the logs in the table
* but it can have more keys. For example if a log is removed from the table
* its key can be removed from the map at a latter time.
* <P>
* The model is modified inside the EDT. The consistency between
* ({@link #rows}) and the map ({@link #allLogs}) is also obtained by
* modifying their contents inside the EDT.
* {@link EDTExecutor} is used to ensure to access the model from inside the EDT.
* <P>
* To reduce the overload refreshing the content of the table when a lot of logs
* have been added or removed, the refresh is triggered only after a certain amount
* of time by a dedicated thread (see {@link #run()}).
* <P>
* When a log is deleted, it key is immediately removed from the List of logs ({@link #rows}
* but the key is still in the cache ({@link #allLogs}). A dedicated thread, {@link KeysDeleter},
* is in charge of removing unused keys. This operation can be safely done in a non-EDT
* thread.
* <P>
* <CODE>LogEntryTableModelBase</CODE> can be reused by log tables with basic functionalities
* like the error browsers.
* <P>
* TODO: manage threads with a {@link ScheduledThreadPoolExecutor}
*
* @author acaproni
*
*/
public class LogEntryTableModelBase extends AbstractTableModel implements Runnable {
/**
* The class to delete the keys in a dedicated thread.
* <P>
* Keys to remove are added only by {@link LogEntryTableModelBase#removeExceedingLogs()} inside the EDT.
*
* @author acaproni
*
*/
private class KeysDeleter implements Runnable {
/**
* If closed then new keys are rejected and the thread terminates
*/
private volatile boolean closed=false;
/**
* The cache of keys to be deleted by the thread.
* <P>
* {@link ArrayList} seems the best choice considering that we need only 3
* operations: add, get and clear.
*/
private final List<Integer> keysToDelete = Collections.synchronizedList(new ArrayList<Integer>());
/**
* Invalidate the list that will be cleared.
* <P>
* This method is needed when the cache is cleared to avoid removing the
* same keys more then once
*
* @see LogEntryTableModelBase#clearAll()
*/
public void invalidate() {
keysToDelete.clear();
}
/**
* Add a key to the collection of keys to delete.
*
* @param key The not <code>null</code> key to delete
*/
public void scheduleForDeletion(Integer key) {
if (key==null) {
throw new IllegalArgumentException("Adding a null key is not allowed");
}
if (closed) {
return;
}
keysToDelete.add(key);
}
/**
* Add the keys of the passed List
* to the collection of keys to delete.
*
* @param keys The not <code>null</code> keys to delete
*/
public void scheduleForDeletion(List<Integer> keys) {
if (keys==null) {
throw new IllegalArgumentException("Adding a null key is not allowed");
}
if (closed) {
return;
}
keysToDelete.addAll(keys);
}
/**
* The thread to remove the keys
*/
@Override
public void run() {
while (!closed) {
try {
Thread.sleep(KEY_DELETION_INTERVAL);
} catch (InterruptedException ie) {
continue;
}
if (keysToDelete.isEmpty()) {
continue;
}
// Make a local copy to avoid blocking the EDT
List<Integer> temp;
synchronized(keysToDelete) {
temp= new ArrayList<Integer>(keysToDelete);
keysToDelete.clear();
}
for (Integer keyToRemove: temp) {
try {
allLogs.deleteLog(keyToRemove);
} catch (Throwable t) {
// An exception removing the key:
// there is nothing to do so we print out a message and continue
System.err.println("Error deleting a key from the cache of logs "+keyToRemove);
t.printStackTrace(System.err);
}
}
temp.clear();
}
keysToDelete.clear();
}
/**
* Terminates the computation
*/
public void close() {
closed=true;
}
}
/**
* Signal the thread to terminate
*/
private volatile boolean terminateThread=false;
/**
* The interval (msec) between two refreshes of the content
* of the table
*/
private static final int UPDATE_INTERVAL=2000;
/**
* The interval of time to remove the keys
* from the cache (msec)
*/
private static final int KEY_DELETION_INTERVAL = 60000;
/**
* The vector of logs to add in the rows.
* <P>
* Newly arrived logs are appended to this vector and flushed into
* <code>rows</code> by the <code>TableUpdater</code> thread.
* <P>Newest logs are in the tail; oldest logs in the head.
*/
private final List<ILogEntry> rowsToAdd = Collections.synchronizedList(new Vector<ILogEntry>());
/**
* The processor to reduce the logs
*/
private final LogProcessor logProcessor = new LogProcessor();
/**
* The cache of all the logs received.
*/
protected LogCache allLogs;
/**
* Each row of the table shows a log identified by a key returned by the cache.
* <P>
* This vector stores the key of each log in the table.
*
*/
protected final RowEntries rows = new RowEntries();
/**
* The thread to refresh the content of the table
*/
protected Thread tableUpdater;
/**
* It deletes unused key from the cache
*/
private final KeysDeleter keysDeleter = new KeysDeleter();
/**
* The thread to delete unused keys from the cache
* every {@link #KEY_DELETION_INTERVAL} time interval
*
* @see KeysDeleter
*/
private Thread keysDeleterThread=null;
/**
* The LoggingClient that owns this table model
*/
protected final LoggingClient loggingClient;
/**
* <code>true</code> if the model has been closed
*/
private volatile boolean closed=false;
/**
* HTML open TAG for the string in the table
*/
private static final String htmlStartTAG="<html>";
/**
* HTML close TAG for the string in the table
*/
private static final String htmlCloseTAG="</html>";
/**
* The empty string is returned when a object is <code>null</code>
*/
private static final String emptyString="";
/**
* The max number of logs in the table.
* <P>
* A value of 0 means unlimited.
*/
private volatile int maxLog=0;
/**
* Constructor
*/
public LogEntryTableModelBase(LoggingClient client) throws Exception {
if (client==null) {
throw new IllegalArgumentException("Invalid null LoggingClient");
}
this.loggingClient=client;
try {
allLogs = new LogCache();
} catch (LogCacheException lce) {
System.err.println("Exception instantiating the cache: "+lce.getMessage());
lce.printStackTrace(System.err);
throw new Exception("Exception instantiating the cache: ",lce);
}
}
/**
* Start the thread to update the table model.
*
* @see {@link LogEntryTableModelBase#tableUpdater}
*/
public void start() {
tableUpdater=new Thread(this,"LogEntryTableModelBase.TableUpdaterThread");
tableUpdater.setDaemon(true);
tableUpdater.start();
keysDeleterThread = new Thread(keysDeleter,"LogEntryTableModelBase.KeysDeleter");
keysDeleterThread.setDaemon(true);
keysDeleterThread.start();
}
/**
* Return number of columns in table
*/
@Override
public final int getColumnCount() {
return LogField.values().length+1;
}
/**
* @return The number of files used by the cache
*/
public int numberOfUsedFiles() {
return allLogs.getNumberOfCacheFiles();
}
/**
*
* @return The amount of disk space used by the files of the cache
* @throws IOException In case of error getting the size of at least
* one of the used cache files
*/
public long usedDiskSpace() throws IOException {
return allLogs.getFilesSize();
}
/**
* @see javax.swing.table.TableModel#getRowCount()
*/
@Override
public int getRowCount() {
return rows.size();
}
/**
* Returns an item according to the row and the column of its position.
*
* @return java.lang.Object
* @param row int
* @param column int
*/
@Override
public Object getValueAt(int row, int column) {
switch (column) {
case 1: {// TIMESTAMP
try {
return new Date(allLogs.getLogTimestamp(rows.get(row)));
} catch (Exception e) {
// This can happen because deletion of logs is done asynchronously
return null;
}
}
case 2: { // ENTRYTYPE
try {
return allLogs.getLogType(rows.get(row));
} catch (Exception e) {
// This can happen because deletion of logs is done asynchronously
return null;
}
}
default: {
ILogEntry log=getVisibleLogEntry(row);
if (log==null) {
return null;
}
if (column == 0) {
return Boolean.valueOf(log.hasDatas());
} else {
Object val=log.getField(LogField.values()[column-1]);
if (val==null || val.toString()==null || val.toString().isEmpty()) {
return emptyString;
}
// Format the string as HTML
String ret=val.toString().replaceAll("<","<");
ret=ret.replaceAll(">",">");
ret=ret.replaceAll("\n", "<BR>");
return htmlStartTAG+ret+htmlCloseTAG;
}
}
}
}
/**
* Return the log shown in the passed row.
* <P>
* This must be called inside the EDT.
*
* @param row The row of the table containing the log
* @return The log in the passed row
*/
public ILogEntry getVisibleLogEntry(int row) {
if (closed) {
return null;
}
try {
ILogEntry ret;
ret=allLogs.getLog(rows.get(row));
return ret;
} catch (LogCacheException e) {
e.printStackTrace();
return null;
}
}
/**
* Remove all the logs.
*/
public void clearAll() {
// Remove the logs waiting to be inserted
EDTExecutor.instance().execute(new Runnable() {
public void run() {
rowsToAdd.clear();
rows.clear();
keysDeleter.invalidate();
try {
allLogs.clear();
} catch (Throwable t) {
System.err.println("Exception caught clearing the cache: "+t.getMessage());
t.printStackTrace(System.err);
JOptionPane.showInternalMessageDialog(loggingClient, "Error clearing the cache.", "Error clearing the cache", JOptionPane.ERROR_MESSAGE);
}
fireTableDataChanged();
}
});
}
/**
* Return the number of logs in cache
* without those waiting to be added in {@link TableUpdater#rowsToAdd}.
*
* @return The number of logs in cache
*/
public long totalLogNumber() {
return rows.size();
}
/**
* @return The time frame of the log in cache
* @see com.cosylab.logging.client.cache.LogCache
*/
public Calendar getTimeFrame() {
return allLogs.getTimeFrame();
}
/**
* Return the key of the log in the given position of the
* vector of keys.
* <P>
* There are several cases that forbids to retrieve the key in the given position,
* in such a situations the method return <code>null</code>.
* One typical situation is when the entry has been deleted by the <code>LogDeleter</code>.
*
* @param index The position in the model of the key
* @return The key in the passed position or
* <code>null</code> if it is not possible to return the key
*
* @see findKeyPos(Integer key)
*/
public Integer getLogKey(int index) {
return rows.get(index);
}
/**
* Return the position of the key in the vector.
* <P>
* There are cases when the key is not anymore in the vector and in such situations
* this method return <code>null</code>.
* <BR>For example it could happen if the log has been deleted by the <code>LogDeleter</code>.
*
* @param key The key whose position in the vector has to be found
* @return The position of the key in the vector of logs or
* <code>-1</code> if the key is not in the vector
*/
public int findKeyPos(Integer key) {
if (key==null) {
throw new IllegalArgumentException("The key can't be null");
}
return rows.indexOf(key);
}
/**
* Returns name of the column based LogEntryXML fields.
* If the specified index does not return a valid column, blank string is returned.
* Creation date: (11/11/2001 13:50:16)
* @return java.lang.String
* @param columnIndex int
*/
public final String getColumnName(int columnIndex) {
if (columnIndex == 0 ) {
return "";
}
columnIndex=columnIndex-1;
return (columnIndex>=0 && columnIndex<LogField.values().length) ?
LogField.values()[columnIndex].getName() : "";
}
/**
* Returns default class for column.
* Creation date: (12/1/2001 14:18:53)
* @return java.lang.Class
* @param column int
*/
public final Class<?> getColumnClass(int column) {
if (column == 0) {
return Boolean.class;
} else {
int col=column-1;
if (col>=0 && col<LogField.values().length) {
return LogField.values()[col].getType();
}
return String.class;
}
}
/**
* Replace a log entry with another.
*
* @param pos The position in the cache of the log to replace
* @param newEntry The new LogEntryXML
*/
public void replaceLog(final int pos, final ILogEntry newEntry) {
EDTExecutor.instance().execute(new Runnable() {
@Override
public void run() {
// Replace the entry in the list of all the logs (allLogs)
try {
allLogs.replaceLog(pos,newEntry);
} catch (LogCacheException e) {
System.err.println("Error replacing log "+pos);
e.printStackTrace();
}
}
});
}
/**
* Closes all the threads and frees the resources
* This is the last method to call before closing the application
* @param sync If it is true wait the termination of the threads before returning
*/
public void close(boolean sync) {
terminateThread=true;
keysDeleter.close();
closed=true;
if (tableUpdater!=null) {
tableUpdater.interrupt();
if (sync) {
try {
// We do not want to wait forever..
tableUpdater.join(10000);
if (tableUpdater.isAlive()) {
System.err.println("LogEntryTableModelBase.TableUpdater thread still alive!");
}
} catch (InterruptedException ie) {}
}
}
clearAll();
if (keysDeleterThread!=null) {
keysDeleterThread.interrupt();
if (sync) {
try {
// We do not want to wait forever..
keysDeleterThread.join(10000);
if (keysDeleterThread.isAlive()) {
System.err.println("LogEntryTableModelBase.KeysDeleter thread still alive!");
}
} catch (InterruptedException ie) {}
}
}
}
/**
* Adds a log to {@link TableUpdater#rowsToAdd} ready to be flushed
* in the table at the next iteration.
* <P>
* To avoid updating the table very frequently, the logs to add are immediately
* inserted in the <code>LogCache</code> but their insertion in the table is delayed
* and done by the <code>TableUpdater</code> thread.
* <BR>
* For this reason each log is inserted in the temporary vector <code>rowsToAdd</code>
* that will be flushed into <code>rows</code> by the thread.
*
* @param log The log to add
*/
public void appendLog(ILogEntry log) {
if (log==null) {
throw new IllegalArgumentException("Can't append a null log to the table model");
}
if (!closed) {
rowsToAdd.add(log);
}
}
/**
* Flush the logs from the temporary vector into the table.
* <P>
* New logs are appended in the temporary vector <code>rowsToAdd</code> to limit
* the frequency of updating the table model.
* This method flushes the logs from the temporary vector into the model vector
* (<code>rows</code>).
*
* @return <code>true</code> if at least one log has been added to the model
*/
private void flushLogs() {
// rowsToAdd is reduced then copied into temp:
// it is possible to append logs again without waiting for the flush
// i.e. modification of the model inside EDT do not block rowsToAdd
final List<ILogEntry> temp;
synchronized (rowsToAdd) {
if (rowsToAdd.isEmpty()) {
return;
}
// try to apply reduction rules only in OPERATOR mode
try {
if (loggingClient.getEngine().getAudience().getInfo()==AudienceInfo.OPERATOR) {
logProcessor.reduce(rowsToAdd);
}
} catch (Throwable t) {
System.err.println("Exception caught ("+t.getMessage()+")while reducing logs: reduction disabled this time");
t.printStackTrace(System.err);
}
temp=new Vector<ILogEntry>(rowsToAdd);
rowsToAdd.clear();
}
// Add the reduced logs into the model (from inside the EDT)
try {
EDTExecutor.instance().executeSync(new Runnable() {
@Override
public void run() {
for (int t=temp.size()-1; t>=0; t--) {
ILogEntry log=temp.get(t);
Integer key;
try {
key=Integer.valueOf(allLogs.add(log));
} catch (LogCacheException lce) {
System.err.println("Exception caught while inserting a new log entry in cache:");
System.err.println(lce.getLocalizedMessage());
lce.printStackTrace(System.err);
continue;
}
rows.add(key);
}
// Finally notify the change
fireTableRowsInserted(0, temp.size()-1);
}
});
} catch (InvocationTargetException e) {
System.out.println("!!! Exception: "+e.getMessage()+"; model size="+rows.size());
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* Set the max number of logs to keep in cache
* This is the max number of logs stored in cache
* (the visible logs can be less)
*
* @param max The max number of logs
* 0 means unlimited
*/
public void setMaxLog(int max) {
if (max<0) {
throw new IllegalArgumentException("Impossible to set the max log to "+max);
}
maxLog=max;
}
/**
* Check if there are too many logs in the table and if it is the case
* then remove the oldest.
* <P>
* This method must be executed inside the EDT.
*
* @see #maxLog
*/
private void removeExceedingLogs() {
try {
EDTExecutor.instance().executeSync(new Runnable() {
@Override
public void run() {
int sz=rows.size();
if (maxLog<=0 || sz<=maxLog) {
// The model is unbounded or there is still enough room in the model
return;
}
keysDeleter.scheduleForDeletion(rows.removeLastEntries(rows.size()-maxLog));
fireTableDataChanged();
}
});
} catch (InvocationTargetException e) {
System.out.println("!!! Exception: "+e.getMessage()+"; model size="+rows.size());
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
while (!terminateThread) {
try {
Thread.sleep(UPDATE_INTERVAL);
} catch (InterruptedException ie) {
continue;
}
// Flush the logs waiting to be added in the model
try {
flushLogs();
} catch (Throwable t) {
// This thread never fails!
System.err.println("Error in thread "+Thread.currentThread().getName()+": "+t.getMessage());
t.printStackTrace(System.err);
}
// Too many logs? Remove the oldest!
removeExceedingLogs();
}
}
}