org.modeshape.schematic.internal.io.BsonDataOutput Maven / Gradle / Ivy
Show all versions of modeshape-schematic Show documentation
/*
* 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 (C) 2011 Red Hat Inc.
*/
public class BsonDataOutput implements DataOutput {
private static final int DEFAULT_BUFFER_SIZE = 1048 * 8;
private final List 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 s
, taken in order, one byte is written
* to the output stream. If s
is null
, a NullPointerException
is thrown.
*
* If s.length
is zero, then no bytes are written. Otherwise, the character s[0]
is written first,
* then s[1]
, and so on; the last character written is s[s.length-1]
. For each character, one byte
* is written, the low-order byte, in exactly the manner of the writeByte
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 s
, to the output stream, in order, two bytes per character. If
* s
is null
, a NullPointerException
is thrown. If s.length
is zero, then
* no characters are written. Otherwise, the character s[0]
is written first, then s[1]
, and so on;
* the last character written is s[s.length-1]
. For each character, two bytes are actually written, low-order
* byte first, in exactly the manner of the writeChar
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 modified UTF-8 representation of every character in the string
* s
. This is similar to the standard {@link #writeUTF(String)} but without the leading two byte length.
* If s
is null
, a NullPointerException
is thrown. Each character in the string
* s
is converted to a group of one, two, or three bytes, depending on the value of the character.
*
* If a character c
is in the range \u0001
through \u007f
, it is represented by
* one byte:
*
*
*
* (byte)c
*
*
* If a character c
is \u0000
or is in the range \u0080
through
* \u07ff
, then it is represented by two bytes, to be written in the order shown:
*
*
*
*
* (byte)(0xc0 | (0x1f & (c >> 6)))
* (byte)(0x80 | (0x3f & c))
*
*
*
* If a character c
is in the range \u0800
through uffff
, then it is represented by
* three bytes, to be written in the order shown:
*
*
*
*
* (byte)(0xe0 | (0x0f & (c >> 12)))
* (byte)(0x80 | (0x3f & (c >> 6)))
* (byte)(0x80 | (0x3f & c))
*
*
*
* First, the total number of bytes needed to represent all the characters of s
is calculated. If this number is
* larger than 65535
, then a UTFDataFormatException
is thrown. Otherwise, this length is written to
* the output stream in exactly the manner of the writeShort
method; after this, the one-, two-, or three-byte
* representation of each character in the string s
is written.
*
* The bytes written by this method may be read by the readUTF
method of interface DataInput
, which
* will then return a String
equal to s
.
*
* @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();
}
}