/* * ALMA - Atacama Large Millimiter Array (c) European Southern Observatory, 2006 * * 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.util.stringqueue; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.RandomAccessFile; import java.text.ParseException; import java.util.Date; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import alma.acs.util.IsoDateFormat; /** * Each file used by the cache. * <P> * The cache is composed by * <UL> * <LI>a {@link File} to know the length of the file on disc. * <LI>a {@link RandomAccessFile} for reading and writing * </UL> * There are two booleans signaling if the file is used for reading and writing. * In this way we know when the reads and writes are terminated and the file * can be safely closed. * * @author acaproni * */ public class QueueFile { /** * The name of the file */ public final String fileName; /** * The key identifying this file. * <P> * The key is stored only to perform run-time tests of correctness. */ public final Integer key; /** * The <code>RandomAccessFile</code> of the file of this entry. * <P> * This is not <code>null</code> only when used for I/O. */ private RandomAccessFile raFile = null; /** * The file used to build <code>raFile</code>. * <P> * It is not null as soon as raFile is not null */ private File file=null; /** * Signal if the file is used for reading */ private boolean reading=false; /** * Signal if the file is used for writing */ private boolean writing=false; /** * The date of the oldest log in this file in milliseconds */ private long oldestLogDateMillis=0; /** * The date of the youngest log in this file in milliseconds */ private long youngestLogDateMillis=0; /** * The (case insensitive) string to look for in the pushed string while getting the date. * <P> * For example, if the queue is used to store logs, this string is * <code>TIMESTAMP="</code> */ private final String timestampStrTag; /** * All ISO timestamps like ("2014-10-09T14:04:25.984") * have the same size! */ private static final int timeStampLength=23; /** * The number of strings in this file */ private final AtomicLong stringsInFile = new AtomicLong(); /** * Constructor * * @param fName The name of the file * @param key The key of this entry * @param rf The <code>RandomAccessFile</code> used for I/O * @param f The <code>File</code> used to get the length * @param tstampStrTag The string to find the timestamp in each pushed string * @throws IOException In case of error writing in the file * * @see {@link QueueFile(String fName, Integer key)} */ public QueueFile(String fName, Integer key, RandomAccessFile rf, File f, String tstampStrTag) throws IOException { if (fName==null || fName.isEmpty()) { throw new IllegalArgumentException("The file name can't be null not empty"); } if (key==null) { throw new IllegalArgumentException("Invalid null key"); } if (rf==null) { throw new IllegalArgumentException("Invalid null random file"); } if (tstampStrTag==null || tstampStrTag.isEmpty()) { throw new IllegalArgumentException("Invalid timestamp identifier."); } fileName=fName; this.key=key; raFile=rf; file=f; this.timestampStrTag=tstampStrTag.toUpperCase(); } /** * An helper methods that returns the <code>File</code>. * <P> * As soon as <code>raFile</code> is not <code>null</code>, <code>file</code> is not <code>null</code> too. * * A new {@link File} is built if <code>file</code> is <code>null</code> * otherwise the method returns a reference to <code>file</code>. * * @return The file * @throws FileNotFoundException If the file does not exist */ public File getFile() throws FileNotFoundException { if (file!=null) { return file; } File f = new File(fileName); if (!f.exists()) { throw new FileNotFoundException("The cache file "+fileName+" does not exist"); } if (!f.canRead() || !f.canWrite()) { throw new IllegalStateException("Impossible to read/write "+fileName); } file=f; return file; } /** * An helper method that returns a <code>RandomAccessFile</code> by the file name * <code>fileName</code>. * <P> * The random access file is built from the <code>fileName</code>. * * @return The file to read and/or write items * @throws IOException In case of error creating the <code>File</code>. * @throws FileNotFoundException If the file does not exist */ private void openFile() throws FileNotFoundException { raFile = new RandomAccessFile(getFile(),"rw"); } /** * Release all the resources (for instance it releases the random * file). */ public void close() { if (raFile!=null) { try { synchronized (raFile) { raFile.close(); } } catch (Throwable t) { // Nothing to do here: print a message and go ahead. System.err.println("Error closing the file "+fileName+": "+t.getMessage()); } raFile=null; } file=null; } /** * Check if the file is used for reading or writing and * if not used, close the random file. */ private void checkRaFileUsage() { // Release raFile and file if both the file for input // and output are null (unused) if (!reading && !writing) { try { raFile.close(); } catch (Throwable t) { // An error closing the file: do not stop the computation but cross the fingers! System.err.println("Error closing "+fileName+": "+t.getMessage()); } raFile=null; file=null; } } /** * Return the size of the file * * @return the size of the file */ public long getFileLength() { if (file==null) { throw new IllegalStateException("The file is null"); } return file.length(); } /** * Write the passed string in the file. * * @param str The string to write in the file * @return The ending position of the string in the file * * @throws IOException In case of error while performing I/O * @throws StringQueueException In case of error reading the date of the log from str */ public synchronized QueueEntry writeOnFile(String str, Integer key) throws IOException, StringQueueException { if (str==null || str.isEmpty()) { throw new IllegalArgumentException("Invalid string to write on file"); } if (!this.key.equals(key)) { throw new IllegalArgumentException("Wrong key while writing"); } if (raFile==null) { openFile(); } // Update the timestamps // // It is better to update the timestamps before writing in the file // because if the string is malformed we get a exception and do not // add in the file updateLogDates(str); writing=true; long startPos; long endPos; synchronized (raFile) { startPos = file.length(); raFile.seek(startPos); raFile.writeBytes(str); endPos = file.length(); } // The file has one more string! stringsInFile.incrementAndGet(); return new QueueEntry(key,startPos,endPos); } /** * Read a string from the file * * @param entry The cache entry to saying how to read the entry * * @return The string read from the file */ public synchronized String readFromFile(QueueEntry entry) throws IOException { if (entry==null) { throw new IllegalArgumentException("The QueueEntry can't be null"); } if (entry.key!=key) { throw new IllegalArgumentException("Wrong key while reading"); } if (raFile==null) { openFile(); } reading=true; byte buffer[] = new byte[(int)(entry.end-entry.start)]; raFile.seek(entry.start); int bytesRead=raFile.read(buffer); if (bytesRead!=buffer.length) { throw new IOException("read returned "+bytesRead+" instead of "+buffer.length); } return new String(buffer); } /** * Set the reading mode of the file. * * @param reading <code>true</code> if the file is used for reading */ public synchronized void setReadingMode(boolean reading) { this.reading=reading; checkRaFileUsage(); } /** * Set the writing mode of the file. * * @param writing <code>true</code> if the file is used for writing */ public synchronized void setWritingMode(boolean writing) { this.writing=writing; checkRaFileUsage(); } /** * Update the times of the youngest and oldest log * in this file. * * @param str The string representing the log */ private void updateLogDates(String str) throws StringQueueException{ long millis=0; // Date of current log read from str // To improve performances I don't want to parse the string // in this method so I look for the // timestamp that has always the same format str=str.toUpperCase(); int pos=str.indexOf(timestampStrTag); if (pos==-1) { String msg=timestampStrTag+" not found in: ["+str+"]!!!"; throw new StringQueueException(msg); } // switch to the end of the TIMESTAMP string int startPosOfTimestamp=pos+timestampStrTag.length(); String timestamp=str.substring(startPosOfTimestamp, startPosOfTimestamp+timeStampLength); try { millis=IsoDateFormat.parseIsoTimestamp(timestamp).getTime(); } catch (ParseException e) { throw new StringQueueException("Error parsing the date from: ["+timestamp+"]",e); } // check and store the date if (millis<youngestLogDateMillis || youngestLogDateMillis==0) { youngestLogDateMillis=millis; } if (millis>oldestLogDateMillis || oldestLogDateMillis==0) { oldestLogDateMillis=millis; } } /** * @return the date of the youngest log in this file * in ISO format */ public String minDate() { if (youngestLogDateMillis==0) { return null; } else { return IsoDateFormat.formatDate(new Date(youngestLogDateMillis)); } } /** * @return The number of strings in this file */ public long getNumOfStringsInFile() { return stringsInFile.get(); } /** * @return the date of the oldest log in this file * in ISO format */ public String maxDate() { if (oldestLogDateMillis==0) { return null; } else { return IsoDateFormat.formatDate(new Date(oldestLogDateMillis)); } } }