/* * 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.File; import java.io.IOException; import java.io.RandomAccessFile; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.Random; import java.util.Set; import java.util.TreeMap; import java.util.Vector; import com.cosylab.logging.engine.log.ILogEntry; import com.cosylab.logging.engine.log.LogEntry; import com.cosylab.logging.engine.log.LogTypeHelper; import com.cosylab.logging.engine.log.ILogEntry.AdditionalData; import com.cosylab.logging.engine.log.LogField; /** * This class implements the cache in order to be able to manage * long log files. * It is implemented by a file of logs and a data structure to associate * the position in a file (LogCacheInfo) to each log. * Such data structure is realized by a TreeMap whith the key given by the (progressive, * unique) number of a log. * * Actually it is not possible to remove a log from the file. * To effectively delete logs from disk we need some kind of garbage collector that * removes the logs marked as delete from the file. * This garbage collector is needed to avoid the file on disk grows indefinitely. * * Another potential problem is given by the integer identifier of the logs that * grows indefinitely. A temporary solution could be that of using long instad of integers. * * @author acaproni * */ public class LogFileCache implements ILogMap { /** * An entry in the cache on disk. * * We need this object in order to be able to delete log from the cache. * * @author acaproni * */ protected static class LogCacheInfo { /** * Starting position in the file */ public long start; /** * The length of the string in the file */ public int len; /** * Create LogCacheInfo marked as not deleted * * @param startPos The starting position in the file on disk * @param length The length of the string the file on disk */ public LogCacheInfo(long startPos, int length) { if (startPos<0 || length<=0) { throw new IllegalArgumentException("Invalid positions for log [start="+startPos+", len="+length+"]"); } start=startPos; len=length; } /** * Empty constructor */ public LogCacheInfo() { } } /** * The name of the log file of the cache * This file is filled of logs when they arrive from the notification channel or by reading an input file * The file will be destroyed when the object is destroyed. */ private String logFileName; /** * The file of logs is accessed in a random way * (the positions are stored in the index) */ protected RandomAccessFile file=null; /** * Each log is identified by a unique integer used as key to get the log from the file * (throw the index data structure) logID is incremented whenever a new log is added * * NOTE: in this implementation this value is incremented without taking * care of the logs deleted. */ protected int logID=0; /** * The index of the log is a SortedMap having the number identifying a log as key. * Each entry is a LogCacheInfo containing the starting and ending position of the log in the file */ protected TreeMap<Integer,LogCacheInfo> index = new TreeMap<Integer,LogCacheInfo>(); /** * The buffer to build the logs to write in cache */ private StringBuilder sb=new StringBuilder(); /** * The separator between the field of the logs written in cache */ private static final String SEPARATOR = ""+((char)0); /** * The logs replaced (for example the logs with some info added) * They are usually a few so we keep them in memory */ protected HashMap<Integer,ILogEntry> replacedLogs = new HashMap<Integer,ILogEntry>(); /** * The name of each file of cache is generated by appending a random number */ private static final Random rndNumberGenerator = new Random(System.currentTimeMillis()); /** * * @return The number of logs in cache */ public synchronized int getSize() { int size; synchronized(index) { size=index.size(); } return size; } /** * Return the length of the file on disk * * @return The size of the file cache * * @throws IOException in case of I/O error * @see java.io.RandomAccessFile */ public synchronized long getFileSize() throws IOException{ return (file==null) ? 0 : file.length(); } /** * Init the file where the cache stores the logs * If the file already exists it is truncated to 0 length * this situation might happen whenever the cache is cleared * * @throws IOException */ protected void initCache() throws IOException { // The temporary file if (file==null) { file = new RandomAccessFile(getFile(),"rw"); } file.setLength(0); // Clear the index synchronized(index) { index.clear(); } // Clear the map of the replaced logs synchronized(replacedLogs) { replacedLogs.clear(); } } /** * Create the file for the cache trying in several places * before giving up. * * @ return The name of the file for the temporary log file * @throws IOException If an error happened while creating the new file */ private String getFile() throws IOException { String name=null; File f=null; // This does not work because the file is created into a // $ACSDATA/tmp/ACS_INSTANCE.x that might be destroyed outside the control // of jlog (for example acsStop). // I have commented out this line for the time being //name = FileHelper.getTempFileName(null,"jlog"+Math.abs(random)+".tmp"); try { // Try to create the cache in $ACS_TMP String acsdata = System.getProperty("ACS.tmp"); if (!acsdata.endsWith(File.separator)) { acsdata=acsdata+File.separator; } File dir = new File(acsdata); f = File.createTempFile("jlog",".tmp",dir); } catch (IOException ioe) { // Another error :-O String homeDir = System.getProperty("user.dir"); File homeFileDir = new File(homeDir); if (homeFileDir.isDirectory() && homeFileDir.canWrite()) { do { // Try to create the file in the home directory int random = rndNumberGenerator.nextInt(); name = homeDir + File.separator+"jlog"+random+".jlog"; f = new File(name); } while (f.exists()); } else { // If for any reason the home folder was not writable then try // to get a system temporary file f= File.createTempFile("jlog",".tmp"); name=f.getAbsolutePath(); } } logFileName=f.getAbsolutePath(); f.deleteOnExit(); f=null; return logFileName; } /** * Empty the cache. * * @throws IOException */ public synchronized void clear() throws LogCacheException { synchronized(index) { index.clear(); } logID=0; if (file==null) { return; } try { file.close(); if (logFileName!=null) { File f = new File(logFileName); if (!f.delete()) { throw new LogCacheException("File.delete() could not delete "+logFileName); } } } catch (IOException e) { throw new LogCacheException("Error clearing cache file "+logFileName,e); } finally { file=null; logFileName=null; } } /** * Return the string representation (XML) of the log in position idx * The string is read from the temporary file * * @param idx The position of the log; valid position are between 0 and size-1 * @return A String representing the log in the given position * @throws IOException If in case of error seekeing or reading from file * @throws LogCacheException if the number of bytes read is less then expected */ private String getLogAsString(int idx) throws IOException, LogCacheException { if (idx<0 || idx>=logID) { throw new IndexOutOfBoundsException("Index out of bounds: "+idx); } LogCacheInfo info=null; // Get the position of the log in the file synchronized(index) { info = index.get(idx); } // Allocate the string byte buffer[] = new byte[info.len]; // Move to the starting of the log and read the log // // This operation must be performed in mutual exclusion // because other thread can access the cache in the same moment // (one of them might be the load) synchronized(file) { file.seek(info.start); int bytesRead=file.read(buffer); if (bytesRead!=info.len) { throw new LogCacheException("Error reding from file: got "+bytesRead+" instead of "+info.len); } } return new String(buffer); } /** * Return the log in the given position * * @param pos The position of the log * @return The LogEntryXML or null in case of error */ public synchronized ILogEntry getLog(Integer key) throws LogCacheException { // Some check about the key if (key<0 || key>=logID) { throw new LogCacheException("Key "+key+" out of range [0,"+logID+"["); } // Check if the log is in the index if (!index.containsKey(key)) { throw new LogCacheException("A log with the given key ("+key+") has not been found in the index"); } // Check if the log is present in the list of the replaced logs synchronized(replacedLogs) { if (replacedLogs.containsKey(key)) { return replacedLogs.get(key); } } String logStr = null; try { logStr =getLogAsString(key); } catch (IOException ioe) { throw new LogCacheException("Exception getting log from cache"); } try { return fromCacheString(logStr); } catch (Exception e) { System.err.println("Exception "+e.getMessage()); throw new LogCacheException("Exception parsing a log [logStr.len="+logStr.length()+", log={"+logStr+"}]",e); } } /** * Append a log to the end of the cache * * @param log The log to append in the cache * @return The key in the cache of the added log */ public synchronized int add(ILogEntry log) throws LogCacheException { if (log==null) { throw new LogCacheException("Trying to add a null log!"); } if (file==null) { try { initCache(); } catch (IOException e) { throw new LogCacheException("Error initializing the cache",e); } } String cacheLogStr=toCacheString(log); long startingPos; synchronized(file) { try { startingPos=file.length(); file.seek(file.length()); file.writeBytes(cacheLogStr); } catch (IOException ioe) { throw new LogCacheException("Error adding a log",ioe); } } LogCacheInfo info = new LogCacheInfo(startingPos, cacheLogStr.length()); synchronized(index) { index.put(logID,info); } return logID++; } /** * Replace the log in the given position with the new one * NOTE: the new log is kept in memory * @param position The position of the log to replace * @param log The key (identifier) ot the log */ public void replaceLog(Integer key, ILogEntry log) throws LogCacheException { if (!index.containsKey(key)) { throw new LogCacheException("Trying to replace a log not in cache"); } synchronized(replacedLogs) { replacedLogs.put(key,log); } } protected String toCacheString(ILogEntry log) { sb.delete(0,sb.length()); for (LogField field: LogField.values()) { Object obj = log.getField(field); if (obj!=null) { if (obj instanceof Date) { sb.append(((Date)obj).getTime()); } else if (obj instanceof LogTypeHelper ) { sb.append(((LogTypeHelper)obj).ordinal()); } else { sb.append(obj.toString()); } } sb.append((char)0); } if (log.hasDatas()) { Vector<ILogEntry.AdditionalData> datas = log.getAdditionalData(); for (int t=0; t<datas.size(); t++) { ILogEntry.AdditionalData data = datas.get(t); sb.append(data.name); sb.append(SEPARATOR); sb.append(data.value); sb.append(SEPARATOR); } } return sb.toString(); } private ILogEntry fromCacheString(String str) { String[] strs = str.split(SEPARATOR); Long millis = new Long(strs[0]); Integer entrytype = new Integer(strs[1]); String srcObject = null; if (strs.length>2) { srcObject=strs[2]; } String fileNM = null; if (strs.length>3) { fileNM=strs[3]; } Integer line = null; if (strs.length>4 && strs[4].length()!=0) { line =new Integer(strs[4]); } String routine = null; if (strs.length>5) { routine=strs[5]; } String host = null; if (strs.length>6) { host=strs[6]; } String process = null; if (strs.length>7) { process=strs[7]; } String context = null; if (strs.length>8) { context=strs[8]; } String thread = null; if (strs.length>9) { thread=strs[9]; } String logid = null; if (strs.length>10) { logid=strs[10]; } Integer priority = null; if (strs.length>11 && strs[11].length()>0) { priority=new Integer(strs[11]); } String uri = null; if (strs.length>12) { uri=strs[12]; } String stackid = null; if (strs.length>13) { stackid=strs[13]; } Integer stacklevel = null; if (strs.length>14 && strs[14].length()>0) { stacklevel =Integer.parseInt(strs[14]); } String logmessage = null; if (strs.length>15) { logmessage=strs[15]; } String audience = null; if (strs.length>16) { audience=strs[16]; } String array = null; if (strs.length>17) { array=strs[17]; } String antenna = null; if (strs.length>18) { antenna=strs[18]; } Vector<ILogEntry.AdditionalData> addDatas = null; if (strs.length>LogField.values().length) { addDatas = new Vector<ILogEntry.AdditionalData>(); for (int t=LogField.values().length; t<strs.length; t+=2) { addDatas.add(new AdditionalData(strs[t],strs[t+1])); } } return new LogEntry( millis, entrytype, fileNM, line, routine, host, process, context, thread, logid, priority, uri, stackid, stacklevel, logmessage, srcObject, audience, array, antenna, addDatas); } /** * Delete a log * The log is marked as deleted and moved from the index to * the deleteLogIndex. * The log still exists in the file but is not accessible. * * @param pos The key of the log to remove */ public synchronized void deleteLog(Integer key) throws LogCacheException { if (key<0 || key>=logID) { throw new LogCacheException("Key "+key+" out of range [0,"+logID+"["); } if (!index.containsKey(key)) { throw new LogCacheException("The log "+key+" is not in cache [0,"+logID+"["); } // Remove the log from the index synchronized (index) { index.remove(key); } // Remove the log from the replacedLogs synchronized(replacedLogs) { replacedLogs.remove(key); } } /** * Delete a set of logs * * @param keys The keys of logs to delete */ public synchronized void deleteLogs(Collection<Integer> keys) throws LogCacheException { if (keys==null) { throw new IllegalArgumentException("Invalid null parameter"); } for (Integer key: keys) { deleteLog(key); } } /** * Return the key of the first valid log (FIFO). * The key of the first log is 0 but it can change if the log 0 has * been deleted. * * @return The key of the first log * null if the cache is empty */ public Integer getFirstLog() { synchronized(index) { if (index.size()==0) { return null; } return index.firstKey(); } } /** * Append at most n keys from the first valid logs to the collection. * First here means first in the FIFO policy. * * The number of added keys can be less then n if the cache doesn't * contain enough logs. * * @param n The desired number of keys of first logs * @param keys The collection to add they keys to * @return The number of keys effectively added */ public int getFirstLogs(int n, Collection<Integer> keys) { if (n<=0 || keys==null) { throw new IllegalArgumentException("Invalid number of requested key or null collection"); } int ret=0; Set<Integer> allTheKeys = index.keySet(); Iterator<Integer> iter = allTheKeys.iterator(); while (iter.hasNext() && ret<n) { keys.add(iter.next()); ret++; } return ret; } /** * Return the key of the last valid log (FIFO) * The key of the last log is the key of the last inserted log * but it can cheang if such log has been deleted * * @return The key of the last inserted log * null if th cache is empty */ public Integer getLastLog() { synchronized(index) { if (index.size()==0) { return null; } return index.lastKey(); } } /** * Return a set with all the keys of the logs in cache * * @return The keys of the logs in cache */ public Set<Integer> keySet() { return index.keySet(); } /** * Return an iterator over the logs in cache */ public Iterator<ILogEntry> iterator() { return new LogIterator(this); } }