1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.io.input;
18
19 import java.io.Closeable;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.UnsupportedEncodingException;
23 import java.nio.ByteBuffer;
24 import java.nio.channels.SeekableByteChannel;
25 import java.nio.charset.Charset;
26 import java.nio.charset.CharsetEncoder;
27 import java.nio.charset.StandardCharsets;
28 import java.nio.file.Files;
29 import java.nio.file.Path;
30 import java.nio.file.StandardOpenOption;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collections;
34 import java.util.List;
35
36 import org.apache.commons.io.Charsets;
37 import org.apache.commons.io.FileSystem;
38 import org.apache.commons.io.StandardLineSeparator;
39 import org.apache.commons.io.build.AbstractStreamBuilder;
40
41
42
43
44
45
46
47
48
49
50 public class ReversedLinesFileReader implements Closeable {
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 public static class Builder extends AbstractStreamBuilder<ReversedLinesFileReader, Builder> {
72
73
74
75
76 public Builder() {
77 setBufferSizeDefault(DEFAULT_BLOCK_SIZE);
78 setBufferSize(DEFAULT_BLOCK_SIZE);
79 }
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 @Override
104 public ReversedLinesFileReader get() throws IOException {
105 return new ReversedLinesFileReader(getPath(), getBufferSize(), getCharset());
106 }
107
108 }
109
110 private final class FilePart {
111 private final long no;
112
113 private final byte[] data;
114
115 private byte[] leftOver;
116
117 private int currentLastBytePos;
118
119
120
121
122
123
124
125
126
127 private FilePart(final long no, final int length, final byte[] leftOverOfLastFilePart) throws IOException {
128 this.no = no;
129 final int dataLength = length + (leftOverOfLastFilePart != null ? leftOverOfLastFilePart.length : 0);
130 this.data = new byte[dataLength];
131 final long off = (no - 1) * blockSize;
132
133
134 if (no > 0 ) {
135 channel.position(off);
136 final int countRead = channel.read(ByteBuffer.wrap(data, 0, length));
137 if (countRead != length) {
138 throw new IllegalStateException("Count of requested bytes and actually read bytes don't match");
139 }
140 }
141
142 if (leftOverOfLastFilePart != null) {
143 System.arraycopy(leftOverOfLastFilePart, 0, data, length, leftOverOfLastFilePart.length);
144 }
145 this.currentLastBytePos = data.length - 1;
146 this.leftOver = null;
147 }
148
149
150
151
152 private void createLeftOver() {
153 final int lineLengthBytes = currentLastBytePos + 1;
154 if (lineLengthBytes > 0) {
155
156 leftOver = Arrays.copyOf(data, lineLengthBytes);
157 } else {
158 leftOver = null;
159 }
160 currentLastBytePos = -1;
161 }
162
163
164
165
166
167
168
169
170 private int getNewLineMatchByteCount(final byte[] data, final int i) {
171 for (final byte[] newLineSequence : newLineSequences) {
172 boolean match = true;
173 for (int j = newLineSequence.length - 1; j >= 0; j--) {
174 final int k = i + j - (newLineSequence.length - 1);
175 match &= k >= 0 && data[k] == newLineSequence[j];
176 }
177 if (match) {
178 return newLineSequence.length;
179 }
180 }
181 return 0;
182 }
183
184
185
186
187
188
189 private String readLine() {
190
191 String line = null;
192 int newLineMatchByteCount;
193
194 final boolean isLastFilePart = no == 1;
195
196 int i = currentLastBytePos;
197 while (i > -1) {
198
199 if (!isLastFilePart && i < avoidNewlineSplitBufferSize) {
200
201
202 createLeftOver();
203 break;
204 }
205
206
207 if ((newLineMatchByteCount = getNewLineMatchByteCount(data, i)) > 0 ) {
208 final int lineStart = i + 1;
209 final int lineLengthBytes = currentLastBytePos - lineStart + 1;
210
211 if (lineLengthBytes < 0) {
212 throw new IllegalStateException("Unexpected negative line length=" + lineLengthBytes);
213 }
214 final byte[] lineData = Arrays.copyOfRange(data, lineStart, lineStart + lineLengthBytes);
215
216 line = new String(lineData, charset);
217
218 currentLastBytePos = i - newLineMatchByteCount;
219 break;
220 }
221
222
223 i -= byteDecrement;
224
225
226 if (i < 0) {
227 createLeftOver();
228 break;
229 }
230 }
231
232
233 if (isLastFilePart && leftOver != null) {
234
235 line = new String(leftOver, charset);
236 leftOver = null;
237 }
238
239 return line;
240 }
241
242
243
244
245
246
247
248 private FilePart rollOver() throws IOException {
249
250 if (currentLastBytePos > -1) {
251 throw new IllegalStateException("Current currentLastCharPos unexpectedly positive... "
252 + "last readLine() should have returned something! currentLastCharPos=" + currentLastBytePos);
253 }
254
255 if (no > 1) {
256 return new FilePart(no - 1, blockSize, leftOver);
257 }
258
259 if (leftOver != null) {
260 throw new IllegalStateException("Unexpected leftover of the last block: leftOverOfThisFilePart="
261 + new String(leftOver, charset));
262 }
263 return null;
264 }
265 }
266
267 private static final String EMPTY_STRING = "";
268
269 private static final int DEFAULT_BLOCK_SIZE = FileSystem.getCurrent().getBlockSize();
270
271
272
273
274
275
276
277 public static Builder builder() {
278 return new Builder();
279 }
280
281 private final int blockSize;
282 private final Charset charset;
283 private final SeekableByteChannel channel;
284 private final long totalByteLength;
285 private final long totalBlockCount;
286 private final byte[][] newLineSequences;
287 private final int avoidNewlineSplitBufferSize;
288 private final int byteDecrement;
289 private FilePart currentFilePart;
290 private boolean trailingNewlineOfFileSkipped;
291
292
293
294
295
296
297
298
299
300 @Deprecated
301 public ReversedLinesFileReader(final File file) throws IOException {
302 this(file, DEFAULT_BLOCK_SIZE, Charset.defaultCharset());
303 }
304
305
306
307
308
309
310
311
312
313
314
315 @Deprecated
316 public ReversedLinesFileReader(final File file, final Charset charset) throws IOException {
317 this(file.toPath(), charset);
318 }
319
320
321
322
323
324
325
326
327
328
329
330
331
332 @Deprecated
333 public ReversedLinesFileReader(final File file, final int blockSize, final Charset charset) throws IOException {
334 this(file.toPath(), blockSize, charset);
335 }
336
337
338
339
340
341
342
343
344
345
346
347
348
349 @Deprecated
350 public ReversedLinesFileReader(final File file, final int blockSize, final String charsetName) throws IOException {
351 this(file.toPath(), blockSize, charsetName);
352 }
353
354
355
356
357
358
359
360
361
362
363
364 @Deprecated
365 public ReversedLinesFileReader(final Path file, final Charset charset) throws IOException {
366 this(file, DEFAULT_BLOCK_SIZE, charset);
367 }
368
369
370
371
372
373
374
375
376
377
378
379
380
381 @Deprecated
382 public ReversedLinesFileReader(final Path file, final int blockSize, final Charset charset) throws IOException {
383 this.blockSize = blockSize;
384 this.charset = Charsets.toCharset(charset);
385
386
387 final CharsetEncoder charsetEncoder = this.charset.newEncoder();
388 final float maxBytesPerChar = charsetEncoder.maxBytesPerChar();
389 if (maxBytesPerChar == 1f || this.charset == StandardCharsets.UTF_8) {
390
391 byteDecrement = 1;
392 } else if (this.charset == Charset.forName("Shift_JIS") ||
393
394 this.charset == Charset.forName("windows-31j") ||
395 this.charset == Charset.forName("x-windows-949") ||
396 this.charset == Charset.forName("gbk") ||
397 this.charset == Charset.forName("x-windows-950")) {
398 byteDecrement = 1;
399 } else if (this.charset == StandardCharsets.UTF_16BE || this.charset == StandardCharsets.UTF_16LE) {
400
401
402
403 byteDecrement = 2;
404 } else if (this.charset == StandardCharsets.UTF_16) {
405 throw new UnsupportedEncodingException(
406 "For UTF-16, you need to specify the byte order (use UTF-16BE or " + "UTF-16LE)");
407 } else {
408 throw new UnsupportedEncodingException(
409 "Encoding " + charset + " is not supported yet (feel free to " + "submit a patch)");
410 }
411
412
413
414 this.newLineSequences = new byte[][] {
415 StandardLineSeparator.CRLF.getBytes(this.charset),
416 StandardLineSeparator.LF.getBytes(this.charset),
417 StandardLineSeparator.CR.getBytes(this.charset)
418 };
419
420 this.avoidNewlineSplitBufferSize = newLineSequences[0].length;
421
422
423 this.channel = Files.newByteChannel(file, StandardOpenOption.READ);
424 this.totalByteLength = channel.size();
425 int lastBlockLength = (int) (this.totalByteLength % blockSize);
426 if (lastBlockLength > 0) {
427 this.totalBlockCount = this.totalByteLength / blockSize + 1;
428 } else {
429 this.totalBlockCount = this.totalByteLength / blockSize;
430 if (this.totalByteLength > 0) {
431 lastBlockLength = blockSize;
432 }
433 }
434 this.currentFilePart = new FilePart(totalBlockCount, lastBlockLength, null);
435
436 }
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451 @Deprecated
452 public ReversedLinesFileReader(final Path file, final int blockSize, final String charsetName) throws IOException {
453 this(file, blockSize, Charsets.toCharset(charsetName));
454 }
455
456
457
458
459
460
461 @Override
462 public void close() throws IOException {
463 channel.close();
464 }
465
466
467
468
469
470
471
472 public String readLine() throws IOException {
473
474 String line = currentFilePart.readLine();
475 while (line == null) {
476 currentFilePart = currentFilePart.rollOver();
477 if (currentFilePart == null) {
478
479 break;
480 }
481 line = currentFilePart.readLine();
482 }
483
484
485 if (EMPTY_STRING.equals(line) && !trailingNewlineOfFileSkipped) {
486 trailingNewlineOfFileSkipped = true;
487 line = readLine();
488 }
489
490 return line;
491 }
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508 public List<String> readLines(final int lineCount) throws IOException {
509 if (lineCount < 0) {
510 throw new IllegalArgumentException("lineCount < 0");
511 }
512 final ArrayList<String> arrayList = new ArrayList<>(lineCount);
513 for (int i = 0; i < lineCount; i++) {
514 final String line = readLine();
515 if (line == null) {
516 return arrayList;
517 }
518 arrayList.add(line);
519 }
520 return arrayList;
521 }
522
523
524
525
526
527
528
529
530
531
532
533
534
535 public String toString(final int lineCount) throws IOException {
536 final List<String> lines = readLines(lineCount);
537 Collections.reverse(lines);
538 return lines.isEmpty() ? EMPTY_STRING : String.join(System.lineSeparator(), lines) + System.lineSeparator();
539 }
540
541 }