Index: /trunk/gli/src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java
===================================================================
--- /trunk/gli/src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java	(revision 5168)
+++ /trunk/gli/src/org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java	(revision 5168)
@@ -0,0 +1,643 @@
+package org.greenstone.gatherer.util;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.io.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.event.*;
+import javax.swing.text.*;
+
+import org.greenstone.gatherer.Gatherer;
+import org.greenstone.gatherer.util.StaticStrings;
+
+/** A Document whose underlying data is stored in a RandomAccessFile, and whose Element implementations lack the memory hogging problems associated with anything that extends the AbstractDocument class. This Document, for reasons of time constraints and sanity, only provides an editting ability of appending new lines to the end of the current document, perfect for our logging needs, completely useless for text editing purposes. Furthermore, since the append actions tend to somewhat swamp the IO, I'll temporarily store strings in the structure model, and write them out using a separate thread. */
+public class AppendLineOnlyFileDocument
+    implements Document {
+
+    static final private String EMPTY_STR      = "";
+    static final private String GLI_HEADER_STR = "X:GLI Import and Build log:";
+
+    private AppendLineOnlyFileDocumentElement root_element;
+    private AppendLineOnlyFileDocumentOwner owner;
+    private EventListenerList listeners_list;
+    private HashMap cache;
+    private HashMap properties;
+    private long length;
+    private RandomAccessFile file;
+    private WriterThread writer;
+
+    public AppendLineOnlyFileDocument(String filename) {
+	System.err.println("Creating log: " + filename);
+	// Initialization
+	this.cache = new HashMap();
+	this.listeners_list = new EventListenerList();
+	this.properties = new HashMap();
+	this.writer = new WriterThread();
+	writer.start();
+	// Open underlying file
+	try {
+	    file = new RandomAccessFile(filename, "rw");
+	    // Create the root element.
+	    length = file.length();
+	    root_element = new AppendLineOnlyFileDocumentElement();
+	    // Now quickly read through the underlying file, building an Element for each line.
+	    long start_offset = 0L;
+	    file.seek(start_offset);
+	    int character = -1;
+	    while((character = file.read()) != -1) {
+		if(character == StaticStrings.NEW_LINE_CHARACTER) {
+		    long end_offset = file.getFilePointer();
+		    Element child_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset);
+		    root_element.add(child_element);
+		    child_element = null;
+		    start_offset = end_offset;
+		}
+	    }
+	    // If there we no lines found, then append the file header.
+	    if(root_element.getElementCount() == 0) {
+		appendLine(GLI_HEADER_STR);
+	    }
+	}
+	catch (Exception error) {
+	    Gatherer.printStackTrace(error);
+	}
+    }
+    
+    /** Adds a document listener for notification of any changes. */
+    public void addDocumentListener(DocumentListener listener) {
+	///ystem.err.println("addDocumentListener(" + listener + ")");
+	listeners_list.add(DocumentListener.class, listener);
+    }
+
+    /** Append some content after the document. */
+    public void appendLine(String str) {
+	// Ensure the string ends in a newline
+	if(!str.endsWith("\n")) {
+	    str = str + "\n";
+	}
+	try {
+	    int str_length = str.length();
+	    long start_offset = length;
+	    long end_offset = start_offset + (long) str_length;
+	    length = length + str_length;
+	    //write(start_offset, end_offset, str, str_length);
+	    // Create a new element to represent this line
+	    AppendLineOnlyFileDocumentElement new_line_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset, str);
+	    root_element.add(new_line_element);
+	    // Queue the content to be written.
+	    writer.queue(new_line_element);
+	    // Now fire an event so everyone knows the content has changed.
+	    DocumentEvent event = new AppendLineOnlyFileDocumentEvent(new_line_element, (int)start_offset, str_length, DocumentEvent.EventType.INSERT);
+	    Object[] listeners = listeners_list.getListenerList();
+	    for (int i = listeners.length - 2; i >= 0; i = i - 2) {
+		if (listeners[i] == DocumentListener.class) {
+		    ((DocumentListener)listeners[i+1]).insertUpdate(event);
+		}
+	    }
+	    listeners = null;
+	    event = null;
+	    new_line_element = null;
+	}
+	catch(Exception error) {
+	    Gatherer.printStackTrace(error);
+	}
+    }
+
+    /** Returns a position that will track change as the document is altered. */
+    public Position createPosition(int offs)  {
+	return new AppendLineOnlyFileDocumentPosition(offs);
+    }
+
+    /** Gets the default root element for the document model. */
+    public Element getDefaultRootElement() {
+	return root_element;
+    }
+
+    /** Returns the length of the data. */
+    public int getLength() {
+	return (int) length;
+    }
+	
+    public Object getProperty(Object key) {
+	return properties.get(key);
+    }
+
+    public boolean isStillWriting() {
+	return writer.isStillWriting();
+    }
+
+    /** Gets a sequence of text from the document. */
+    public String getText(int offset, int l) 
+	throws BadLocationException {
+	String request = "getText(" + offset + ", " + l + ")";
+	///ystem.err.println(request);
+	String text = (String) cache.get(request);
+	if(text == null || text.length() < l) {
+	    try {
+		int file_length = (int) file.length();
+		///ystem.err.println("file_length = " + file_length + ", length = " + length);
+		if(l == 0) {
+		    text = EMPTY_STR;
+		}
+		else if(0 <= offset && offset < length && (offset + l) <= length) {
+		    if(offset < file_length) {
+			text = read((long)offset, l);
+			if(text.length() != l) {
+			    ///ystem.err.println("Asked for " + l + " characters of text. But only read " + text.length());
+			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
+			}
+		    }
+		    else {
+			int index = root_element.getElementIndex(offset);
+			if(index < root_element.getElementCount()) {
+			    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) root_element.getElement(index);
+			    text = element.getContent();
+			}
+			else {
+			    ///ystem.err.println("Index is " + index + " but there are only " + root_element.getElementCount() + " children.");
+			    throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset); 
+			}
+		    }
+		    
+		}
+	    }
+	    catch (IOException error) {
+		Gatherer.printStackTrace(error);
+	    }
+	    if(text == null) {
+		///ystem.err.println("Text is null.");
+		throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
+	    }
+	    cache.put(request, text);
+	}
+	request = null;
+	return text;
+    }
+
+    /** Fetches the text contained within the given portion of the document. */
+    public void getText(int offset, int length, Segment txt) 
+	throws javax.swing.text.BadLocationException  {
+	String str = getText(offset, length);
+	txt.array = str.toCharArray();
+	txt.count = str.length();
+	txt.offset = 0;
+	str = null;
+    }
+	
+    public void putProperty(Object key, Object value) {
+	properties.put(key, value);
+    }
+
+    public boolean ready() {
+	return (file != null);
+    }
+	
+    //Removes a document listener.
+    public void removeDocumentListener(DocumentListener listener) {
+	///ystem.err.println("removeDocumentListener()");
+	listeners_list.remove(DocumentListener.class, listener);
+    }
+
+    public void setExit() {
+	writer.exit();
+    }
+
+    public void setOwner(AppendLineOnlyFileDocumentOwner owner) {
+	this.owner = owner;
+    }
+
+    /** To record the final state of the logging process we reserve a single character at the start of the file. */
+    public synchronized void setSpecialCharacter(char character) {
+	try {
+	    file.seek(0L);
+	    file.write((int)character);
+	}
+	catch (Exception error) {
+	    Gatherer.printStackTrace(error);
+	}
+    }
+
+    private synchronized String read(long start_offset, int l) 
+	throws IOException {
+	//print("read(" + start_offset + ", " + l + ")... ");
+	byte[] buffer = new byte[l];
+	file.seek(start_offset);
+	int result = file.read(buffer, 0, l);
+	//print("read() complete");
+	return new String(buffer);
+    }
+
+    private synchronized void write(long start_offset, long end_offset, String str, int l) 
+	throws IOException {	
+	//print("write(" + start_offset + ", " + end_offset + ", " + str + ", " + l + ")");
+	file.setLength(end_offset);
+	file.seek(start_offset);
+	file.write(str.getBytes(), 0, l);
+	//print("write() complete");
+    }
+
+    private class AppendLineOnlyFileDocumentElement
+	extends ArrayList
+	implements Element {
+
+	private Element parent;
+	private long end;
+	private long start;
+	private String content;
+	
+	/** Construct a new root element, which can have no content, but calculates its start and end from its children.
+	 * @param  start the starting offset as a long
+	 * @param  end the ending offset as a long.
+	 */
+	public AppendLineOnlyFileDocumentElement() {
+	    super();
+	    this.end = 0L;
+	    this.parent = null;
+	    this.start = 0L;
+	}
+
+
+	/** Construct a new element, whose content is found in bytes start to end - 1 within the random access file backing this document.
+	 * @param  start the starting offset as a long
+	 * @param  end the ending offset as a long.
+	 */
+	public AppendLineOnlyFileDocumentElement(long start, long end) {
+	    super();
+	    this.end = end;
+	    this.parent = null;
+	    this.start = start;
+	}
+
+	/** Construct a new element, whose content is provided, but should at some later time be written to bytes start to end - 1 in the random access file backing this document.
+	 * @param  start the starting offset as a long
+	 * @param  end the ending offset as a long.
+	 */
+	public AppendLineOnlyFileDocumentElement(long start, long end, String content) {
+	    super();
+	    this.content = content;
+	    this.end = end;
+	    this.parent = null;
+	    this.start = start;
+	}
+
+	public void add(Element child) {
+	    super.add(child);
+	    ((AppendLineOnlyFileDocumentElement)child).setParent(this);
+	}
+
+	public void clearContent() {
+	    content = null;
+	}
+
+	/** This document does not allow content markup. */
+	public AttributeSet getAttributes() {
+	    return null;
+	}
+
+	public String getContent() {
+	    return content;
+	}
+
+	/** Fetches the document associated with this element. 
+	 * @return the AppendLineOnlyDocument containing this element
+	 */
+	public Document getDocument() {
+	    return AppendLineOnlyFileDocument.this;
+	}
+
+	/** Fetches the child element at the given index. */
+	public Element getElement(int index) {
+	    Element element;
+	    if(0 <= index && index < size()) {
+		element = (Element) get(index);
+	    }
+	    else {
+		throw new IndexOutOfBoundsException("AppendLineOnlyDocument.AppendLineOnlyFileDocumentElement.getElement(" + index + ")");
+	    }
+	    return element;
+	}
+          
+	/** Gets the number of child elements contained by this element. */
+	public int getElementCount() {
+	    return size();
+	}
+
+	/** Gets the child element index closest to the given offset. */
+	public int getElementIndex(int offset) {
+	    int index = -1;
+	    if(parent != null) {
+		index = -1;
+	    }
+	    else if(offset < 0) {
+		index = 0;
+	    }
+	    else if(offset >= length) {
+		index = size() - 1;
+	    }
+	    else {
+		int size = size();
+		for(int i = 0; index == -1 && i < size; i++) {
+		    Element child = (Element) get(i);
+		    if(child.getStartOffset() <= offset && offset < child.getEndOffset()) {
+			index = i;
+		    }
+		    child = null;
+		}
+	    }
+	    return index;
+	}
+          
+	/** Fetches the offset from the beginning of the document that this element ends at. */
+	public int getEndOffset() {
+	    if(parent != null) {
+		return (int) end;
+	    }
+	    // Return the Documents length.
+	    else {
+		return (int) length;
+	    }
+	}
+          
+	/** This method retrieves the name of the element, however names are not important in this document so the name is always an empty string.
+	 * @return an empty String
+	 */
+	public String getName() {
+	    return StaticStrings.EMPTY_STR;
+	}
+          
+	/** Fetches the parent element. */
+	public Element getParentElement() {
+	    return parent;
+	}
+          
+	/** Fetches the offset from the beginning of the document that this element begins at. */
+	public int getStartOffset() {
+	    if(parent != null) {
+		return (int) start;
+	    }
+	    else {
+		return 0;
+	    }
+	}
+          
+	/** Since this is a very simple model, only the root node can have children. All the children are leaves. */
+	public boolean isLeaf() {
+	    return (parent != null);
+	} 
+
+	public void setParent(Element parent) {
+	    this.parent = parent;
+	}
+    }
+
+    private class AppendLineOnlyFileDocumentEvent
+	implements DocumentEvent {
+
+	private DocumentEvent.EventType type;
+	private AppendLineOnlyFileDocumentElement element;
+	private AppendLineOnlyFileDocumentElementChange element_change;
+	private int len;
+	private int offset;
+	
+	public AppendLineOnlyFileDocumentEvent(AppendLineOnlyFileDocumentElement element, int offset, int len, DocumentEvent.EventType type) {
+	    this.element = element;
+	    this.element_change = null;
+	    this.len = len;
+	    this.offset = offset;
+	    this.type = type;
+	}
+
+	public Document getDocument() {
+	    return AppendLineOnlyFileDocument.this;
+	}
+
+	public int getLength() {
+	    return len;
+	}
+
+	public int getOffset() {
+	    return offset;
+	}
+
+	public DocumentEvent.EventType getType() {
+	    return type;
+	}
+
+	// ***** IGNORE *****
+	public DocumentEvent.ElementChange getChange(Element elem) {
+	    if(element_change == null) {
+		element_change = new AppendLineOnlyFileDocumentElementChange();
+	    }
+	    return element_change;
+	}
+
+	private class AppendLineOnlyFileDocumentElementChange
+	    implements DocumentEvent.ElementChange {
+
+	    private Element[] children_added;
+	    private Element[] children_removed;
+	    private int index;
+	    public AppendLineOnlyFileDocumentElementChange() {
+		children_added = new Element[1];
+		children_added[0] = element;
+		children_removed = new Element[0];
+		index = root_element.indexOf(element);
+	    }
+	    /** Gets the child element that was added to the given parent element. 
+	     * @return an Element[] containing the added element
+	     */
+	    public Element[] getChildrenAdded() {
+		return children_added;
+	    }
+	    /** This model does not allow elements to be removed.
+	     * @return an Element[] containing nothing
+	     */
+	    public Element[] getChildrenRemoved() {
+		return children_removed;
+	    }
+          
+	    /** Returns the root element, as our document structure is only two layers deep.
+	     * @return the root Element
+	     */
+	    public Element getElement() {
+		return root_element;
+	    }
+          
+	    /** Fetches the index within the element represented. 
+	     * @return an int specifying the index of change within the root element
+	     */
+	    public int getIndex() {
+		return index;
+	    }
+	}
+    }
+
+    private class AppendLineOnlyFileDocumentPosition
+	implements Position {
+
+	private int offset;
+
+	public AppendLineOnlyFileDocumentPosition(int offset) {
+	    this.offset = offset;
+	}
+
+	public int getOffset() {
+	    return offset;
+	}
+    }
+
+    private class WriterThread
+	extends Thread {
+
+	private boolean exit;
+	private boolean running;
+	private Vector queue;
+
+	public WriterThread() {
+	    super("WriterThread");
+	    exit = false;
+	    queue = new Vector();
+	}
+
+	public synchronized void exit() {
+	    //print("WriterThread.exit() start");
+	    exit = true;
+	    notify();
+	    //print("WriterThread.exit() complete");
+	}
+
+	public boolean isStillWriting() {
+	    return running;
+	}
+
+	public void run() {
+	    running = true;
+	    while(!exit) {
+		if(!queue.isEmpty()) { 
+		    AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) queue.remove(0);
+		    // Write the content to file.
+		    String content = element.getContent();
+		    if(content != null) {
+			try {
+			    write(element.getStartOffset(), element.getEndOffset(), content, content.length());
+			}
+			catch(Exception error) {
+			    Gatherer.printStackTrace(error);
+			}
+			element.clearContent();
+		    }
+		}
+		else {
+		    synchronized(this) {
+			try {
+			    //print("WriterThread.wait() start");
+			    wait();
+			    //print("WriterThread.wait() complete");
+			}
+			catch(Exception error) {
+			}
+		   }
+		}
+	    }
+	    running = false;
+	    if(owner != null) {
+		owner.remove(AppendLineOnlyFileDocument.this);
+	    }
+	}
+
+	public synchronized void queue(Element element) {
+	    //print("WriterThread.queue() start");
+	    queue.add(element);
+	    notify();
+	    //print("WriterThread.queue() complete");
+	}
+    }
+
+    // ***** METHODS WE ARE NOW IGNORING BECAUSE WE ARE VIRTUALLY READ-ONLY *****
+    
+    /** Adds an undo listener for notification of any changes. */
+    public void addUndoableEditListener(UndoableEditListener listener) {}
+    	
+    /** */
+    public Position getEndPosition() {
+	///ystem.err.println("getEndPosition()");
+	return null;
+    }
+
+    /** Gets all root elements defined. */
+    public Element[] getRootElements() {return null;}
+	
+    public Position getStartPosition() {
+	///ystem.err.println("getStartPosition()");
+	return null;
+    }
+    
+    public void insertString(int offset, String str, AttributeSet a) {}
+
+    /** Removes some content from the document. */
+    public void remove(int offs, int len) {}
+
+    /** Removes an undo listener. */
+    public void removeUndoableEditListener(UndoableEditListener listener) {}
+    
+    /** Renders a runnable apparently. */
+    public void render(Runnable r) {}
+
+    static synchronized public void print(String message) {
+	Gatherer.println(message);
+    }
+
+    static public void main(String[] args) {
+	JFrame frame = new JFrame("AppendLineOnlyFileDocument Test");
+	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+	frame.setSize(640,480);
+	JPanel content = (JPanel) frame.getContentPane();
+	
+	//PlainDocument document = new PlainDocument();
+	//document.setAsynchronousLoadPriority(-1);
+	final AppendLineOnlyFileDocument document = new AppendLineOnlyFileDocument("temp.txt");
+
+	final JTextArea text_area = new JTextArea(document);
+
+	JButton read_button = new JButton("Read Huge File");
+	read_button.addActionListener(new ActionListener() {
+		public void actionPerformed(ActionEvent event) {
+		    Thread task = new Thread("LoadHugeFileThread") {
+			    public void run() {
+				// Load the specified document
+				try {
+				    BufferedReader in = new BufferedReader(new FileReader(new File("big.txt")));
+				    String line;
+				    
+				    while ((line = in.readLine()) != null) {
+					document.appendLine(line);
+					try {
+					    // Wait a random ammount of time.
+					    synchronized(this) {
+						//print("LoadHugeFileThread.wait() start");
+						wait(100);
+						//print("LoadHugeFileThread.wait() complete");
+					    }
+					}
+					catch(Exception error) {
+					    error.printStackTrace();
+					}
+				    }
+				    
+				} catch (Exception error) {
+				    error.printStackTrace();
+				}
+			    }
+			};
+		    task.start();
+		    
+		}
+	    });
+	content.setLayout(new BorderLayout());
+	content.add(new JScrollPane(text_area), BorderLayout.CENTER);
+	content.add(read_button, BorderLayout.SOUTH);
+
+	frame.show();
+    }
+}
