blob: c6ab4f0ea10cb499a3a75b32bc0b2430e0d61e70 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.sis.internal.storage.io;
import java.util.Arrays;
import java.io.Flushable;
import java.io.IOException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.DoubleBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;
import java.nio.ShortBuffer;
import java.nio.channels.SeekableByteChannel;
import java.nio.channels.WritableByteChannel;
import org.apache.sis.internal.util.Numerics;
import org.apache.sis.internal.storage.Resources;
import static org.apache.sis.util.ArgumentChecks.ensureBetween;
/**
* Provides convenience methods for working with a ({@link WritableByteChannel}, {@link ByteBuffer}) pair.
* The channel and the buffer must be supplied by the caller. It is okay if they have already been used
* before {@code ChannelDataOutput} creation.
*
* <h2>Encapsulation</h2>
* This class exposes publicly the {@linkplain #channel} and the {@linkplain #buffer buffer} because this class
* is not expected to perform all possible data manipulations that we can do with the buffers. This class is only
* a helper tool, which often needs to be completed by specialized operations performed directly on the buffer.
* However, users are encouraged to transfer data from the buffer to the channel using only the methods provided
* in this class if they want to keep the {@link #seek(long)} and {@link #getStreamPosition()} values accurate.
*
* <p>Since this class is only a helper tool, it does not "own" the channel and consequently does not provide
* {@code close()} method. It is users responsibility to close the channel after usage.</p>
*
* <h2>Relationship with {@code DataOutput}</h2>
* This class API is compatibly with the {@link java.io.DataOutput} interface, so subclasses can implement that
* interface if they wish. This class does not implement {@code DataOutput} itself because it is not needed for
* SIS purposes.
* However the {@link ChannelImageOutputStream} class implements the {@code DataOutput} interface, together with
* the {@link javax.imageio.stream.ImageOutputStream} one, mostly for situations when inter-operability with
* {@link javax.imageio} is needed.
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 0.5
* @since 0.5
* @module
*/
public class ChannelDataOutput extends ChannelData implements Flushable {
/**
* The channel where data are written.
* This is supplied at construction time.
*/
public final WritableByteChannel channel;
/**
* Creates a new data output for the given channel and using the given buffer.
*
* @param filename a file identifier used only for formatting error message.
* @param channel the channel where data are written.
* @param buffer the buffer where to put the data.
* @throws IOException if an error occurred while creating the data output.
*/
public ChannelDataOutput(final String filename, final WritableByteChannel channel, final ByteBuffer buffer)
throws IOException
{
super(filename, channel, buffer);
this.channel = channel;
buffer.limit(0);
}
/**
* Makes sure that the buffer can accept at least <var>n</var> more bytes.
* It is caller's responsibility to ensure that the given number of bytes is
* not greater than the {@linkplain ByteBuffer#capacity() buffer capacity}.
*
* <p>After this method call, the buffer {@linkplain ByteBuffer#limit() limit}
* will be equal or greater than {@code position + n}.</p>
*
* @param n the minimal number of additional bytes that the {@linkplain #buffer buffer} shall accept.
* @throws IOException if an error occurred while writing to the channel.
*/
private void ensureBufferAccepts(final int n) throws IOException {
final int capacity = buffer.capacity();
assert n >= 0 && n <= capacity : n;
int after = buffer.position() + n;
if (after > buffer.limit()) {
/*
* We will increase the limit for every new 'put' operation in order to maintain the number
* of valid bytes in the buffer. If the new limit would exceed the buffer capacity, then we
* need to write some bytes now.
*/
if (after > capacity) {
buffer.flip();
do {
final int c = channel.write(buffer);
if (c == 0) {
onEmptyTransfer();
}
after -= c;
} while (after > capacity);
/*
* We wrote a sufficient amount of bytes - usually all of them, but not necessarily.
* If there is some unwritten bytes, move them the the beginning of the buffer.
*/
bufferOffset += buffer.position();
buffer.compact();
assert after >= buffer.position();
}
buffer.limit(after);
}
}
/**
* Returns the current byte position of the stream.
*
* @return the position of the stream.
*/
@Override
public long getStreamPosition() {
long position = super.getStreamPosition();
/*
* ChannelDataOutput uses a different strategy than ChannelDataInput: if some bits were in process
* of being written, the buffer position is set to the byte AFTER the byte containing the bits. We
* need to apply a correction here for this strategy.
*/
if (getBitOffset() != 0) {
position--;
}
return position;
}
/**
* Writes a single bit. This method uses only the rightmost bit of the given argument;
* the upper 31 bits are ignored.
*
* @param bit the bit to write (rightmost bit).
* @throws IOException if an error occurred while creating the data output.
*/
public final void writeBit(final int bit) throws IOException {
writeBits(bit, 1);
}
/**
* Writes a sequence of bits. This method uses only the <code>numBits</code> rightmost bits;
* other bits are ignored.
*
* @param bits the bits to write (rightmost bits).
* @param numBits the number of bits to write.
* @throws IOException if an error occurred while creating the data output.
*/
public final void writeBits(long bits, int numBits) throws IOException {
ensureBetween("numBits", 0, Long.SIZE, numBits);
if (numBits != 0) {
int bitOffset = getBitOffset();
if (bitOffset != 0) {
bits &= Numerics.bitmask(numBits) - 1; // Make sure that high-order bits are zero.
final int r = numBits - (Byte.SIZE - bitOffset);
/*
* 'r' is the number of bits than we can not store in the current byte. This value may be negative,
* which means that the current byte has space for more bits than what we have, in which case some
* room will still exist after this method call (i.e. the 'bitOffset' will still non-zero).
*/
final long mask;
if (r >= 0) {
mask = bits >>> r;
bitOffset = 0;
} else {
mask = bits << -r;
bitOffset += numBits;
}
numBits = r;
assert (mask & ~0xFFL) == 0 : mask;
final int p = buffer.position() - 1;
buffer.put(p, (byte) (buffer.get(p) | mask));
}
/*
* At this point, we are going to write only whole bytes.
*/
while (numBits > 0) {
numBits -= Byte.SIZE;
final long part;
if (numBits >= 0) {
part = bits >>> numBits;
} else {
part = bits << -numBits;
bitOffset = Byte.SIZE + numBits;
}
writeByte((int) part);
}
setBitOffset(bitOffset);
}
}
/**
* Writes the 8 low-order bits of {@code v} to the stream.
* The 24 high-order bits of {@code v} are ignored.
* This method ensures that there is space for at least 1 byte in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#put(byte)}.
*
* @param value byte to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeByte(final int value) throws IOException {
ensureBufferAccepts(Byte.BYTES);
buffer.put((byte) value);
}
/**
* Writes the 16 low-order bits of value to the stream.
* The 16 high-order bits of {@code v} are ignored.
* This method ensures that there is space for at least 2 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putShort(short)}.
*
* @param value short integer to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeShort(final int value) throws IOException {
ensureBufferAccepts(Short.BYTES);
buffer.putShort((short) value);
}
/**
* Writes char value (16 bits) into the steam.
* This method ensures that there is space for at least 2 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putChar(char)}.
*
* @param value character to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeChar(final int value) throws IOException {
ensureBufferAccepts(Character.BYTES);
buffer.putChar((char) value);
}
/**
* Writes integer value (32 bits) into the steam.
* This method ensures that there is space for at least 4 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putInt(int)}.
*
* @param value integer to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeInt(final int value) throws IOException {
ensureBufferAccepts(Integer.BYTES);
buffer.putInt(value);
}
/**
* Writes long value (64 bits) into the steam.
* This method ensures that there is space for at least 4 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putLong(long)}.
*
* @param value long integer to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeLong(final long value) throws IOException {
ensureBufferAccepts(Long.BYTES);
buffer.putLong(value);
}
/**
* Writes float value (32 bits) into the steam.
* This method ensures that there is space for at least 4 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putFloat(float)}.
*
* @param value floating point value to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeFloat(final float value) throws IOException {
ensureBufferAccepts(Float.BYTES);
buffer.putFloat(value);
}
/**
* Writes double value (64 bits) into the steam.
* This method ensures that there is space for at least 8 bytes in the buffer,
* (writing previous bytes into the channel if necessary), then delegates to {@link ByteBuffer#putDouble(double)}.
*
* @param value double precision floating point value to be written.
* @throws IOException if some I/O exception occurs during writing.
*/
public final void writeDouble(final double value) throws IOException {
ensureBufferAccepts(Double.BYTES);
buffer.putDouble(value);
}
/**
* Writes all bytes from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return write(src, 0, src.length);
* }
*
* @param src an array of bytes to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void write(final byte[] src) throws IOException {
write(src, 0, src.length);
}
/**
* Writes all shorts from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeShorts(src, 0, src.length);
* }
*
* @param src an array of shorts to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeShorts(final short[] src) throws IOException {
writeShorts(src, 0, src.length);
}
/**
* Writes all characters from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeChars(src, 0, src.length);
* }
*
* @param src an array of characters to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeChars(final char[] src) throws IOException {
writeChars(src, 0, src.length);
}
/**
* Writes all integers from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeInts(src, 0, src.length);
* }
*
* @param src an array of integers to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeInts(final int[] src) throws IOException {
writeInts(src, 0, src.length);
}
/**
* Writes all longs from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeLongs(src, 0, src.length);
* }
*
* @param src an array of longs to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeLongs(final long[] src) throws IOException {
writeLongs(src, 0, src.length);
}
/**
* Writes all floats from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeFloats(src, 0, src.length);
* }
*
* @param src an array of floats to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeFloats(final float[] src) throws IOException {
writeFloats(src, 0, src.length);
}
/**
* Writes all doubles from the given array into the stream.
* The implementation is as below:
*
* {@preformat java
* return writeDoubles(src, 0, src.length);
* }
*
* @param src an array of doubles to be written into stream.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeDoubles(final double[] src) throws IOException {
writeDoubles(src, 0, src.length);
}
/**
* Writes {@code length} bytes starting at index {@code offset} from the given array.
*
* @param src an array containing the bytes to write.
* @param offset index within {@code src} of the first byte to write.
* @param length the number of bytes to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void write(final byte[] src, int offset, int length) throws IOException {
if (length != 0) {
do {
final int n = Math.min(buffer.capacity(), length);
ensureBufferAccepts(n);
buffer.put(src, offset, n);
offset += n;
length -= n;
} while (length != 0);
} else {
/*
* Since the 'bitOffset' validity is determined by the position, if the position
* did not changed, then we need to clear the 'bitOffset' flag manually.
*/
clearBitOffset();
}
}
/**
* Helper class for the {@code writeFully(…)} methods,
* in order to avoid duplicating almost identical code many times.
*/
private abstract class ArrayWriter {
/**
* Creates a new buffer of the type required by the array to write.
* This method is guaranteed to be invoked exactly once.
*/
abstract Buffer createView();
/**
* Transfers the data from the array of primitive Java type known by the subclass into buffer
* created by {@link #createView()}. This method may be invoked an arbitrary amount of time.
*/
abstract void transfer(int offset, int length);
/**
* Skips the given amount of bytes in the buffer. It is caller responsibility to ensure
* that there is enough bytes remaining in the buffer.
*
* @param nByte byte shift of buffer position.
*/
private void skipInBuffer(int nByte) {
buffer.position(buffer.position() + nByte);
}
/**
* Writes {@code length} characters from the array to the stream.
*
* @param dataSize the size of the Java primitive type which is the element of the array.
* @param offset the starting position within {@code src} to write.
* @param length the number of characters to write.
* @throws IOException if an error occurred while writing the stream.
*/
final void writeFully(final int dataSize, int offset, int length) throws IOException {
clearBitOffset(); // Actually needed only if length == 0.
ensureBufferAccepts(Math.min(length * dataSize, buffer.capacity()));
final Buffer view = createView(); // Must be after ensureBufferAccept
int n = Math.min(view.remaining(), length);
transfer(offset, n);
skipInBuffer(n * dataSize);
while ((length -= n) != 0) {
offset += n;
ensureBufferAccepts(Math.min(length, view.capacity()) * dataSize);
view.rewind().limit(buffer.remaining() / dataSize);
transfer(offset, n = view.remaining());
skipInBuffer(n * dataSize);
}
}
}
/**
* Writes {@code length} chars starting at index {@code offset} from the given array.
*
* @param src an array containing the characters to write.
* @param offset index within {@code src} of the first char to write.
* @param length the number of chars to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeChars(final char[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private CharBuffer view;
@Override Buffer createView() {return view = buffer.asCharBuffer();}
@Override void transfer(int offset, int n) {view.put(src, offset, n);}
}.writeFully(Character.BYTES, offset, length);
}
/**
* Writes {@code length} shorts starting at index {@code offset} from the given array.
*
* @param src an array containing the shorts to write.
* @param offset index within {@code src} of the first short to write.
* @param length the number of shorts to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeShorts(final short[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private ShortBuffer view;
@Override Buffer createView() {return view = buffer.asShortBuffer();}
@Override void transfer(int offset, int length) {view.put(src, offset, length);}
}.writeFully(Short.BYTES, offset, length);
}
/**
* Writes {@code length} integers starting at index {@code offset} from the given array.
*
* @param src an array containing the integers to write.
* @param offset index within {@code src} of the first integer to write.
* @param length the number of integers to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeInts(final int[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private IntBuffer view;
@Override Buffer createView() {return view = buffer.asIntBuffer();}
@Override void transfer(int offset, int n) {view.put(src, offset, n);}
}.writeFully(Integer.BYTES, offset, length);
}
/**
* Writes {@code length} longs starting at index {@code offset} from the given array.
*
* @param src an array containing the longs to write.
* @param offset index within {@code src} of the first long to write.
* @param length the number of longs to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeLongs(final long[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private LongBuffer view;
@Override Buffer createView() {return view = buffer.asLongBuffer();}
@Override void transfer(int offset, int n) {view.put(src, offset, n);}
}.writeFully(Long.BYTES, offset, length);
}
/**
* Writes {@code length} floats starting at index {@code offset} from the given array.
*
* @param src an array containing the floats to write.
* @param offset index within {@code src} of the first float to write.
* @param length the number of floats to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeFloats(final float[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private FloatBuffer view;
@Override Buffer createView() {return view = buffer.asFloatBuffer();}
@Override void transfer(int offset, int n) {view.put(src, offset, n);}
}.writeFully(Float.BYTES, offset, length);
}
/**
* Writes {@code length} doubles starting at index {@code offset} from the given array.
*
* @param src an array containing the doubles to write.
* @param offset index within {@code src} of the first double to write.
* @param length the number of doubles to write.
* @throws IOException if an error occurred while writing the stream.
*/
public final void writeDoubles(final double[] src, int offset, int length) throws IOException {
new ArrayWriter() {
private DoubleBuffer view;
@Override Buffer createView() {return view = buffer.asDoubleBuffer();}
@Override void transfer(int offset, int n) {view.put(src, offset, n);}
}.writeFully(Double.BYTES, offset, length);
}
/**
* Fills the buffer with the zero values from its position up to the limit.
* After this method call, the position is undetermined and shall be set to
* a new value by the caller.
*/
private void clear() {
if (buffer.hasArray()) {
final int offset = buffer.arrayOffset();
Arrays.fill(buffer.array(), offset + buffer.position(), offset + buffer.limit(), (byte) 0);
} else {
while (buffer.hasRemaining()) {
buffer.put((byte) 0);
}
}
}
/**
* Moves to the given position in the stream, relative to the stream position at construction time.
* If the given position is greater than the stream length, then the values of bytes between the
* previous stream length and the given position are unspecified. The limit is unchanged.
*
* @param position the position where to move.
* @throws IOException if the stream can not be moved to the given position.
*/
@Override
public final void seek(final long position) throws IOException {
long p = Math.subtractExact(position, bufferOffset);
if (p >= 0 && p <= buffer.limit()) {
/*
* Requested position is inside the current limits of the buffer.
*/
buffer.position((int) p);
clearBitOffset();
} else if (channel instanceof SeekableByteChannel) {
/*
* Requested position is outside the current limits of the buffer,
* but we can set the new position directly in the channel.
*/
flush();
((SeekableByteChannel) channel).position(Math.addExact(channelOffset, position));
bufferOffset = position;
} else if (p >= 0) {
/*
* Requested position is after the current buffer limit and
* we can not seek, so we have to pad with some zero values.
*/
p -= buffer.limit();
flush(); // Also set the position to 0.
if (p <= buffer.capacity()) {
buffer.limit((int) p);
clear();
buffer.position((int) p);
} else {
buffer.clear();
clear();
do {
if (channel.write(buffer) == 0) {
onEmptyTransfer();
}
bufferOffset += buffer.position();
p -= buffer.position();
buffer.rewind();
} while (p > buffer.capacity());
buffer.limit((int) p).position((int) p);
}
} else {
// We can not move position beyond the buffered part.
throw new IOException(Resources.format(Resources.Keys.StreamIsForwardOnly_1, filename));
}
}
/**
* Flushes the {@link #buffer buffer} content to the channel.
* This method does <strong>not</strong> flush the channel itself.
*
* @throws IOException if an error occurred while writing to the channel.
*/
@Override
public final void flush() throws IOException {
buffer.rewind();
writeFully();
buffer.limit(0);
clearBitOffset();
}
/**
* Writes the buffer content up to the given position, then set the buffer position to the given value.
* The {@linkplain ByteBuffer#limit() buffer limit} is unchanged, and the buffer offset is incremented
* by the given value.
*/
@Override
final void flushAndSetPosition(final int position) throws IOException {
final int limit = buffer.limit();
buffer.rewind().limit(position);
writeFully();
buffer.limit(limit);
}
/**
* Writes fully the buffer content from its position to its limit.
* After this method call, the buffer position is equals to its limit.
*
* @throws IOException if an error occurred while writing to the channel.
*/
private void writeFully() throws IOException {
int n = buffer.remaining();
bufferOffset += n;
while (n != 0) {
final int c = channel.write(buffer);
if (c == 0) {
onEmptyTransfer();
}
n -= c;
}
assert !buffer.hasRemaining() : buffer;
}
}