Source code: org/greenstone/gatherer/util/AppendLineOnlyFileDocument.java
1 package org.greenstone.gatherer.util;
2
3 import java.awt.*;
4 import java.awt.event.*;
5 import java.io.*;
6 import java.util.*;
7 import javax.swing.*;
8 import javax.swing.event.*;
9 import javax.swing.text.*;
10
11 import org.greenstone.gatherer.Gatherer;
12 import org.greenstone.gatherer.util.StaticStrings;
13
14 /** 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. */
15 public class AppendLineOnlyFileDocument
16 implements Document {
17
18 static final private String EMPTY_STR = "";
19 static final private String GLI_HEADER_STR = "X:GLI Import and Build log:";
20
21 private AppendLineOnlyFileDocumentElement root_element;
22 private AppendLineOnlyFileDocumentOwner owner;
23 private EventListenerList listeners_list;
24 private HashMap cache;
25 private HashMap properties;
26 private long length;
27 private RandomAccessFile file;
28 private WriterThread writer;
29
30 public AppendLineOnlyFileDocument(String filename) {
31 ///ystem.err.println("Creating log: " + filename);
32 // Initialization
33 this.cache = new HashMap();
34 this.listeners_list = new EventListenerList();
35 this.properties = new HashMap();
36 this.writer = new WriterThread();
37 writer.start();
38 // Open underlying file
39 try {
40 file = new RandomAccessFile(filename, "rw");
41 // Create the root element.
42 length = file.length();
43 root_element = new AppendLineOnlyFileDocumentElement();
44 // Now quickly read through the underlying file, building an Element for each line.
45 long start_offset = 0L;
46 file.seek(start_offset);
47 int character = -1;
48 while((character = file.read()) != -1) {
49 if(character == StaticStrings.NEW_LINE_CHAR) {
50 long end_offset = file.getFilePointer();
51 Element child_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset);
52 root_element.add(child_element);
53 child_element = null;
54 start_offset = end_offset;
55 }
56 }
57 // If there we no lines found, then append the file header.
58 if(root_element.getElementCount() == 0) {
59 appendLine(GLI_HEADER_STR);
60 }
61 }
62 catch (Exception error) {
63 Gatherer.printStackTrace(error);
64 }
65 }
66
67 /** Adds a document listener for notification of any changes. */
68 public void addDocumentListener(DocumentListener listener) {
69 ///ystem.err.println("addDocumentListener(" + listener + ")");
70 listeners_list.add(DocumentListener.class, listener);
71 }
72
73 /** Append some content after the document. */
74 public void appendLine(String str) {
75 // Ensure the string ends in a newline
76 if(!str.endsWith("\n")) {
77 str = str + "\n";
78 }
79 try {
80 int str_length = str.length();
81 long start_offset = length;
82 long end_offset = start_offset + (long) str_length;
83 length = length + str_length;
84 //write(start_offset, end_offset, str, str_length);
85 // Create a new element to represent this line
86 AppendLineOnlyFileDocumentElement new_line_element = new AppendLineOnlyFileDocumentElement(start_offset, end_offset, str);
87 root_element.add(new_line_element);
88 // Queue the content to be written.
89 writer.queue(new_line_element);
90 // Now fire an event so everyone knows the content has changed.
91 DocumentEvent event = new AppendLineOnlyFileDocumentEvent(new_line_element, (int)start_offset, str_length, DocumentEvent.EventType.INSERT);
92 Object[] listeners = listeners_list.getListenerList();
93 for (int i = listeners.length - 2; i >= 0; i = i - 2) {
94 if (listeners[i] == DocumentListener.class) {
95 ((DocumentListener)listeners[i+1]).insertUpdate(event);
96 }
97 }
98 listeners = null;
99 event = null;
100 new_line_element = null;
101 }
102 catch(Exception error) {
103 Gatherer.printStackTrace(error);
104 }
105 }
106
107 /** Returns a position that will track change as the document is altered. */
108 public Position createPosition(int offs) {
109 return new AppendLineOnlyFileDocumentPosition(offs);
110 }
111
112 /** Gets the default root element for the document model. */
113 public Element getDefaultRootElement() {
114 return root_element;
115 }
116
117 /** Returns the length of the data. */
118 public int getLength() {
119 return (int) length;
120 }
121
122 /** A version of get length which essentially returns the offset to the start of the last line in the document.
123 * @return the offset length as an int
124 */
125 public int getLengthToNearestLine() {
126 AppendLineOnlyFileDocumentElement last_element = (AppendLineOnlyFileDocumentElement)root_element.getElement(root_element.getElementCount() - 1);
127 if(last_element != null ) {
128 return last_element.getStartOffset();
129 }
130 else {
131 return (int) length; // The best we can do.
132 }
133 }
134
135 public Object getProperty(Object key) {
136 return properties.get(key);
137 }
138
139 public boolean isStillWriting() {
140 return writer.isStillWriting();
141 }
142
143 /** Gets a sequence of text from the document. */
144 public String getText(int offset, int l)
145 throws BadLocationException {
146 String request = "getText(" + offset + ", " + l + ")";
147 ///ystem.err.println(request);
148 String text = null;//(String) cache.get(request);
149 if(text == null || text.length() < l) {
150 try {
151 int file_length = (int) file.length();
152 ///ystem.err.println("file_length = " + file_length + ", length = " + length);
153 if(l == 0) {
154 text = EMPTY_STR;
155 }
156 else if(0 <= offset && offset < length && (offset + l) <= length) {
157 if(offset < file_length) {
158 text = read((long)offset, l);
159 if(text.length() != l) {
160 ///ystem.err.println("Asked for " + l + " characters of text. But only read " + text.length());
161 throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
162 }
163 }
164 else {
165 int index = root_element.getElementIndex(offset);
166 if(index < root_element.getElementCount()) {
167 AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) root_element.getElement(index);
168 text = element.getContent();
169 }
170 else {
171 ///ystem.err.println("Index is " + index + " but there are only " + root_element.getElementCount() + " children.");
172 throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
173 }
174 }
175
176 }
177 }
178 catch (IOException error) {
179 Gatherer.printStackTrace(error);
180 }
181 if(text == null) {
182 ///ystem.err.println("Text is null.");
183 throw new BadLocationException("AppendLineOnlyDocument.getText(" + offset + ", " + l + ")", offset);
184 }
185 //cache.put(request, text);
186 }
187 request = null;
188 return text;
189 }
190
191 /** Fetches the text contained within the given portion of the document. */
192 public void getText(int offset, int length, Segment txt)
193 throws javax.swing.text.BadLocationException {
194 String str = getText(offset, length);
195 txt.array = str.toCharArray();
196 txt.count = str.length();
197 txt.offset = 0;
198 str = null;
199 }
200
201 public void putProperty(Object key, Object value) {
202 properties.put(key, value);
203 }
204
205 public boolean ready() {
206 return (file != null);
207 }
208
209 //Removes a document listener.
210 public void removeDocumentListener(DocumentListener listener) {
211 ///ystem.err.println("removeDocumentListener()");
212 listeners_list.remove(DocumentListener.class, listener);
213 }
214
215 public void setExit() {
216 writer.exit();
217 }
218
219 public void setOwner(AppendLineOnlyFileDocumentOwner owner) {
220 this.owner = owner;
221 }
222
223 /** To record the final state of the logging process we reserve a single character at the start of the file. */
224 public synchronized void setSpecialCharacter(char character) {
225 try {
226 file.seek(0L);
227 file.write((int)character);
228 }
229 catch (Exception error) {
230 Gatherer.printStackTrace(error);
231 }
232 }
233
234 private synchronized String read(long start_offset, int l)
235 throws IOException {
236 //print("read(" + start_offset + ", " + l + ")... ");
237 byte[] buffer = new byte[l];
238 file.seek(start_offset);
239 int result = file.read(buffer, 0, l);
240 //print("read() complete");
241 return new String(buffer);
242 }
243
244 private synchronized void write(long start_offset, long end_offset, String str, int l)
245 throws IOException {
246 //print("write(" + start_offset + ", " + end_offset + ", " + str + ", " + l + ")");
247 file.setLength(end_offset);
248 file.seek(start_offset);
249 file.write(str.getBytes(), 0, l);
250 //print("write() complete");
251 }
252
253 private class AppendLineOnlyFileDocumentElement
254 extends ArrayList
255 implements Element {
256
257 private Element parent;
258 private long end;
259 private long start;
260 private String content;
261
262 /** Construct a new root element, which can have no content, but calculates its start and end from its children.
263 * @param start the starting offset as a long
264 * @param end the ending offset as a long.
265 */
266 public AppendLineOnlyFileDocumentElement() {
267 super();
268 this.end = 0L;
269 this.parent = null;
270 this.start = 0L;
271 }
272
273
274 /** Construct a new element, whose content is found in bytes start to end - 1 within the random access file backing this document.
275 * @param start the starting offset as a long
276 * @param end the ending offset as a long.
277 */
278 public AppendLineOnlyFileDocumentElement(long start, long end) {
279 super();
280 this.end = end;
281 this.parent = null;
282 this.start = start;
283 }
284
285 /** 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.
286 * @param start the starting offset as a long
287 * @param end the ending offset as a long.
288 */
289 public AppendLineOnlyFileDocumentElement(long start, long end, String content) {
290 super();
291 this.content = content;
292 this.end = end;
293 this.parent = null;
294 this.start = start;
295 }
296
297 public void add(Element child) {
298 super.add(child);
299 ((AppendLineOnlyFileDocumentElement)child).setParent(this);
300 }
301
302 public void clearContent() {
303 content = null;
304 }
305
306 /** This document does not allow content markup. */
307 public AttributeSet getAttributes() {
308 return null;
309 }
310
311 public String getContent() {
312 return content;
313 }
314
315 /** Fetches the document associated with this element.
316 * @return the AppendLineOnlyDocument containing this element
317 */
318 public Document getDocument() {
319 return AppendLineOnlyFileDocument.this;
320 }
321
322 /** Fetches the child element at the given index. */
323 public Element getElement(int index) {
324 Element element;
325 if(0 <= index && index < size()) {
326 element = (Element) get(index);
327 }
328 else {
329 throw new IndexOutOfBoundsException("AppendLineOnlyDocument.AppendLineOnlyFileDocumentElement.getElement(" + index + ")");
330 }
331 return element;
332 }
333
334 /** Gets the number of child elements contained by this element. */
335 public int getElementCount() {
336 return size();
337 }
338
339 /** Gets the child element index closest to the given offset. */
340 public int getElementIndex(int offset) {
341 int index = -1;
342 if(parent != null) {
343 index = -1;
344 }
345 else if(offset < 0) {
346 index = 0;
347 }
348 else if(offset >= length) {
349 index = size() - 1;
350 }
351 else {
352 int size = size();
353 for(int i = 0; index == -1 && i < size; i++) {
354 Element child = (Element) get(i);
355 if(child.getStartOffset() <= offset && offset < child.getEndOffset()) {
356 index = i;
357 }
358 child = null;
359 }
360 }
361 return index;
362 }
363
364 /** Fetches the offset from the beginning of the document that this element ends at. */
365 public int getEndOffset() {
366 if(parent != null) {
367 return (int) end;
368 }
369 // Return the Documents length.
370 else {
371 return (int) length;
372 }
373 }
374
375 /** This method retrieves the name of the element, however names are not important in this document so the name is always an empty string.
376 * @return an empty String
377 */
378 public String getName() {
379 return StaticStrings.EMPTY_STR;
380 }
381
382 /** Fetches the parent element. */
383 public Element getParentElement() {
384 return parent;
385 }
386
387 /** Fetches the offset from the beginning of the document that this element begins at. */
388 public int getStartOffset() {
389 if(parent != null) {
390 return (int) start;
391 }
392 else {
393 return 0;
394 }
395 }
396
397 /** Since this is a very simple model, only the root node can have children. All the children are leaves. */
398 public boolean isLeaf() {
399 return (parent != null);
400 }
401
402 public void setParent(Element parent) {
403 this.parent = parent;
404 }
405 }
406
407 private class AppendLineOnlyFileDocumentEvent
408 implements DocumentEvent {
409
410 private DocumentEvent.EventType type;
411 private AppendLineOnlyFileDocumentElement element;
412 private AppendLineOnlyFileDocumentElementChange element_change;
413 private int len;
414 private int offset;
415
416 public AppendLineOnlyFileDocumentEvent(AppendLineOnlyFileDocumentElement element, int offset, int len, DocumentEvent.EventType type) {
417 this.element = element;
418 this.element_change = null;
419 this.len = len;
420 this.offset = offset;
421 this.type = type;
422 }
423
424 public Document getDocument() {
425 return AppendLineOnlyFileDocument.this;
426 }
427
428 public int getLength() {
429 return len;
430 }
431
432 public int getOffset() {
433 return offset;
434 }
435
436 public DocumentEvent.EventType getType() {
437 return type;
438 }
439
440 // ***** IGNORE *****
441 public DocumentEvent.ElementChange getChange(Element elem) {
442 if(element_change == null) {
443 element_change = new AppendLineOnlyFileDocumentElementChange();
444 }
445 return element_change;
446 }
447
448 private class AppendLineOnlyFileDocumentElementChange
449 implements DocumentEvent.ElementChange {
450
451 private Element[] children_added;
452 private Element[] children_removed;
453 private int index;
454 public AppendLineOnlyFileDocumentElementChange() {
455 children_added = new Element[1];
456 children_added[0] = element;
457 children_removed = new Element[0];
458 index = root_element.indexOf(element);
459 }
460 /** Gets the child element that was added to the given parent element.
461 * @return an Element[] containing the added element
462 */
463 public Element[] getChildrenAdded() {
464 return children_added;
465 }
466 /** This model does not allow elements to be removed.
467 * @return an Element[] containing nothing
468 */
469 public Element[] getChildrenRemoved() {
470 return children_removed;
471 }
472
473 /** Returns the root element, as our document structure is only two layers deep.
474 * @return the root Element
475 */
476 public Element getElement() {
477 return root_element;
478 }
479
480 /** Fetches the index within the element represented.
481 * @return an int specifying the index of change within the root element
482 */
483 public int getIndex() {
484 return index;
485 }
486 }
487 }
488
489 private class AppendLineOnlyFileDocumentPosition
490 implements Position {
491
492 private int offset;
493
494 public AppendLineOnlyFileDocumentPosition(int offset) {
495 this.offset = offset;
496 }
497
498 public int getOffset() {
499 return offset;
500 }
501 }
502
503 private class WriterThread
504 extends Thread {
505
506 private boolean exit;
507 private boolean running;
508 private Vector queue;
509
510 public WriterThread() {
511 super("WriterThread");
512 exit = false;
513 queue = new Vector();
514 }
515
516 public synchronized void exit() {
517 //print("WriterThread.exit() start");
518 exit = true;
519 notify();
520 //print("WriterThread.exit() complete");
521 }
522
523 public boolean isStillWriting() {
524 return running;
525 }
526
527 public void run() {
528 running = true;
529 while(!exit) {
530 if(!queue.isEmpty()) {
531 AppendLineOnlyFileDocumentElement element = (AppendLineOnlyFileDocumentElement) queue.remove(0);
532 // Write the content to file.
533 String content = element.getContent();
534 if(content != null) {
535 try {
536 write(element.getStartOffset(), element.getEndOffset(), content, content.length());
537 }
538 catch(Exception error) {
539 Gatherer.printStackTrace(error);
540 }
541 element.clearContent();
542 }
543 }
544 else {
545 synchronized(this) {
546 try {
547 //print("WriterThread.wait() start");
548 wait();
549 //print("WriterThread.wait() complete");
550 }
551 catch(Exception error) {
552 }
553 }
554 }
555 }
556 running = false;
557 if(owner != null) {
558 owner.remove(AppendLineOnlyFileDocument.this);
559 }
560 }
561
562 public synchronized void queue(Element element) {
563 //print("WriterThread.queue() start");
564 queue.add(element);
565 notify();
566 //print("WriterThread.queue() complete");
567 }
568 }
569
570 // ***** METHODS WE ARE NOW IGNORING BECAUSE WE ARE VIRTUALLY READ-ONLY *****
571
572 /** Adds an undo listener for notification of any changes. */
573 public void addUndoableEditListener(UndoableEditListener listener) {}
574
575 /** */
576 public Position getEndPosition() {
577 ///ystem.err.println("getEndPosition()");
578 return null;
579 }
580
581 /** Gets all root elements defined. */
582 public Element[] getRootElements() {return null;}
583
584 public Position getStartPosition() {
585 ///ystem.err.println("getStartPosition()");
586 return null;
587 }
588
589 public void insertString(int offset, String str, AttributeSet a) {}
590
591 /** Removes some content from the document. */
592 public void remove(int offs, int len) {}
593
594 /** Removes an undo listener. */
595 public void removeUndoableEditListener(UndoableEditListener listener) {}
596
597 /** Renders a runnable apparently. */
598 public void render(Runnable r) {}
599
600 static synchronized public void print(String message) {
601 Gatherer.println(message);
602 }
603
604 static public void main(String[] args) {
605 JFrame frame = new JFrame("AppendLineOnlyFileDocument Test");
606 frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
607 frame.setSize(640,480);
608 JPanel content = (JPanel) frame.getContentPane();
609
610 //PlainDocument document = new PlainDocument();
611 //document.setAsynchronousLoadPriority(-1);
612 final AppendLineOnlyFileDocument document = new AppendLineOnlyFileDocument("temp.txt");
613
614 final JTextArea text_area = new JTextArea(document);
615
616 JButton read_button = new JButton("Read Huge File");
617 read_button.addActionListener(new ActionListener() {
618 public void actionPerformed(ActionEvent event) {
619 Thread task = new Thread("LoadHugeFileThread") {
620 public void run() {
621 // Load the specified document
622 try {
623 BufferedReader in = new BufferedReader(new FileReader(new File("big.txt")));
624 String line;
625
626 while ((line = in.readLine()) != null) {
627 document.appendLine(line);
628 try {
629 // Wait a random ammount of time.
630 synchronized(this) {
631 //print("LoadHugeFileThread.wait() start");
632 wait(100);
633 //print("LoadHugeFileThread.wait() complete");
634 }
635 }
636 catch(Exception error) {
637 error.printStackTrace();
638 }
639 }
640
641 } catch (Exception error) {
642 error.printStackTrace();
643 }
644 }
645 };
646 task.start();
647
648 }
649 });
650 content.setLayout(new BorderLayout());
651 content.add(new JScrollPane(text_area), BorderLayout.CENTER);
652 content.add(read_button, BorderLayout.SOUTH);
653
654 frame.show();
655 }
656 }