View Javadoc
1   package org.apache.commons.io.input.fork;
2   
3   /*
4    * TODO remove once https://issues.apache.org/jira/browse/IO-444 is fixed
5    */
6   import java.io.ByteArrayOutputStream;
7   import java.io.File;
8   import java.io.FileNotFoundException;
9   import java.io.IOException;
10  import java.io.RandomAccessFile;
11  import java.nio.charset.Charset;
12  import java.util.concurrent.atomic.AtomicBoolean;
13  
14  import org.apache.commons.io.FileUtils;
15  import org.apache.commons.io.IOUtils;
16  
17  class TailerRun implements Runnable {
18  
19      private static final String RAF_MODE = "r";
20      /**
21       * The character set that will be used to read the file.
22       */
23      private final Charset cset;
24      /**
25       * Whether to tail from the end or start of file
26       */
27      private final boolean end;
28  
29      /**
30       * The file which will be tailed.
31       */
32      private final File file;
33  
34      private final AtomicBoolean finished = new AtomicBoolean(false);
35  
36      /**
37       * Buffer on top of RandomAccessFile.
38       */
39      private final byte inbuf[];
40  
41      /**
42       * The last time the file was checked for changes.
43       */
44      private long last = 0;
45  
46      /**
47       * The listener to notify of events when tailing.
48       */
49      private final TailerListener listener;
50  
51      /**
52       * position within the file
53       */
54      private long position = 0;
55      private RandomAccessFile reader = null;
56  
57      /**
58       * Whether to close and reopen the file whilst waiting for more input.
59       */
60      private final boolean reOpen;
61  
62      private final AtomicBoolean startedOnce = new AtomicBoolean(false);
63  
64      public TailerRun(final File file, final Charset cset, final TailerListener listener, final boolean end,
65              final boolean reOpen, final int bufSize) {
66          this.file = file;
67          this.cset = cset;
68          this.end = end;
69          this.reOpen = reOpen;
70          this.inbuf = new byte[bufSize];
71          this.listener = listener;
72      }
73  
74      public void cleanup() {
75          IOUtils.closeQuietly(this.reader);
76          this.reader = null;
77          this.finished.set(true);
78      }
79  
80      public boolean hasFinished() {
81          return this.finished.get();
82      }
83  
84      public boolean hasStartedOnce() {
85          return this.startedOnce.get();
86      }
87  
88      /**
89       * Read new lines.
90       *
91       * @param reader
92       *            The file to read
93       * @return The new position after the lines have been read
94       * @throws java.io.IOException
95       *             if an I/O error occurs.
96       */
97      private long readLines(final RandomAccessFile reader) throws IOException {
98          final ByteArrayOutputStream lineBuf = new ByteArrayOutputStream(64);
99          long pos = reader.getFilePointer();
100         long rePos = pos; // position to re-read
101         int num;
102         boolean seenCR = false;
103         // FIXME replace -1 with EOF when we're merging back into commons-io
104         while ((num = reader.read(this.inbuf)) != -1) {
105             for (int i = 0; i < num; i++) {
106                 final byte ch = this.inbuf[i];
107                 switch (ch) {
108                     case '\n':
109                         seenCR = false; // swallow CR before LF
110                         this.listener.handle(new String(lineBuf.toByteArray(), this.cset));
111                         lineBuf.reset();
112                         rePos = pos + i + 1;
113                         break;
114                     case '\r':
115                         if (seenCR) {
116                             lineBuf.write('\r');
117                         }
118                         seenCR = true;
119                         break;
120                     default:
121                         if (seenCR) {
122                             seenCR = false; // swallow final CR
123                             this.listener.handle(new String(lineBuf.toByteArray(), this.cset));
124                             lineBuf.reset();
125                             rePos = pos + i + 1;
126                         }
127                         lineBuf.write(ch);
128                 }
129             }
130             pos = reader.getFilePointer();
131         }
132         IOUtils.closeQuietly(lineBuf); // not strictly necessary
133         reader.seek(rePos); // Ensure we can re-read if necessary
134         return rePos;
135     }
136 
137     @Override
138     public void run() {
139         try {
140             this.startedOnce.set(true);
141             this.listener.begin();
142             // Open the file
143             if (this.reader == null) {
144                 try {
145                     this.reader = new RandomAccessFile(this.file, TailerRun.RAF_MODE);
146                 } catch (final FileNotFoundException e) {
147                     this.listener.fileNotFound();
148                     return;
149                 }
150                 // The current position in the file
151                 this.position = this.end ? this.file.length() : 0;
152                 this.last = this.file.lastModified();
153                 this.reader.seek(this.position);
154             }
155             final boolean newer = FileUtils.isFileNewer(this.file, this.last); // IO-279,
156             // must
157             // be
158             // done
159             // first
160             // Check the file length to see if it was rotated
161             final long length = this.file.length();
162             if (length < this.position) {
163                 // File was rotated
164                 this.listener.fileRotated();
165                 // Reopen the reader after rotation
166                 try {
167                     // Ensure that the old file is closed iff we re-open it
168                     // successfully
169                     final RandomAccessFile save = this.reader;
170                     this.reader = new RandomAccessFile(this.file, TailerRun.RAF_MODE);
171                     // At this point, we're sure that the old file is rotated
172                     // Finish scanning the old file and then we'll start with
173                     // the new one
174                     try {
175                         this.readLines(save);
176                     } catch (final IOException ioe) {
177                         this.listener.handle(ioe);
178                     }
179                     this.position = 0;
180                     // close old file explicitly rather than relying on GC
181                     // picking up previous RAF
182                     IOUtils.closeQuietly(save);
183                 } catch (final FileNotFoundException e) {
184                     // in this case we continue to use the previous reader and
185                     // position values
186                     this.listener.fileNotFound();
187                 }
188             } else {
189                 // File was not rotated
190                 // See if the file needs to be read again
191                 if (length > this.position) {
192                     // The file has more content than it did last time
193                     this.position = this.readLines(this.reader);
194                     this.last = this.file.lastModified();
195                 } else if (newer) {
196                     /*
197                      * This can happen if the file is truncated or overwritten
198                      * with the exact same length of information. In cases like
199                      * this, the file position needs to be reset
200                      */
201                     this.position = 0;
202                     this.reader.seek(this.position); // cannot be null here
203 
204                     // Now we can read new lines
205                     this.position = this.readLines(this.reader);
206                     this.last = this.file.lastModified();
207                 }
208             }
209             if (this.reOpen) {
210                 IOUtils.closeQuietly(this.reader);
211                 this.reader = new RandomAccessFile(this.file, TailerRun.RAF_MODE);
212                 this.reader.seek(this.position);
213             }
214         } catch (final Exception e) {
215             this.listener.handle(e);
216         } finally {
217             this.listener.commit();
218         }
219     }
220 
221 }