/* * ModeShape (http://www.modeshape.org) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.modeshape.schematic.internal.io; import java.io.DataOutput; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.CharBuffer; import java.nio.channels.Channels; import java.nio.channels.WritableByteChannel; import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.StandardCharsets; import java.text.CharacterIterator; import java.text.StringCharacterIterator; import java.util.ArrayList; import java.util.List; /** * An implementation of {@link DataOutput} with additional methods needed for writing BSON formatted content. Specifically, this * class reads little-endian byte order (purposefully in violation of the DataInput interface specification) and provides a way to * read C-style strings (where the length is not known up front but instead contain all characters until the zero-byte * terminator), which are commonly used within the BSON specification. * * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. */ public class BsonDataOutput implements DataOutput { private static final int DEFAULT_BUFFER_SIZE = 1048 * 8; private final List<ByteBuffer> buffers = new ArrayList<>(); private final int bufferSize; private final byte[] bytes = new byte[8]; private int position = 0; private int size = 0; private ByteBuffer remainderBuffer; public BsonDataOutput() { this.bufferSize = DEFAULT_BUFFER_SIZE; } protected ByteBuffer getBufferFor( int position ) { int bufferIndex = position / bufferSize; while (bufferIndex >= buffers.size()) { buffers.add(newByteBuffer(bufferSize)); } return buffers.get(bufferIndex); } protected ByteBuffer newByteBuffer( int bufferSize ) { return ByteBuffer.allocate(bufferSize).order(ByteOrder.LITTLE_ENDIAN); } protected void updateSize( int newSize ) { if (size < newSize) { size = newSize; } } public int size() { return size; } @Override public void writeByte( int value ) { writeByte(position, value); position += 1; } public void writeByte( int position, int value ) { updateSize(position + 1); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; buffer.put(index, (byte)value); } @Override public void write( byte[] b ) { write(position, b, 0, b.length); position += b.length; } @Override public void write( byte[] b, int off, int len ) { write(position, b, off, len); position += len; } public void write( int position, byte[] value, int offset, int length ) { updateSize(position + length); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; for (int i = offset; i != length; ++i) { byte b = value[i]; if (index == bufferSize) { // We have to use the next buffer ... buffer = getBufferFor(position); index = position % bufferSize; } buffer.put(index, b); ++index; ++position; } } @Override public void write( int b ) { writeByte((byte) b); } @Override public void writeBoolean( boolean value ) { writeBoolean(position, value); position += 1; } public void writeBoolean( int position, boolean value ) { updateSize(position + 1); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; buffer.put(index, value ? (byte)1 : (byte)0); } @Override public void writeChar( int value ) { writeChar(position, value); position += 2; } public void writeChar( int position, int value ) { updateSize(position + 2); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; if (buffer.limit() - index >= 2) { // There's enough room to write on the buffer ... buffer.putChar(index, (char)value); } else { // The value will have to span buffers ... bytes[0] = (byte)value; bytes[1] = (byte)(value >> 8); write(position, bytes, 0, 2); } } @Override public void writeInt( int value ) { writeInt(position, value); position += 4; } public void writeInt( int position, int value ) { updateSize(position + 4); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; if (buffer.limit() - index >= 4) { // There's enough room to write on the buffer ... buffer.putInt(index, value); } else { // The value will have to span buffers ... bytes[0] = (byte)value; bytes[1] = (byte)(value >> 8); bytes[2] = (byte)(value >> 16); bytes[3] = (byte)(value >> 24); write(position, bytes, 0, 4); } } @Override public void writeShort( int value ) { writeShort(position, value); position += 2; } public void writeShort( int position, int value ) { updateSize(position + 2); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; if (buffer.limit() - index >= 2) { // There's enough room to write on the buffer ... buffer.putShort(index, (short)value); } else { // The value will have to span buffers ... bytes[0] = (byte)value; bytes[1] = (byte)(value >> 8); write(position, bytes, 0, 2); } } @Override public void writeLong( long value ) { writeLong(position, value); position += 8; } public void writeLong( int position, long value ) { updateSize(position + 8); ByteBuffer buffer = getBufferFor(position); int index = position % bufferSize; if (buffer.limit() - index >= 8) { // There's enough room to write on the buffer ... buffer.putLong(index, value); } else { // The value will have to span buffers ... bytes[0] = (byte)value; bytes[1] = (byte)(value >> 8); bytes[2] = (byte)(value >> 16); bytes[3] = (byte)(value >> 24); bytes[4] = (byte)(value >> 32); bytes[5] = (byte)(value >> 40); bytes[6] = (byte)(value >> 48); bytes[7] = (byte)(value >> 56); write(position, bytes, 0, 8); } } @Override public void writeFloat( float value ) { writeFloat(position, value); position += 4; } public void writeFloat( int position, float value ) { writeInt(position, Float.floatToRawIntBits(value)); } @Override public void writeDouble( double value ) { writeDouble(position, value); position += 8; } public void writeDouble( int position, double value ) { writeLong(position, Double.doubleToRawLongBits(value)); } /** * Writes a string to the output stream. For every character in the string <code>s</code>, taken in order, one byte is written * to the output stream. If <code>s</code> is <code>null</code>, a <code>NullPointerException</code> is thrown. * <p> * If <code>s.length</code> is zero, then no bytes are written. Otherwise, the character <code>s[0]</code> is written first, * then <code>s[1]</code>, and so on; the last character written is <code>s[s.length-1]</code>. For each character, one byte * is written, the low-order byte, in exactly the manner of the <code>writeByte</code> method . The high-order eight bits of * each character in the string are ignored. * * @param str the string value to be written. * @deprecated The semantics of {@code writeBytes(String s)} are considered dangerous. Please use {@link #writeUTF(String s)}, * {@link #writeChars(String s)} or another write method instead. */ @Deprecated @Override public void writeBytes( String str ) { CharacterIterator iter = new StringCharacterIterator(str); for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { writeByte(c); } } /** * Writes every character in the string <code>s</code>, to the output stream, in order, two bytes per character. If * <code>s</code> is <code>null</code>, a <code>NullPointerException</code> is thrown. If <code>s.length</code> is zero, then * no characters are written. Otherwise, the character <code>s[0]</code> is written first, then <code>s[1]</code>, and so on; * the last character written is <code>s[s.length-1]</code>. For each character, two bytes are actually written, low-order * byte first, in exactly the manner of the <code>writeChar</code> method. * * @param str the string value to be written. */ @Override public void writeChars( String str ) { CharacterIterator iter = new StringCharacterIterator(str); for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { writeChar(c); } } @Override public void writeUTF( String str ) { int numBytesWritten = 0; int numBytesPosition = this.position; // Write a placeholder for the length ... writeShort(numBytesPosition, numBytesWritten); numBytesWritten = writeUTF(position, str); position += numBytesWritten; // Now write the real number of bytes written ... writeShort(numBytesPosition, numBytesWritten); } /** * Writes the <a href="DataInput.html#modified-utf-8">modified UTF-8</a> representation of every character in the string * <code>s</code>. <i>This is similar to the standard {@link #writeUTF(String)} but without the leading two byte length.</i> * If <code>s</code> is <code>null</code>, a <code>NullPointerException</code> is thrown. Each character in the string * <code>s</code> is converted to a group of one, two, or three bytes, depending on the value of the character. * <p> * If a character <code>c</code> is in the range <code>\u0001</code> through <code>\u007f</code>, it is represented by * one byte: * <p> * * <pre> * (byte)c * </pre> * <p> * If a character <code>c</code> is <code>\u0000</code> or is in the range <code>\u0080</code> through * <code>\u07ff</code>, then it is represented by two bytes, to be written in the order shown: * <p> * * <pre> * <code> * (byte)(0xc0 | (0x1f & (c >> 6))) * (byte)(0x80 | (0x3f & c)) * </code> * </pre> * <p> * If a character <code>c</code> is in the range <code>\u0800</code> through <code>uffff</code>, then it is represented by * three bytes, to be written in the order shown: * <p> * * <pre> * <code> * (byte)(0xe0 | (0x0f & (c >> 12))) * (byte)(0x80 | (0x3f & (c >> 6))) * (byte)(0x80 | (0x3f & c)) * </code> * </pre> * <p> * First, the total number of bytes needed to represent all the characters of <code>s</code> is calculated. If this number is * larger than <code>65535</code>, then a <code>UTFDataFormatException</code> is thrown. Otherwise, this length is written to * the output stream in exactly the manner of the <code>writeShort</code> method; after this, the one-, two-, or three-byte * representation of each character in the string <code>s</code> is written. * <p> * The bytes written by this method may be read by the <code>readUTF</code> method of interface <code>DataInput</code> , which * will then return a <code>String</code> equal to <code>s</code>. * * @param str the string value to be written. * @return the number of bytes written */ public int writeUTFString( String str ) { int numBytesWritten = writeUTF(position, str); position += numBytesWritten; return numBytesWritten; } public int writeUTF( int position, String str ) { CharBuffer chars = CharBuffer.wrap(str); CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder(); ByteBuffer output = getBufferFor(position); int index = position % bufferSize; int newPosition = position; output.position(index); try { while (chars.hasRemaining()) { CoderResult result = encoder.encode(chars, output, true); if (output == remainderBuffer) { // There is some remainder from the previous iteration ... output.flip(); while (output.remaining() > 0) { writeByte(newPosition, output.get()); ++newPosition; } } else { // Calculate the new position based upon where we started ... newPosition += output.position() - index; } if (result.isError()) { // Some problem occurred ... result.throwException(); } else if (result.isOverflow()) { // Unable to write all of the content to the output buffer, so there's still some characters left ... if (output.remaining() > 0) { // We ran out of space in 'buffer', so go again with a small 'remainder' buffer ... if (remainderBuffer != null) { // There is one, so clear it out and prepare it for use ... remainderBuffer.clear(); } else { // There is none, so allocate one for this object ... remainderBuffer = ByteBuffer.allocate(4); } output = remainderBuffer; index = 0; } else { // We ran out of space by fully utilizing this buffer, so just go to the next buffer ... output = getBufferFor(newPosition); index = newPosition % bufferSize; output.position(index); } } } } catch (Exception e) { throw new RuntimeException("Unable to encode string", e); } updateSize(newPosition); return newPosition - position; } /** * Write all content to the supplied stream. * * @param stream the stream to which the content is to be written. * @throws IOException if there is a problem writing to the supplied stream */ public void writeTo( OutputStream stream ) throws IOException { writeTo(Channels.newChannel(stream)); } /** * Write all content to the supplied channel. * * @param channel the channel to which the content is to be written. * @throws IOException if there is a problem writing to the supplied stream */ public void writeTo( WritableByteChannel channel ) throws IOException { int numberOfBytesToWrite = size; for (ByteBuffer buffer : buffers) { if (buffer == null) { // already flushed continue; } int numBytesInBuffer = Math.min(numberOfBytesToWrite, bufferSize); buffer.position(numBytesInBuffer); buffer.flip(); channel.write(buffer); numberOfBytesToWrite -= numBytesInBuffer; } buffers.clear(); } }