source: branches/1.4.0/src/org/armedbear/lisp/util/RandomAccessCharacterFile.java

Last change on this file was 14863, checked in by Mark Evenson, 9 years ago

(Vibhu Mohindra) Fix RandomAccessCharacterFile?.java

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 19.7 KB
Line 
1/*
2 * RandomAccessCharacterFile.java
3 *
4 * Copyright (C) 2008 Hideo at Yokohama
5 * Copyright (C) 2008-2009 Erik Huelsmann
6 * $Id: RandomAccessCharacterFile.java 14863 2016-09-04 07:16:19Z mevenson $
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
21 *
22 * As a special exception, the copyright holders of this library give you
23 * permission to link this library with independent modules to produce an
24 * executable, regardless of the license terms of these independent
25 * modules, and to copy and distribute the resulting executable under
26 * terms of your choice, provided that you also meet, for each linked
27 * independent module, the terms and conditions of the license of that
28 * module.  An independent module is a module which is not derived from
29 * or based on this library.  If you modify this library, you may extend
30 * this exception to your version of the library, but you are not
31 * obligated to do so.  If you do not wish to do so, delete this
32 * exception statement from your version.
33 */
34
35package org.armedbear.lisp.util;
36
37import java.io.IOException;
38import java.io.PushbackInputStream;
39import java.io.OutputStream;
40import java.io.RandomAccessFile;
41import java.io.PushbackReader;
42import java.io.Reader;
43import java.io.StringReader;
44import java.io.Writer;
45import java.nio.ByteBuffer;
46import java.nio.CharBuffer;
47import java.nio.channels.FileChannel;
48import java.nio.charset.Charset;
49import java.nio.charset.CharsetDecoder;
50import java.nio.charset.CharsetEncoder;
51import java.nio.charset.CoderResult;
52import java.nio.charset.CodingErrorAction;
53import java.nio.charset.UnsupportedCharsetException;
54
55import org.armedbear.lisp.Debug;
56
57import static org.armedbear.lisp.Lisp.error;
58
59import org.armedbear.lisp.SimpleError;
60import org.armedbear.lisp.SimpleString;
61
62public class RandomAccessCharacterFile {
63
64    private class RandomAccessInputStream extends PushbackInputStream {
65
66        public RandomAccessInputStream() {
67            super(null);
68        }
69
70        private byte[] read_buf = new byte[1];
71
72        @Override
73        public final int read() throws IOException {
74            int len = read(read_buf);
75            if (len == 1) {
76                // byte is signed, char is unsigned, int is signed.
77                // buf can hold 0xff, we want it as 0xff in int, not -1.
78                return 0xff & (int) read_buf[0];
79            } else {
80                return -1;
81            }
82            // ### BUG: 'int read()' is to return a *codepoint*,
83            // not the half of a surrogate pair!
84        }
85
86        @Override
87        public final int read(byte[] b, int off, int len) throws IOException {
88            return RandomAccessCharacterFile.this.read(b, off, len);
89        }
90
91        @Override
92        public final void unread(int b) throws IOException {
93            RandomAccessCharacterFile.this.unreadByte((byte)b);
94        }
95
96        @Override
97        public final void unread(byte[] b, int off, int len) throws IOException {
98            for (int i = 0; i < len; i++)
99                this.unread(b[off+i]);
100        }
101
102        @Override
103        public final void unread(byte[] b) throws IOException {
104            this.unread(b, 0, b.length);
105        }
106
107        @Override
108        public final int available() throws IOException {
109            return (int)(RandomAccessCharacterFile.this.length()
110                            - RandomAccessCharacterFile.this.position());
111        }
112
113        @Override
114        public final synchronized void mark(int readlimit) {
115        }
116
117        @Override
118        public final boolean markSupported() {
119            return false;
120        }
121
122        @Override
123        public final synchronized void reset() throws IOException {
124            throw new IOException("Operation not supported");
125        }
126
127        @Override
128        public final long skip(long n) throws IOException {
129            RandomAccessCharacterFile.this.position(RandomAccessCharacterFile.this.position()+n);
130            return n;
131        }
132
133        @Override
134        public final int read(byte[] b) throws IOException {
135            return this.read(b, 0, b.length);
136        }
137
138        @Override
139        public final void close() throws IOException {
140            RandomAccessCharacterFile.this.close();
141        }
142    }
143
144    private class RandomAccessOutputStream extends OutputStream {
145
146        RandomAccessOutputStream() {
147        }
148
149        private byte[] buf = new byte[1];
150        public final void write(int b) throws IOException {
151            buf[0] = (byte)b;
152            RandomAccessCharacterFile.this.write(buf, 0, 1);
153        }
154
155        @Override
156        public final void write(byte[] b) throws IOException {
157            RandomAccessCharacterFile.this.write(b, 0, b.length);
158        }
159
160        @Override
161        public final void write(byte[] b, int off, int len) throws IOException {
162            RandomAccessCharacterFile.this.write(b, off, len);
163        }
164
165        @Override
166        public final void flush() throws IOException {
167            RandomAccessCharacterFile.this.flush();
168        }
169
170        @Override
171        public final void close() throws IOException {
172            RandomAccessCharacterFile.this.close();
173        }
174    }
175
176    // dummy reader which we need to call the Pushback constructor
177    // because a null value won't work
178    static Reader staticReader = new StringReader("");
179
180    private class RandomAccessReader extends PushbackReader {
181
182        RandomAccessReader() {
183                // because we override all methods of Pushbackreader,
184                // staticReader will never be referenced
185                super(staticReader);
186        }
187
188        @Override
189        public final void close() throws IOException {
190            RandomAccessCharacterFile.this.close();
191        }
192
193        private char[] read_buf = new char[1];
194
195        @Override
196        public final int read() throws IOException {
197            int n = this.read(read_buf);
198
199            if (n == 1)
200                return read_buf[0];
201            else
202                return -1;
203            // ### BUG: 'int read()' is to return a codepoint!
204            // not the half of a surrogate pair!
205        }
206
207        @Override
208        public final void unread(int c) throws IOException {
209            RandomAccessCharacterFile.this.unreadChar((char)c);
210        }
211
212        @Override
213        public final void unread(char[] cbuf, int off, int len) throws IOException {
214            for (int i = 0; i < len; i++)
215                this.unread(cbuf[off+i]);
216        }
217
218        @Override
219        public final void unread(char[] cbuf) throws IOException {
220            this.unread(cbuf, 0, cbuf.length);
221        }
222
223        @Override
224        public final int read(CharBuffer target) throws IOException {
225            //FIXME: to be implemented
226            throw new IOException("Not implemented");
227        }
228
229        @Override
230        public final int read(char[] cbuf) throws IOException {
231            return RandomAccessCharacterFile.this.read(cbuf, 0, cbuf.length);
232        }
233
234        @Override
235        public final int read(char[] cb, int off, int len) throws IOException {
236            return RandomAccessCharacterFile.this.read(cb, off, len);
237        }
238
239        @Override
240        public final boolean ready() throws IOException {
241            return true;
242        }
243    }
244
245    private class RandomAccessWriter extends Writer {
246
247        RandomAccessWriter() {
248        }
249
250        public final void close() throws IOException {
251            RandomAccessCharacterFile.this.close();
252        }
253
254        public final void flush() throws IOException {
255            RandomAccessCharacterFile.this.flush();
256        }
257
258        @Override
259        public final void write(char[] cb, int off, int len) throws IOException {
260            RandomAccessCharacterFile.this.write(cb, off, len);
261        }
262
263    }
264
265
266    final static int BUFSIZ = 4*1024; // setting this to a small value like 8 is helpful for testing.
267
268    private RandomAccessWriter writer;
269    private RandomAccessReader reader;
270    private RandomAccessInputStream inputStream;
271    private RandomAccessOutputStream outputStream;
272    private FileChannel fcn;
273
274    private Charset cset;
275    private CharsetEncoder cenc;
276    private CharsetDecoder cdec;
277
278    /**
279     * bbuf is treated as a cache of the file content.
280     * If it points to somewhere in the middle of the file, it holds the copy of the file content,
281     * even when you are writing a large chunk of data.  If you write in the middle of a file,
282     * bbuf first gets filled with contents of the data, and only after that any new data is
283     * written on bbuf.
284     * The exception is when you are appending data at the end of the file.
285     */
286    private ByteBuffer bbuf;
287    private boolean bbufIsDirty; /* whether bbuf holds data that must be written. */
288    private boolean bbufIsReadable; /* whether bbuf.remaining() contains readable content. */
289    private long bbufpos; /* where the beginning of bbuf is pointing in the file now. */
290
291    public RandomAccessCharacterFile(RandomAccessFile raf, String encoding) throws IOException {
292
293        fcn = raf.getChannel();
294
295        setEncoding(encoding);
296        bbuf = ByteBuffer.allocate(BUFSIZ);
297
298        // there is no readable data available in the buffers.
299        bbuf.flip();
300
301        // there is no write pending data in the buffers.
302        bbufIsDirty = false;
303
304        bbufIsReadable = true;
305
306        bbufpos = fcn.position();
307
308        reader = new RandomAccessReader();
309        writer = new RandomAccessWriter();
310        inputStream = new RandomAccessInputStream();
311        outputStream = new RandomAccessOutputStream();
312    }
313
314    public void setEncoding(String encoding) {
315      if (encoding == null) {
316        cset = Charset.defaultCharset();
317      } else {
318        try {
319          cset = Charset.forName(encoding);
320        } catch (UnsupportedCharsetException e) {
321          error(new SimpleError("Undefined encoding: " + encoding));
322        }
323      }
324      cdec = cset.newDecoder();
325      cdec.onMalformedInput(CodingErrorAction.REPLACE);
326      cdec.onUnmappableCharacter(CodingErrorAction.REPLACE);
327      cenc = cset.newEncoder();
328    }
329
330    public Writer getWriter() {
331        return writer;
332    }
333
334    public PushbackReader getReader() {
335        return reader;
336    }
337
338    public PushbackInputStream getInputStream() {
339        return inputStream;
340    }
341
342    public OutputStream getOutputStream() {
343        return outputStream;
344    }
345
346    public final void close() throws IOException {
347        internalFlush(true);
348        fcn.close();
349    }
350
351    public final void flush() throws IOException {
352        internalFlush(false);
353    }
354
355    private final boolean ensureReadBbuf(boolean force) throws IOException {
356        boolean bufReady = true;
357
358        if ((bbuf.remaining() == 0) || force || ! bbufIsReadable) {
359            // need to read from the file.
360
361            if (bbufIsDirty) {
362                flushBbuf(false);
363                bbuf.clear();
364                bbufIsReadable = false;
365            } else {
366                int bbufEnd = bbufIsReadable ? bbuf.limit() : bbuf.position();
367                fcn.position(bbufpos + bbufEnd);
368                bbufpos += bbuf.position();
369                if (bbufIsReadable) {
370                  bbuf.compact();
371                  bbufIsReadable = false;
372                } else //must discard the junk bytes after bbuf.position()
373                  bbuf.clear();
374            }
375
376            bufReady = (fcn.read(bbuf) != -1);
377            bbuf.flip();
378            bbufIsReadable = true;
379        }
380
381        return bufReady;
382    }
383
384
385    final int read(char[] cb, int off, int len) throws IOException {
386        CharBuffer cbuf = CharBuffer.wrap(cb, off, len);
387        boolean decodeWasUnderflow = false;
388        boolean atEof = false;
389        while ((cbuf.remaining() > 0) && ! atEof) {
390            int oldRemaining = cbuf.remaining();
391            atEof = ! ensureReadBbuf(decodeWasUnderflow);
392            CoderResult r = cdec.decode(bbuf, cbuf, atEof );
393            if (oldRemaining == cbuf.remaining()
394                && CoderResult.OVERFLOW == r) {
395                // if this happens, the decoding failed
396                // but the bufs didn't advance. Advance
397                // them manually and do manual replacing,
398                // otherwise we loop endlessly. This occurs
399                // at least when parsing latin1 files with
400                // lowercase o-umlauts in them
401                // Note that this is at the moment copy-paste
402                // with DecodingReader.read()
403                cbuf.put('?');
404                bbuf.get();
405            }
406            decodeWasUnderflow = (CoderResult.UNDERFLOW == r);
407        }
408        if (cbuf.remaining() == len) {
409            return -1;
410        } else {
411            return len - cbuf.remaining();
412        }
413    }
414
415    final void write(char[] cb, int off, int len) throws IOException {
416        CharBuffer cbuf = CharBuffer.wrap(cb, off, len);
417        encodeAndWrite(cbuf, false, false);
418    }
419
420    private final void internalFlush(boolean endOfFile) throws IOException {
421        if (endOfFile) {
422            CharBuffer cbuf = CharBuffer.allocate(0);
423            encodeAndWrite(cbuf, true, endOfFile);
424        } else {
425            flushBbuf(false);
426        }
427    }
428
429    private final void encodeAndWrite(CharBuffer cbuf, boolean flush,
430                                      boolean endOfFile) throws IOException {
431        while (cbuf.remaining() > 0) {
432            CoderResult r = cenc.encode(cbuf, bbuf, endOfFile);
433            bbufIsDirty = true;
434            if (CoderResult.OVERFLOW == r || bbuf.remaining() == 0) {
435                flushBbuf(false);
436                bbuf.clear();
437                bbufIsReadable = false;
438            }
439            if (r.isUnmappable()) {
440                throw new RACFUnmappableCharacterException(cbuf.position(),
441                                                           cbuf.charAt(cbuf.position()),
442                                                           cset.name());
443            }
444            if (r.isMalformed()) {
445                // We don't really expect Malformed, but not handling it
446                // will cause an infinite loop if we don't...
447                throw new RACFMalformedInputException(cbuf.position(),
448                                                      cbuf.charAt(cbuf.position()),
449                                                      cset.name());
450            }
451            // UNDERFLOW is the normal condition where cbuf runs out
452            // before bbuf is filled.
453        }
454        if (bbuf.position() > 0 && bbufIsDirty && flush) {
455            flushBbuf(false);
456        }
457    }
458
459    public final void position(long newPosition) throws IOException {
460        flushBbuf(true);
461        long bbufend = bbufpos // in case bbuf is readable, its contents is valid
462            + (bbufIsReadable ? bbuf.limit() : bbuf.position()); // beyond position()
463        if (newPosition >= bbufpos && newPosition < bbufend) {
464            // near seek. within existing data of bbuf.
465            if (!bbufIsReadable) { //rewinding. keep tail buffered.
466              bbuf.limit(bbuf.position());
467              bbufIsReadable = true;
468            }
469            bbuf.position((int)(newPosition - bbufpos));
470        } else {
471            fcn.position(newPosition);
472            // far seek; discard the buffer (it's already cleared)
473            bbuf.clear();
474            bbuf.flip(); // "there is no useful data on this buffer yet."
475            bbufIsReadable = true;
476            bbufpos = newPosition;
477        }
478    }
479
480    public final long position() throws IOException {
481        return bbufpos + bbuf.position(); // the logical position within the file.
482    }
483
484    public final long length() throws IOException {
485        flushBbuf(true);
486        return fcn.size();
487    }
488
489    final void flushBbuf(boolean commitOnly) throws IOException {
490        if (commitOnly && !bbufIsDirty)
491            return;
492        //otherwise, we do at least need to increase bbufpos
493
494        fcn.position(bbufpos);
495
496        // if the buffer is dirty, the modifications have to be
497        // before position(): before re-positioning, this.position()
498        // calls this function.
499        if (commitOnly) {
500            ByteBuffer dup = bbuf.duplicate();
501            dup.flip();
502            fcn.write(dup);
503            //ideally, should restore fcn.position(). but don't for performance.
504//            fcn.position(fcn.position()-dup.position());
505            bbufIsDirty = false; //this fixed stas's bug, but not mine.
506            return;
507        }
508       
509        if (bbufIsDirty) {
510          bbuf.flip();
511          fcn.write(bbuf);
512        }
513
514        bbufpos += bbuf.position();
515        bbuf.clear();
516        bbuf.flip(); // there's no useable data in this buffer
517        bbufIsDirty = false;
518        bbufIsReadable = true;
519    }
520
521    public final int read(byte[] b, int off, int len) throws IOException {
522        int pos = off;
523        boolean atEof = false;
524        while (pos - off < len && ! atEof) {
525
526            atEof = ! ensureReadBbuf(false);
527            int want = Math.min(off + len - pos, bbuf.remaining());
528            bbuf.get(b, pos, want);
529            pos += want;
530        }
531        return pos - off;
532    }
533
534    // a method corresponding to the good ol' ungetc in C.
535    // This function may fail when using (combined) character codes that use
536    // escape sequences to switch between sub-codes.
537    // ASCII, ISO-8859 series, any 8bit code are OK, all unicode variations are OK,
538    // but applications of the ISO-2022 encoding framework can have trouble.
539    // Example of such code is ISO-2022-JP which is used in Japanese e-mail.
540    private CharBuffer singleCharBuf;
541    private ByteBuffer shortByteBuf;
542    public final void unreadChar(char c) throws IOException {
543        // algorithm :
544        //  1. encode c into bytes, to find out how many bytes it corresponds to
545        //  2. move the position backwards that many bytes.
546        //  ** we stop here.  Don't bother to write the bytes to the buffer,
547        //     assuming that it is the same as the original data.
548        //     If we allow to write back different characters, the buffer must get 'dirty'
549        //     but that would require read/write permissions on files you use unreadChar,
550        //     even if you are just reading for some tokenizer.
551        //
552        //  So we don't do the following.
553        //  3. write the bytes.
554        //  4. move the position back again.
555        if (singleCharBuf == null) {
556            singleCharBuf = CharBuffer.allocate(1);
557            shortByteBuf = ByteBuffer.allocate((int)cenc.maxBytesPerChar());
558        }
559        singleCharBuf.clear();
560        singleCharBuf.append(c);
561        singleCharBuf.flip();
562        shortByteBuf.clear();
563        cenc.encode(singleCharBuf, shortByteBuf, false);
564        int n = shortByteBuf.position();
565        long pos = position() - n;
566        position(pos);
567    }
568
569    public final void unreadByte(byte b) throws IOException {
570        long pos = position() - 1;
571        position(pos);
572    }
573
574    final void write(byte[] b, int off, int len) throws IOException {
575        int pos = off;
576        while (pos < off + len) {
577            if (bbuf.remaining() == 0) {
578                flushBbuf(false);
579                bbuf.clear();
580                bbufIsReadable = false;
581            }
582            int thisBatchLen = Math.min(off + len - pos, bbuf.remaining());
583            bbuf.put(b, pos, thisBatchLen);
584            pos += thisBatchLen;
585            bbufIsDirty = true;
586        }
587    }
588}
Note: See TracBrowser for help on using the repository browser.