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

Last change on this file was 14735, checked in by Mark Evenson, 10 years ago

Signal SIMPLE-ERROR for invalid external-format arguments.

Addresses <http://abcl.org/trac/ticket/375>.

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