All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.qpid.proton.codec.CompositeReadableBuffer Maven / Gradle / Ivy

There is a newer version: 0.34.1
Show newest version
/*
 * 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.qpid.proton.codec;

import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.InvalidMarkException;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CoderResult;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * ReadableBuffer implementation whose content is made up of one or more
 * byte arrays.
 */
public class CompositeReadableBuffer implements ReadableBuffer {

    private static final List EMPTY_LIST = Collections.unmodifiableList(new ArrayList());
    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.wrap(new byte[0]);
    private static final CompositeReadableBuffer EMPTY_SLICE = new CompositeReadableBuffer(false);
    private static int UNSET_MARK = -1;

    private static final int SHORT_BYTES = 2;
    private static final int INT_BYTES = 4;
    private static final int LONG_BYTES = 8;

    private ArrayList contents;

    // Track active array and our offset into it.
    private int currentArrayIndex = -1;
    private byte[] currentArray;
    private int currentOffset;

    // State global to the buffer.
    private int position;
    private int limit;
    private int capacity;
    private int mark = -1;
    private boolean compactable = true;

    /**
     * Creates a default empty composite buffer
     */
    public CompositeReadableBuffer() {
    }

    private CompositeReadableBuffer(byte[] array, int offset) {
        this.currentArray = array;
        this.currentOffset = offset;
        if(array != null) {
            this.capacity = array.length;
        }
        this.limit = capacity;
    }

    private CompositeReadableBuffer(boolean compactable) {
        this.compactable = compactable;
    }

    public List getArrays() {
        return contents == null ? EMPTY_LIST : Collections.unmodifiableList(contents);
    }

    public int getCurrentIndex() {
        return currentArrayIndex;
    }

    /**
     * Gets the current position index in the current backing array, which represents the current buffer position.
     *
     * This value includes any buffer position movement, and resets when moving across array segments, so it only
     * gives the starting offset for the first array if the buffer position is 0.
     *
     * Value may be out of array bounds if the the buffer currently has no content remaining.
     *
     * @return the position index in the current array representing the current buffer position.
     */
    public int getCurrentArrayPosition() {
        return currentOffset;
    }

    @Override
    public boolean hasArray() {
        return currentArray != null && (contents == null || contents.size() == 1);
    }

    @Override
    public int capacity() {
        return capacity;
    }

    @Override
    public byte[] array() {
        if (hasArray()) {
            return currentArray;
        }

        throw new UnsupportedOperationException("Buffer not backed by a single array");
    }

    @Override
    public int arrayOffset() {
        if (hasArray()) {
            return currentOffset - position;
        }

        throw new UnsupportedOperationException("Buffer not backed by a single array");
    }

    @Override
    public byte get() {
        if (position == limit) {
            throw new BufferUnderflowException();
        }

        final byte result = currentArray[currentOffset++];
        position++;
        maybeMoveToNextArray();

        return result;
    }

    @Override
    public byte get(int index) {
        if (index < 0 || index >= limit) {
            throw new IndexOutOfBoundsException("The given index is not valid: " + index);
        }

        byte result = 0;

        if (index == position) {
            result = currentArray[currentOffset];
        } else if (index < position) {
            result = getBackwards(index);
        } else {
            result = getForward(index);
        }

        return result;
    }

    private byte getForward(int index) {
        byte result = 0;

        int currentArrayIndex = this.currentArrayIndex;
        int currentOffset = this.currentOffset;
        byte[] currentArray = this.currentArray;

        for (int amount = index - position; amount >= 0;) {
            if (amount < currentArray.length - currentOffset) {
                result = currentArray[currentOffset + amount];
                break;
            } else {
                amount -= currentArray.length - currentOffset;
                currentArray = contents.get(++currentArrayIndex);
                currentOffset = 0;
            }
        }

        return result;
    }

    private byte getBackwards(int index) {
        byte result = 0;

        int currentArrayIndex = this.currentArrayIndex;
        int currentOffset = this.currentOffset;
        byte[] currentArray = this.currentArray;

        for (int amount = position - index; amount >= 0;) {
            if ((currentOffset - amount) >= 0) {
                result = currentArray[currentOffset - amount];
                break;
            } else {
                amount -= currentOffset;
                currentArray = contents.get(--currentArrayIndex);
                currentOffset = currentArray.length;
            }
        }

        return result;
    }

