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

Last change on this file was 12902, checked in by vvoutilainen, 14 years ago

Fix reading of data containing scandinavian latin1 characters
correctly, and add a simple test for it. The utf-8 test is
just a sanity test so that umlauts as utf-8 aren't broken, the
latin1 test properly fails without this patch and passes
with this patch.

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