/*
* 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 com.cosylab.logging.client.cache;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import com.cosylab.logging.engine.log.ILogEntry;
import com.cosylab.logging.engine.log.LogTypeHelper;
import com.cosylab.logging.engine.log.LogField;
/**
* The class extends the cache on file keeping a set of logs in memory to avoid to access
* the file for the most frequently accessed logs
* <P>
* The cache stores the logs into an HashMap using their indexes as keys.
* It also stores an array of times and level of logs to speed up the sorting
* done by the table.
*
* @author acaproni
*
*/
public class LogCache extends LogMultiFileCache implements ILogMap {
/**
* The name of the property to set the size of the cache.
* This is the size oif the logs buffered by LogCache
*/
public static final String CACHESIZE_PROPERTY_NAME = "jlog.cache.size";
/**
* The default size of the buffer of logs
*/
public static final int DEFAULT_CACHESIZE = 16384;
/**
* The size of the buffer of logs
*/
private final int actualCacheSize;
/**
* The logs are stored into an HashMap.
* The key is the index of the log.
* This choice make inefficient the deletion of a log.
* In fact when a log is removed, the logs having a greater position
* are shifted one position toward 0 (newPos=oldPos-1) but it is not
* possible to change the key of an entry in the HashMap.
*
*/
private final Map<Integer,ILogEntry> cache=Collections.synchronizedMap(new HashMap<Integer,ILogEntry>());;
/**
* The following list is used to keep ordered the indexes
* in such a way it is fast to insert/remove logs in the cache.
* <P>
* The indexes contained in this object are the indexes in the cache TreeMap
* and the size of the TreeMap and the ArrayBlockingQueue is always the same.
* <BR>
* The functioning is the following:
* <UL>
* <LI> new elements are added in the tail
* <LI> old elements are removed from the head
* <LI> whenever an elements is accessed it is moved of one position
* toward the tail reducing the chance to be removed
* The moving operation is performed swapping the accessed elements
* with its neighborhood
* </UL>
*/
private final LinkedList<Integer> manager = new LinkedList<Integer>();
/**
* The array with the level of each log in the cache
* (useful for setting the log level)
*/
private final Map<Integer,LogTypeHelper> logTypes= Collections.synchronizedMap(new HashMap<Integer,LogTypeHelper>());
/**
* The times of the logs
* Integer is the key of the log, Long is its timestamp
*/
private final Map<Integer,Long> logTimes= Collections.synchronizedMap(new HashMap<Integer,Long>());
/**
* Build a LogCache object
*
* @throws IOException The exception is thrown if the base class
* is not able to create the cache on a file
*/
public LogCache() throws LogCacheException {
this(getDefaultCacheSize());
}
/**
* Build a logCache object of the given size
*
* @param size The size of the cache
* @throws LogCacheException
*/
public LogCache(int size) throws LogCacheException {
super();
if (size<=0) {
throw new LogCacheException("Invalid initial size: "+size);
}
actualCacheSize = size;
System.out.println("Jlog will use cache for " + actualCacheSize + " log records.");
}
/**
* Adds a log in the cache.
* It does nothing because the adding is done by its parent class.
* What it does is to store the level and time of the log in the arrays
*
* @param log The log to add in the cache
* @return The key of the added log in the cache
* @throws LogCacheException If an error happened while adding the log
*/
public synchronized int add(ILogEntry log) throws LogCacheException {
Integer key = super.add(log);
logTypes.put(key,(log.getType()));
logTimes.put(key,(Long)log.getField(LogField.TIMESTAMP));
return key;
}
/**
*
* @param pos The key of the log
* @return The type of the log with the given key
*/
public LogTypeHelper getLogType(Integer key) throws LogCacheException {
if (!logTypes.containsKey(key)) {
throw new LogCacheException("Error: getting the type of a deleted log "+key);
}
return logTypes.get(key);
}
/**
*
* @param pos The key of the log
* @return The timestamp of the log with the given key
*/
public Long getLogTimestamp(Integer key) throws LogCacheException {
if (!logTimes.containsKey(key)) {
throw new LogCacheException("Error: getting the time of a deleted log "+key);
}
return logTimes.get(key);
}
/**
* Gets the default cache size, which comes either from the system property
* <code>jlog.cache.size</code> (see {@link #CACHESIZE_PROPERTY_NAME})
* or, if this property is undefined or invalid, from the fixed size given by
* {@link #DEFAULT_CACHESIZE}.
* @return the default cache size to be used if none is given in the constructor
*/
private static int getDefaultCacheSize() {
Integer cacheSizeFromProperty = Integer.getInteger(CACHESIZE_PROPERTY_NAME);
if (cacheSizeFromProperty != null) {
return cacheSizeFromProperty.intValue();
}
else {
return DEFAULT_CACHESIZE;
}
}
/**
* Return the log with the given key.
* The method is synchronized because both the HashMap and
* the LinkedList must be synchronized if there is a chance
* to acces these objects from more then one thread in the same
* time
* @see java.util.LinkedList
* @see java.util.HashMap
*
* @param pos The key of the log
* @return The LogEntryXML or null in case of error
*/
public synchronized ILogEntry getLog(Integer key) throws LogCacheException {
ILogEntry log;
log = cache.get(key);
if (log!=null) {
// Hit! The log is in the cache
return log;
} else {
// Oops we need to read a log from disk!
return loadNewLog(key);
}
}
/**
* Get a log from the cache on disk updating all the
* internal lists
*
* @param idx The position of the log
* @return The log read from the cache on disk
*/
private synchronized ILogEntry loadNewLog(Integer idx) throws LogCacheException {
// Read the new log from the cache on disk
ILogEntry log = super.getLog(idx);
// There is enough room in the lists?
if (cache.size()==actualCacheSize) {
// We need to create a room for the new element
Integer itemToRemove = manager.removeFirst();
cache.remove(itemToRemove);
}
// Add the log in the cache
cache.put(idx,log);
// Append the index in the manager list
manager.addLast(idx);
return log;
}
/**
* Empty the cache
*
*/
public synchronized void clear() throws LogCacheException {
cache.clear();
manager.clear();
logTypes.clear();
logTimes.clear();
super.clear();
}
/**
* Gets the actual cache size, which may come from {@link #getDefaultCacheSize()} or
* may be given in the constructor.
* @return
*/
public int getCacheSize() {
return actualCacheSize;
}
/**
* Calculate and return the time frame of the logs managed by the GUI
* The time frame is the number of hours/minutes/seconds between the
* newest and the oldest log in the GUI
*
* I prefer to evaluate the frame instead of storing the min and max value
* because it works even if one log is deleted from the cache.
*
* @return The time frame
*/
public synchronized Calendar getTimeFrame() {
Calendar cal = Calendar.getInstance();
long min=Long.MAX_VALUE;
long max=-1;
int len =logTimes.size();
if (len<=1) {
cal.setTimeInMillis(0);
return cal;
}
Collection<Long> times = logTimes.values();
for (Long time : times) {
if (time>max) {
max=time;
}
if (time<min) {
min=time;
}
}
cal.setTimeInMillis(max-min);
return cal;
}
/**
* Delete a log with the given key
*
* @param pos The key of the log to delete
*/
public synchronized void deleteLog(Integer key) throws LogCacheException {
if (cache.containsKey(key)) {
cache.remove(key);
manager.remove(key);
}
logTimes.remove(key);
logTypes.remove(key);
super.deleteLog(key);
}
/**
* Delete a collection of logs
*
* @param keys The keys of the logs to delete
*/
public void deleteLogs(final Integer[] keys) throws LogCacheException {
Thread t = new Thread("LogCache.deleteLogs") {
public void run() {
for (Integer key: keys) {
try {
deleteLog(key);
} catch (Throwable t) {
System.err.println("Error removing log["+key+"] from LogCache: "+t.getMessage());
}
}
}
};
t.setDaemon(true);
t.start();
}
/**
* Returns a set of number of logs (i.e. their position in cache)
* exceeding the given time frame.
* This operation is quite slow because it requires a double scan of the
* logTimes array
*
* @param timeframe The time frame to check in millisecond
* @return A collection of number of logs exceedding the given timeframe
*/
public synchronized Collection<Integer> getLogExceedingTimeFrame(long timeframe) {
// Look for the newest log
// We can't assume the oldest is the latest inserted log because the user
// is allowed to load logs from different sources at any time.
long newestTime=-1;
Collection<Long> times;
times = logTimes.values();
for (Long time: times) {
if (time>newestTime) {
newestTime=time;
}
}
long limit = newestTime-timeframe;
ArrayList<Integer>ret = new ArrayList<Integer>();
Set<Integer> keys = logTimes.keySet();
for (Integer key: keys) {
if (logTimes.get(key)>limit) {
ret.add(key);
}
}
return ret;
}
/**
* The keys in the map
*
* @return The keys in the map
*/
public Set<Integer> keySet() {
return logTimes.keySet();
}
/**
* Return an iterator over the logs in cache
*/
public Iterator<ILogEntry> iterator() {
return new LogIterator(this);
}
}