    @Override
    public int getInt() {
        if (remaining() < INT_BYTES) {
            throw new BufferUnderflowException();
        }

        int result = 0;

        if (currentArray.length - currentOffset >= 4) {
            result = (int)(currentArray[currentOffset++] & 0xFF) << 24 |
                     (int)(currentArray[currentOffset++] & 0xFF) << 16 |
                     (int)(currentArray[currentOffset++] & 0xFF) << 8 |
                     (int)(currentArray[currentOffset++] & 0xFF) << 0;
            maybeMoveToNextArray();
        } else {
            for (int i = INT_BYTES - 1; i >= 0; --i) {
                result |= (int)(currentArray[currentOffset++] & 0xFF) << (i * Byte.SIZE);
                maybeMoveToNextArray();
            }
        }

        position += 4;

        return result;
    }

    @Override
    public long getLong() {
        if (remaining() < LONG_BYTES) {
            throw new BufferUnderflowException();
        }

        long result = 0;

        if (currentArray.length - currentOffset >= 8) {
            result = (long)(currentArray[currentOffset++] & 0xFF) << 56 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 48 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 40 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 32 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 24 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 16 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 8 |
                     (long)(currentArray[currentOffset++] & 0xFF) << 0;
            maybeMoveToNextArray();
        } else {
            for (int i = LONG_BYTES - 1; i >= 0; --i) {
                result |= (long)(currentArray[currentOffset++] & 0xFF) << (i * Byte.SIZE);
                maybeMoveToNextArray();
            }
        }

        position += 8;

        return result;
    }

    @Override
    public short getShort() {
        if (remaining() < SHORT_BYTES) {
            throw new BufferUnderflowException();
        }

        short result = 0;

        for (int i = SHORT_BYTES - 1; i >= 0; --i) {
            result |= (currentArray[currentOffset++] & 0xFF) << (i * Byte.SIZE);
            maybeMoveToNextArray();
        }

        position += 2;

        return result;
    }

    @Override
    public float getFloat() {
        return Float.intBitsToFloat(getInt());
    }

    @Override
    public double getDouble() {
        return Double.longBitsToDouble(getLong());
    }

    @Override
    public CompositeReadableBuffer get(byte[] data) {
        return get(data, 0, data.length);
    }

    @Override
    public CompositeReadableBuffer get(byte[] data, int offset, int length) {
        validateReadTarget(data.length, offset, length);

        if (length > remaining()) {
            throw new BufferUnderflowException();
        }

        int copied = 0;
        while (length > 0) {
            final int chunk = Math.min((currentArray.length - currentOffset), length);
            System.arraycopy(currentArray, currentOffset, data, offset + copied, chunk);

            currentOffset += chunk;
            length -= chunk;
            copied += chunk;

            maybeMoveToNextArray();
        }

        position += copied;

        return this;
    }

    @Override
    public CompositeReadableBuffer get(WritableBuffer target) {
        int length = Math.min(target.remaining(), remaining());

        do {
            final int chunk = Math.min((currentArray.length - currentOffset), length);

            if (chunk == 0) {
                break;  // This buffer is out of data
            }

            target.put(currentArray, currentOffset, chunk);

            currentOffset += chunk;
            position += chunk;
            length -= chunk;

            maybeMoveToNextArray();
        } while (length > 0);

        return this;
    }

    @Override
    public CompositeReadableBuffer position(int position) {
        if (position < 0 || position > limit) {
            throw new IllegalArgumentException("position must be non-negative and no greater than the limit");
        }

        int moveBy = position - this.position;
        if (moveBy >= 0) {
            moveForward(moveBy);
        } else {
            moveBackwards(Math.abs(moveBy));
        }

        this.position = position;

        if (mark > position) {
            mark = UNSET_MARK;
        }

        return this;
    }

    private void moveForward(int moveBy) {
        while (moveBy > 0) {
            if (moveBy < currentArray.length - currentOffset) {
                currentOffset += moveBy;
                break;
            } else {
                moveBy -= currentArray.length - currentOffset;
                if (currentArrayIndex != -1 && currentArrayIndex < contents.size() - 1) {
                    currentArray = contents.get(++currentArrayIndex);
                    currentOffset = 0;
                } else {
                    currentOffset = currentArray.length;
                }
            }
        }
    }

    private void moveBackwards(int moveBy) {
        while (moveBy > 0) {
            if ((currentOffset - moveBy) >= 0) {
                currentOffset -= moveBy;
                break;
            } else {
                moveBy -= currentOffset;
                currentArray = contents.get(--currentArrayIndex);
                currentOffset = currentArray.length;
            }
        }
    }

    @Override
    public int position() {
        return position;
    }

