/* -*- Mode: java; indent-tabs-mode: nil; c-basic-offset: 2 -*- * * The contents of this file are subject to the Mozilla Public License * Version 1.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is the Grendel mail/news client. * * The Initial Developer of the Original Code is Netscape Communications * Corporation. Portions created by Netscape are Copyright (C) 1997 * Netscape Communications Corporation. All Rights Reserved. * * Created: Jamie Zawinski , 10 Sep 1997. */ package grendel.storage; import calypso.util.Assert; import calypso.util.ByteBuf; import calypso.util.ByteLineBuffer; import java.lang.Thread; import java.lang.SecurityException; import java.lang.InterruptedException; import java.util.Vector; import java.util.Enumeration; import java.io.File; import java.io.OutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.FileNotFoundException; import grendel.util.Constants; /** This class represents a newsrc-format disk file, and the newsgroup descriptions within it. */ public class NewsRC { static final boolean DEBUG = true; protected File file = null; // disk file; might not exist, but never null. /* This holds the date of the newsrc file when it was parsed. We need to save this (older) values so that we can tell when the summary file is out of date. This is measured in the units of File.lastModified(). */ protected long file_date = -1; /* This holds the size of the file when it was parsed. We need to save this (older) values so that we can tell when the summary file is out of date. */ protected long file_size = -1; /* Any "options" lines that appeared at the head of the newsrc file. We don't use them, but we preserve them if they exist. */ protected ByteBuf options_lines = null; protected Vector lines = new Vector(); // holds NewsRCLines protected boolean dirty = false; // needs to be saved to disk? Thread updateThread = null; // background save thread. /** Reads and parses the file, and returns a NewsRC object describing it. If the file doesn't exist, the newsrc object will be empty. */ NewsRC(File file) throws IOException { this.file = file; load(); } private synchronized void load() throws IOException { try { FileInputStream in = null; try { in = new FileInputStream(file); parse_file(in); file_size = file.length(); file_date = file.lastModified(); dirty = false; } finally { if (in != null) in.close(); } } catch (FileNotFoundException e) { // do nothing -- it's ok for the file to not exist. } } /** Returns the file associated with this newsrc object. Note that the file may not actually exist on disk yet. */ public File file() { Assert.Assertion(file != null); return file; } protected void parse_file(FileInputStream in) throws IOException { ByteLineBuffer b = new ByteLineBuffer(); ByteBuf inbuf = new ByteBuf(1024); ByteBuf outbuf = new ByteBuf(100); boolean eof = false; lines.removeAllElements(); while (!eof) { inbuf.setLength(0); int r = inbuf.read(in, 1024); if (r == -1) { eof = true; b.pushEOF(); } else { b.pushBytes(inbuf); } while (b.pullLine(outbuf)) { NewsRCLine line = makeNewsRCLine(outbuf); if (line == null) { if (options_lines == null) options_lines = new ByteBuf(outbuf.length()); options_lines.append(outbuf); } else { lines.addElement(line); } } } } /** Call this to indicate that the file needs to be flushed to disk. */ public synchronized void markDirty() { if (dirty) return; if (DEBUG) System.err.println("Marking newsrc file " + file + " dirty."); boolean was_dirty = dirty; dirty = true; verifyFileDate(was_dirty); if (!dirty) return; // verifyFileDate() might have saved or reloaded. if (updateThread == null || !updateThread.isAlive()) { if (DEBUG) System.err.println("spawning newsrc background saver for " + file); updateThread = new NewsRCUpdateThread(this); updateThread.start(); } } /** Parses the given line and creates a NewsRCLine object representing it. Returns null if the line is unparsable. */ protected NewsRCLine makeNewsRCLine(ByteBuf line_buf) { byte line[] = line_buf.toBytes(); int length = line_buf.length(); boolean subscribed = false; int i = 0; while (i < length && line[i] <= (byte)' ') i++; // skip whitespace int start = i; while (i < length && // skip non-whitespace, but line[i] > (byte)' ' && // not the two magic chars line[i] != (byte)':' && line[i] != (byte)'!') i++; if (i == start) return null; String group_name = new String(line, start, i); while (i < length && line[i] <= (byte)' ') i++; // skip whitespace if (i < length && line[i] == (byte)':') subscribed = true; else if (i < length && line[i] == (byte)'!') subscribed = false; else return null; i++; while (i < length && line[i] <= (byte)' ') i++; // skip whitespace return new NewsRCLine(this, group_name, subscribed, line, i, length); } /* Returns an Enumeration of the NewsRCLines in this NewsRC. */ public Enumeration elements() { return lines.elements(); } /** Returns a NewsRCLine describing the named newsgroup. If there isn't one, creates one. */ public NewsRCLine getNewsgroup(String group_name) { for (Enumeration e = lines.elements(); e.hasMoreElements(); ) { NewsRCLine n = (NewsRCLine) e.nextElement(); if (group_name.equals(n.name())) return n; } NewsRCLine n = new NewsRCLine(this, group_name, true); lines.addElement(n); return n; } public void write(OutputStream stream) throws IOException { synchronized (stream) { if (options_lines != null) { stream.write(options_lines.toBytes(), 0, options_lines.length()); } ByteBuf b = new ByteBuf(); final byte[] CR = { (byte)'\r' }; final byte[] LF = { (byte)'\n' }; final byte[] CRLF = { (byte)'\r', (byte)'\n' }; byte linebreak[] = (Constants.ISUNIX ? LF : Constants.ISMAC ? CR : CRLF); for (Enumeration e = lines.elements(); e.hasMoreElements(); ) { NewsRCLine n = (NewsRCLine) e.nextElement(); b.setLength(0); n.write(b); b.append(linebreak); stream.write(b.toBytes(), 0, b.length()); } } } /** Returns the name of a non-existent file that can be used as a temporary file while writing this newsrc. This name is OS-dependent.

On all systems, the summary file resides in the same directory as the folder file, and has the same "base" name.

On Unix, the summary file for .newsrc-HOSTNAME would be .newsrc-HOSTNAME.ns_tmp.

On Windows and Mac, the summary file for news-HOSTNAME.rc would be news-HOSTNAME.TMP. */ protected File tempFileName() { File file = file(); String dir = file.getParent(); String name = file.getName(); if (Constants.ISUNIX) { name = name + ".ns_tmp"; } else { int i = name.lastIndexOf("."); if (i > 0) name = name.substring(0, i); name = name + ".tmp"; } return new File(dir, name); } /** Writes the file associated with this NewsRC object, if there have been any changes to it. */ public void save() throws IOException { if (!dirty) return; synchronized (this) { if (!dirty) return; verifyFileDate(true); save_internal(); } } protected void save_internal() throws IOException { if (!dirty) return; synchronized (this) { if (!dirty) // double check return; File file = file(); File tmp = tempFileName(); OutputStream out = null; try { // make sure the temp file gets deleted. try { // make sure the stream gets closed. out = new FileOutputStream(tmp); out = new BufferedOutputStream(out); if (DEBUG) { System.err.println("Writing tmp newsrc file " + tmp); } write(out); } finally { // make sure the stream gets closed. if (out != null) { out.close(); out = null; } } System.out.println("Delete : "+file.delete()); System.out.println("Rename : "+tmp.renameTo(file)); file_size = file.length(); file_date = file.lastModified(); dirty = false; if (DEBUG) { System.err.println("Renaming tmp newsrc file " + tmp + " to " + file); } tmp = null; } finally { // make sure the temp file gets deleted. if (tmp != null) try { tmp.delete(); } catch (SecurityException e) { } } } } /** Compare the current date/size of the disk file with the date/size at the time we read it, and if the disk version has changed, do something sensible. */ protected synchronized void verifyFileDate(boolean was_dirty) { long cur_size = -1; long cur_date = -1; if (file.exists()) { cur_size = file.length(); cur_date = file.lastModified(); } if (cur_date != file_date || cur_size != file_size) { if (file_date != -1 && cur_date == -1) { // file used to exist, and has been deleted... System.err.println("WARNING: " + file + " was deleted out from under us! Saving now..."); dirty = true; try { save_internal(); } catch (IOException e) { System.err.println("ERROR! Saving " + file + ": " + e); } } else { // either: the file didn't exist and now it does; // or it already existed, but has changed since we read it. // #### dialog box or something synchronized(System.err) { System.err.println("WARNING: " + file + " has changed!"); if (was_dirty) System.err.println(" Discarding our changes and " + "reloading the disk version..."); else System.err.println(" Reloading the disk version..."); } try { load(); } catch (IOException e) { System.err.println("ERROR! Reloading " + file + ": " + e); } // #### Perhaps here we should generate some kind of listener event, // to let the folder list know that the unread message counts // may have changed? } } } public static void main(String argv[]) { try { NewsRC rc = new NewsRC(new File(argv[0])); if (rc.options_lines != null) System.out.print(rc.options_lines); for (Enumeration e = rc.lines.elements(); e.hasMoreElements(); ) System.out.println(e.nextElement()); } catch (IOException e) { System.err.println(e); } } } /** This class implements a thread which runs in the background and saves a modified newsrc file. It dies once the file has been updated; it will be re-spawned if that newsrc file becomes dirty again.

There can be one of these threads per NewsRC object. */ class NewsRCUpdateThread extends Thread { static int delay = 30; // seconds NewsRC rc = null; NewsRCUpdateThread(NewsRC rc) { this.rc = rc; setDaemon(true); setName(getClass().getName() + " " + rc.file()); } public void run() { try { sleep(delay * 1000); } catch (InterruptedException e) { return; // is this the right way to do this? } if (NewsRC.DEBUG) System.err.println("background thread saving newsrc file " + rc.file()); try { synchronized (rc) { rc.updateThread = null; rc.save(); } } catch (IOException e) { // nothing to be done? System.err.println("IOException while saving newsrc file " + rc.file()); } } }