001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.schematic.internal.io;
017
018import java.io.DataInput;
019import java.io.DataInputStream;
020import java.io.EOFException;
021import java.io.IOException;
022import java.lang.ref.SoftReference;
023import java.nio.ByteBuffer;
024import java.nio.CharBuffer;
025import java.nio.charset.CharsetDecoder;
026import java.nio.charset.CoderResult;
027import java.nio.charset.StandardCharsets;
028
029/**
030 * An implementation of {@link DataInput} with additional methods needed for reading BSON formatted content from another DataInput
031 * instance. Specifically, this class reads little-endian byte order (purposefully in violation of the DataInput interface
032 * specification) and provides a way to read C-style strings (where the length is not known up front but instead contain all
033 * characters until the zero-byte terminator), which are commonly used within the BSON specification.
034 * 
035 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc.
036 */
037public class BsonDataInput implements DataInput {
038
039    /**
040     * A thread-local cache of ByteBuffer and CharBuffer instances. Reusing these buffers results in fewer allocations and less to
041     * garbage collect. Note that this cache has to be thread-local, since {@link BufferCache} is not thread-safe.
042     */
043    private final static ThreadLocal<SoftReference<BufferCache>> BUFFER_CACHE = new ThreadLocal<>();
044
045    private static BufferCache getBufferCache() {
046        SoftReference<BufferCache> ref = BUFFER_CACHE.get();
047        if (ref == null || ref.get() == null) {
048            ref = new SoftReference<>(new BufferCache());
049            BUFFER_CACHE.set(ref);
050        }
051        return ref.get();
052    }
053
054    private final DataInput input;
055    private int total = 0;
056
057    public BsonDataInput( DataInput input ) {
058        this.input = input;
059    }
060
061    /**
062     * Returns the number of bytes that have been read from this input.
063     * 
064     * @return the total number of bytes already read
065     * @exception IOException if an I/O error occurs.
066     */
067    public int getTotalBytesRead() throws IOException {
068        return total;
069    }
070
071    @Override
072    public final int readUnsignedByte() throws IOException {
073        int b1 = input.readUnsignedByte();
074        if (b1 == -1) {
075            throw new EOFException();
076        }
077        ++total;
078        return (byte)b1;
079    }
080
081    @Override
082    public boolean readBoolean() throws IOException {
083        return readUnsignedByte() != 0;
084    }
085
086    @Override
087    public byte readByte() throws IOException {
088        return (byte)readUnsignedByte();
089    }
090
091    @Override
092    public char readChar() throws IOException {
093        return (char)readUnsignedShort();
094    }
095
096    @Override
097    public int readUnsignedShort() throws IOException {
098        byte b1 = (byte)readUnsignedByte();
099        byte b2 = (byte)readUnsignedByte();
100        return (b2 & 0xFF) << 8 | (b1 & 0xFF);
101    }
102
103    @Override
104    public short readShort() throws IOException {
105        return (short)readUnsignedShort();
106    }
107
108    @Override
109    public int readInt() throws IOException {
110        byte b1 = (byte)readUnsignedByte();
111        byte b2 = (byte)readUnsignedByte();
112        byte b3 = (byte)readUnsignedByte();
113        byte b4 = (byte)readUnsignedByte();
114        return (b4 & 0xFF) << 24 | (b3 & 0xFF) << 16 | (b2 & 0xFF) << 8 | (b1 & 0xFF);
115    }
116
117    @Override
118    public long readLong() throws IOException {
119        byte b1 = (byte)readUnsignedByte();
120        byte b2 = (byte)readUnsignedByte();
121        byte b3 = (byte)readUnsignedByte();
122        byte b4 = (byte)readUnsignedByte();
123        byte b5 = (byte)readUnsignedByte();
124        byte b6 = (byte)readUnsignedByte();
125        byte b7 = (byte)readUnsignedByte();
126        byte b8 = (byte)readUnsignedByte();
127        return (b8 & 0xFFL) << 56 | (b7 & 0xFFL) << 48 | (b6 & 0xFFL) << 40 | (b5 & 0xFFL) << 32 | (b4 & 0xFFL) << 24
128               | (b3 & 0xFFL) << 16 | (b2 & 0xFFL) << 8 | (b1 & 0xFFL);
129    }
130
131    @Override
132    public float readFloat() throws IOException {
133        return Float.intBitsToFloat(readInt());
134    }
135
136    @Override
137    public double readDouble() throws IOException {
138        return Double.longBitsToDouble(readLong());
139    }
140
141    @Override
142    public void readFully( byte[] b ) throws IOException {
143        readFully(b, 0, b.length);
144    }
145
146    @Override
147    public void readFully( byte[] b,
148                           int off,
149                           int len ) throws IOException {
150        while (len > 0) {
151            int read = read(b, off, len);
152            if (read < 0) {
153                throw new EOFException();
154            }
155            len -= read;
156            off += read;
157        }
158    }
159
160    protected int read( byte[] b,
161                        int off,
162                        int len ) throws IOException {
163        if (b == null) {
164            throw new NullPointerException();
165        } else if (off < 0 || len < 0 || len > b.length - off) {
166            throw new IndexOutOfBoundsException();
167        } else if (len == 0) {
168            return 0;
169        }
170
171        int i = 0;
172        try {
173            for ( ; i < len; i++) {
174                b[off + i] = read();
175            }
176        } catch (EOFException ee) {
177            return -1;
178        }
179        return i;
180    }
181
182    protected final byte read() throws IOException {
183        byte result = input.readByte();
184        ++total;
185        return result;
186    }
187
188    @Override
189    public int skipBytes( int n ) throws IOException {
190        if (n <= 0) return 0;
191        int skipped = input.skipBytes(n);
192        total += skipped;
193        return skipped;
194    }
195
196    @Override
197    public String readLine() {
198        throw new UnsupportedOperationException();
199    }
200
201    @Override
202    public String readUTF() throws IOException {
203        return DataInputStream.readUTF(this);
204    }
205
206    /**
207     * Read a UTF-8 string with the supplied length or, if the length is not known, the UTF-8 characters until the next zero-byte
208     * value. Note that this is different than the {@link #readUTF() standard way} to read a string, which always expects the
209     * length to be the first value on the stream.
210     * 
211     * @param len the number of bytes to read, or -1 if the length is not known and characters should be read until the next
212     *        zero-byte string.
213     * @return the read UTF-8 string
214     * @throws IOException
215     */
216    public String readUTF( int len ) throws IOException {
217        return readUTF(this, len);
218    }
219
220    /**
221     * Utility method to read a UTF-8 string from the supplied DataInput object given the supplied number of bytes to read or, if
222     * the number of bytes is not known, all of the UTF-8 characters until the next zero-byte value. Note that this is different
223     * than the {@link #readUTF() standard way} to read a string, which always expects the length to be the first value on the
224     * stream.
225     * 
226     * @param dis the DataInput from which the UTF-8 string is to be read
227     * @param len the number of bytes to read, or -1 if the length is not known and characters should be read until the next
228     *        zero-byte string.
229     * @return the read UTF-8 string
230     * @throws IOException if there is a problem reading from the input
231     */
232    public static String readUTF( DataInput dis,
233                                  int len ) throws IOException {
234        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
235        BufferCache bufferCache = getBufferCache();
236        ByteBuffer byteBuf = bufferCache.getByteBuffer(BufferCache.MINIMUM_SIZE);
237        CharBuffer charBuf = bufferCache.getCharBuffer(BufferCache.MINIMUM_SIZE);
238        try {
239            byte[] bytes = byteBuf.array();
240            while (len != 0 || byteBuf.position() > 0) {
241                // Need to read more bytes ...
242                if (len < 0) {
243                    // Read until we come across the zero-byte (or until we fill up 'byteBuf') ...
244                    while (byteBuf.remaining() != 0) {
245                        byte b = dis.readByte();
246                        if (b == 0x0) {
247                            len = 0; // no more bytes to read ...
248                            break;
249                        }
250                        byteBuf.put(b);
251                    }
252                    // Prepare byteBuf for reading ...
253                    byteBuf.flip();
254                } else if (len == 0) {
255                    // Don't read anything, but prepare the byteBuf for reading ...
256                    byteBuf.flip();
257                } else {
258                    // We know exactly how much we should read ...
259                    int amountToRead = Math.min(len, Math.min(byteBuf.remaining(), charBuf.remaining()));
260                    int offset = byteBuf.position(); // may have already read some bytes ...
261                    dis.readFully(bytes, offset, amountToRead);
262                    // take into account the offset because we might have carry-over bytes from a previous compaction 
263                    // this happens when decoding multi-byte UTF8 chars
264                    byteBuf.limit(amountToRead + offset); 
265                    byteBuf.rewind();
266                    // Adjust the number of bytes to read ...
267                    len -= amountToRead;
268                }
269
270                // We've either read all we need to or as much as we can (given the buffer's limited size),
271                // so decode what we've read ...
272                boolean endOfInput = len == 0;
273                CoderResult result = decoder.decode(byteBuf, charBuf, endOfInput);
274                if (result.isError()) {
275                    result.throwException();
276                } else if (result.isUnderflow()) {
277                    // We've not read enough bytes yet, so move the bytes that weren't read to the beginning of the buffer
278                    byteBuf.compact();
279                    // and try again ...
280                }
281                if (len > 0 && (charBuf.remaining() == 0 || result.isOverflow())) {
282                    // the output buffer was too small or is at its end.
283                    // allocate a new one which need to have enough capacity to hold the current one (we're appending buffers) and also to hold
284                    // the data from the new iteration.
285                    int newBufferIncrement = (len >  BufferCache.MINIMUM_SIZE) ?  BufferCache.MINIMUM_SIZE : len;
286                    CharBuffer newBuffer = bufferCache.getCharBuffer(charBuf.capacity() + newBufferIncrement);
287                    // prepare the old buffer for reading ...
288                    charBuf.flip();
289                    // and copy the contents ...
290                    newBuffer.put(charBuf);
291                    // and use the new buffer ...
292                    charBuf = newBuffer;
293                }
294            }
295            // We're done, so prepare the character buffer for reading and then convert to a String ...
296            charBuf.flip();
297            return charBuf.toString();
298        } finally {
299            // Return the buffers to the cache ...
300            bufferCache.checkin(byteBuf);
301            bufferCache.checkin(charBuf);
302        }
303    }
304}