    @Override
    public CompositeReadableBuffer slice() {
        int newCapacity = limit() - position();

        final CompositeReadableBuffer result;

        if (newCapacity == 0) {
            result = EMPTY_SLICE;
        } else {
            result = new CompositeReadableBuffer(currentArray, currentOffset);
            result.contents = contents;
            result.currentArrayIndex = currentArrayIndex;
            result.capacity = newCapacity;
            result.limit = newCapacity;
            result.position = 0;
            result.compactable = false;
        }

        return result;
    }

    @Override
    public CompositeReadableBuffer flip() {
        limit = position;
        position(0); // Move by index to avoid corrupting a slice.
        mark = UNSET_MARK;

        return this;
    }

    @Override
    public CompositeReadableBuffer limit(int limit) {
        if (limit < 0 || limit > capacity) {
            throw new IllegalArgumentException("limit must be non-negative and no greater than the capacity");
        }

        if (mark > limit) {
            mark = UNSET_MARK;
        }

        if (position > limit) {
            position(limit);
        }

        this.limit = limit;

        return this;
    }

    @Override
    public int limit() {
        return limit;
    }

    @Override
    public CompositeReadableBuffer mark() {
        this.mark = position;
        return this;
    }

    @Override
    public CompositeReadableBuffer reset() {
        if (mark < 0) {
            throw new InvalidMarkException();
        }

        position(mark);

        return this;
    }

    @Override
    public CompositeReadableBuffer rewind() {
        return position(0);
    }

    @Override
    public CompositeReadableBuffer clear() {
        mark = UNSET_MARK;
        limit = capacity;

        return position(0);
    }

    @Override
    public int remaining() {
        return limit - position;
    }

    @Override
    public boolean hasRemaining() {
        return remaining() > 0;
    }

    @Override
    public CompositeReadableBuffer duplicate() {
        CompositeReadableBuffer duplicated =
            new CompositeReadableBuffer(currentArray, currentOffset);

        if (contents != null) {
            duplicated.contents = new ArrayList<>(contents);
        }

        duplicated.capacity = capacity;
        duplicated.currentArrayIndex = currentArrayIndex;
        duplicated.limit = limit;
        duplicated.position = position;
        duplicated.mark = mark;
        duplicated.compactable = compactable;   // A slice duplicated should not allow compaction.

        return duplicated;
    }

    @Override
    public ByteBuffer byteBuffer() {
        int viewSpan = limit() - position();

        final ByteBuffer result;

        if (viewSpan == 0) {
            result = EMPTY_BUFFER;
        } else if (viewSpan <= currentArray.length - currentOffset) {
            result = ByteBuffer.wrap(currentArray, currentOffset, viewSpan);
        } else {
            result = buildByteBuffer(viewSpan);
        }

        return result.asReadOnlyBuffer();
    }

    private ByteBuffer buildByteBuffer(int span) {
        byte[] compactedView = new byte[span];
        int arrayIndex = currentArrayIndex;

        // Take whatever is left from the current array;
        System.arraycopy(currentArray, currentOffset, compactedView, 0, currentArray.length - currentOffset);
        int copied = currentArray.length - currentOffset;

        while (copied < span) {
            byte[] next = contents.get(++arrayIndex);
            final int length = Math.min(span - copied, next.length);
            System.arraycopy(next, 0, compactedView, copied, length);
            copied += length;
        }

        return ByteBuffer.wrap(compactedView);
    }

    @Override
    public String readUTF8() throws CharacterCodingException {
        return readString(StandardCharsets.UTF_8.newDecoder());
    }

    @Override
    public String readString(CharsetDecoder decoder) throws CharacterCodingException {
        if (!hasRemaining()) {
            return "";
        }

        CharBuffer decoded = null;

        if (hasArray()) {
            decoded = decoder.decode(ByteBuffer.wrap(currentArray, currentOffset, remaining()));
        } else {
            decoded = readStringFromComponents(decoder);
        }

        return decoded.toString();
    }

    private CharBuffer readStringFromComponents(CharsetDecoder decoder) throws CharacterCodingException {
        int size = (int)(remaining() * decoder.averageCharsPerByte());
        CharBuffer decoded = CharBuffer.allocate(size);

        int arrayIndex = currentArrayIndex;
        final int viewSpan = limit() - position();
        int processed = Math.min(currentArray.length - currentOffset, viewSpan);
        ByteBuffer wrapper = ByteBuffer.wrap(currentArray, currentOffset, processed);

        CoderResult step = CoderResult.OVERFLOW;

        do {
            boolean endOfInput = processed == viewSpan;
            step = decoder.decode(wrapper, decoded, endOfInput);
            if (step.isUnderflow() && endOfInput) {
                step = decoder.flush(decoded);
                break;
            }

            if (step.isOverflow()) {
                size = 2 * size + 1;
                CharBuffer upsized = CharBuffer.allocate(size);
                decoded.flip();
                upsized.put(decoded);
                decoded = upsized;
                continue;
            }

            byte[] next = contents.get(++arrayIndex);
            int wrapSize = Math.min(next.length, viewSpan - processed);
            wrapper = ByteBuffer.wrap(next, 0, wrapSize);
            processed += wrapSize;
        } while (!step.isError());

        if (step.isError()) {
            step.throwException();
        }

        return (CharBuffer) decoded.flip();
    }

    /**
     * Compact the buffer dropping arrays that have been consumed by previous
     * reads from this Composite buffer.  The limit is reset to the new capacity
     */
    @Override
    public CompositeReadableBuffer reclaimRead() {
        if (!compactable || (currentArray == null && contents == null)) {
            return this;
        }

        int totalCompaction = 0;
        int totalRemovals = 0;

        for (; totalRemovals < currentArrayIndex; ++totalRemovals) {
            byte[] element = contents.remove(0);
            totalCompaction += element.length;
        }

        currentArrayIndex -= totalRemovals;

        if (currentArray.length == currentOffset) {
            totalCompaction += currentArray.length;

            // If we are sitting on the end of the data (length == offest) then
            // we are also at the last element in the ArrayList if one is currently
            // in use, so remove the data and release the list.
            if (currentArrayIndex == 0) {
                contents.clear();
                contents = null;
            }

            currentArray = null;
            currentArrayIndex = -1;
            currentOffset = 0;
        }

        position -= totalCompaction;
        limit = capacity -= totalCompaction;

        if (mark != UNSET_MARK) {
            mark -= totalCompaction;
        }

        return this;
    }

    /**
     * Adds the given array into the composite buffer at the end.
     * 

* The appended array is not copied so changes to the source array are visible in this * buffer and vice versa. If this composite was empty than it would return true for the * {@link #hasArray()} method until another array is appended. *

* Calling this method resets the limit to the new capacity. * * @param array * The array to add to this composite buffer. * * @throws IllegalArgumentException if the array is null or zero size. * @throws IllegalStateException if the buffer does not allow appends. * * @return a reference to this {@link CompositeReadableBuffer}. */ public CompositeReadableBuffer append(byte[] array) { validateAppendable(); if (array == null || array.length == 0) { throw new IllegalArgumentException("Array must not be empty or null"); } if (currentArray == null) { currentArray = array; currentOffset = 0; } else if (contents == null) { contents = new ArrayList<>(); contents.add(currentArray); contents.add(array); currentArrayIndex = 0; // If we exhausted the array previously then it should move to the new one now. maybeMoveToNextArray(); } else { contents.add(array); // If we exhausted the list previously then it didn't move onward at the time, so it should now. maybeMoveToNextArray(); } capacity += array.length; limit = capacity; return this; } private void validateAppendable() { if (!compactable) { throw new IllegalStateException(); } } private void validateBuffer(ReadableBuffer buffer) { if (buffer == null) { throw new IllegalArgumentException("A non-null buffer must be provided"); } if (!buffer.hasRemaining()) { throw new IllegalArgumentException("Buffer has no remaining content to append"); } } /** * Adds the given composite buffer contents (from current position, up to the limit) into this * composite buffer at the end. The source buffer position will be set to its limit. *

* The appended buffer contents are not copied wherever possible, so changes to the source * arrays are typically visible in this buffer and vice versa. Exceptions include where the * source buffer position is not located at the start of its current backing array, or where the * given buffer has a limit that doesn't encompass all of the last array used, and * so the remainder of that arrays contents must be copied first to append here. *

* Calling this method resets the limit to the new capacity. * * @param buffer * the buffer with contents to append into this composite buffer. * * @throws IllegalArgumentException if the given buffer is null or has zero remainder. * @throws IllegalStateException if the buffer does not allow appends. * * @return a reference to this {@link CompositeReadableBuffer}. */ public CompositeReadableBuffer append(CompositeReadableBuffer buffer) { validateAppendable(); validateBuffer(buffer); byte[] chunk; do { int bufferRemaining = buffer.remaining(); int arrayRemaining = buffer.currentArray.length - buffer.currentOffset; if (buffer.currentOffset > 0 || bufferRemaining < arrayRemaining) { int length = Math.min(arrayRemaining, bufferRemaining); chunk = new byte[length]; System.arraycopy(buffer.currentArray, buffer.currentOffset, chunk, 0, length); } else { chunk = buffer.currentArray; } append(chunk); buffer.position(buffer.position() + chunk.length); } while (buffer.hasRemaining()); return this; } /** * Adds the given readable buffer contents (from current position, up to the limit) into this * composite buffer at the end. The source buffer position will be set to its limit. *

* The appended buffer contents are not copied wherever possible, so changes to the source * arrays are typically visible in this buffer and vice versa. Exceptions are where the * source buffer is not backed by an array, or where the source buffer position is not * located at the start of its backing array, and so the remainder of the contents must * be copied first to append here. *

* Calling this method resets the limit to the new capacity. * * @param buffer * the buffer with contents to append into this composite buffer. * * @throws IllegalArgumentException if the given buffer is null or has zero remainder. * @throws IllegalStateException if the buffer does not allow appends. * * @return a reference to this {@link CompositeReadableBuffer}. */ public CompositeReadableBuffer append(ReadableBuffer buffer) { if(buffer instanceof CompositeReadableBuffer) { append((CompositeReadableBuffer) buffer); } else { validateAppendable(); validateBuffer(buffer); if (buffer.hasArray()) { byte[] chunk = buffer.array(); int bufferRemaining = buffer.remaining(); if (buffer.arrayOffset() > 0 || bufferRemaining < chunk.length) { chunk = new byte[bufferRemaining]; System.arraycopy(buffer.array(), buffer.arrayOffset(), chunk, 0, bufferRemaining); } append(chunk); buffer.position(buffer.position() + chunk.length); } else { byte[] chunk = new byte[buffer.remaining()]; buffer.get(chunk); append(chunk); } } return this; } @Override public int hashCode() { int hash = 1; int remaining = remaining(); if (currentArrayIndex < 0 || remaining <= currentArray.length - currentOffset) { while (remaining > 0) { hash = 31 * hash + currentArray[currentOffset + --remaining]; } } else { hash = hashCodeFromComponents(); } return hash; } private int hashCodeFromComponents() { int hash = 1; byte[] array = currentArray; int arrayOffset = currentOffset; int arraysIndex = currentArrayIndex; // Run to the the array and offset where we want to start the hash from final int remaining = remaining(); for (int moveBy = remaining; moveBy > 0; ) { if (moveBy <= array.length - arrayOffset) { arrayOffset += moveBy; break; } else { moveBy -= array.length - arrayOffset; array = contents.get(++arraysIndex); arrayOffset = 0; } } // Now run backwards through the arrays to match what ByteBuffer would produce for (int moveBy = remaining; moveBy > 0; moveBy--) { hash = 31 * hash + array[--arrayOffset]; if (arrayOffset == 0 && arraysIndex > 0) { array = contents.get(--arraysIndex); arrayOffset = array.length; } } return hash; } @Override public boolean equals(Object other) { if (this == other) { return true; } if (!(other instanceof ReadableBuffer)) { return false; } ReadableBuffer buffer = (ReadableBuffer) other; final int remaining = remaining(); if (remaining != buffer.remaining()) { return false; } if (remaining == 0) { // No content to compare, and we already checked 'remaining' is equal. Protects from NPE below. return true; } if (hasArray() || remaining <= currentArray.length - currentOffset) { // Either there is only one array, or the span to compare is within a single chunk of this buffer, // allowing the compare to directly access the underlying array instead of using slower get methods. return equals(currentArray, currentOffset, remaining, buffer); } else { return equals(this, buffer); } } private static boolean equals(byte[] buffer, int start, int length, ReadableBuffer other) { final int position = other.position(); for (int i = 0; i < length; i++) { if (buffer[start + i] != other.get(position + i)) { return false; } } return true; } private static boolean equals(ReadableBuffer buffer, ReadableBuffer other) { final int origPos = buffer.position(); try { for (int i = other.position(); buffer.hasRemaining(); i++) { if (!equals(buffer.get(), other.get(i))) { return false; } } return true; } finally { buffer.position(origPos); } } @Override public String toString() { StringBuffer builder = new StringBuffer(); builder.append("CompositeReadableBuffer"); builder.append("{ pos="); builder.append(position()); builder.append(" limit="); builder.append(limit()); builder.append(" capacity="); builder.append(capacity()); builder.append(" }"); return builder.toString(); } private static boolean equals(byte x, byte y) { return x == y; } private void maybeMoveToNextArray() { if (currentArray.length == currentOffset) { if (currentArrayIndex >= 0 && currentArrayIndex < (contents.size() - 1)) { currentArray = contents.get(++currentArrayIndex); currentOffset = 0; } } } private static void validateReadTarget(int destSize, int offset, int length) { if ((offset | length) < 0) { throw new IndexOutOfBoundsException("offset and legnth must be non-negative"); } if (((long) offset + (long) length) > destSize) { throw new IndexOutOfBoundsException("target is to small for specified read size"); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy