/* * Copyright © 2015 Cask Data, Inc. * * 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 co.cask.cdap.data.stream; import co.cask.cdap.common.io.BinaryDecoder; import co.cask.cdap.common.io.ByteBuffers; import co.cask.cdap.common.io.Decoder; import co.cask.cdap.common.io.SeekableInputStream; import co.cask.cdap.common.stream.StreamEventDataCodec; import co.cask.cdap.data.file.ReadFilter; import co.cask.common.io.ByteBufferInputStream; import java.io.EOFException; import java.io.IOException; import java.nio.ByteBuffer; import java.util.Map; import javax.annotation.concurrent.NotThreadSafe; /** * A buffer for holding encoded stream events. It is used by {@link StreamDataFileReader} for holding * encoded stream events in each data block. */ @NotThreadSafe final class StreamEventBuffer { private final ByteBufferInputStream bufferInput; private final Decoder decoder; private ByteBuffer buffer; private long basePosition; StreamEventBuffer() { this.buffer = ByteBuffers.EMPTY_BUFFER; this.bufferInput = new ByteBufferInputStream(buffer); this.decoder = new BinaryDecoder(bufferInput); this.basePosition = -1L; } /** * Fills the internal buffer by reading from the given input stream. * * @param input input stream to read from * @param size number of bytes to read * @throws IOException if failed to read from the stream * @throws EOFException if failed to read the given number of bytes from the input */ void fillBuffer(SeekableInputStream input, int size) throws IOException { buffer.clear(); buffer = ensureCapacity(buffer, size); try { basePosition = input.getPos(); int bytesRead = 0; while (bytesRead != size) { int len = input.read(buffer.array(), bytesRead, size - bytesRead); if (len < 0) { throw new EOFException("Expected to read " + size + ", but only " + bytesRead + " was read"); } bytesRead += len; } buffer.limit(size); bufferInput.reset(buffer); } catch (IOException e) { // Make the buffer has nothing to read buffer.position(buffer.limit()); basePosition = -1L; throw e; } } /** * Returns {@code true} if there are events in the buffer, {@code false} otherwise. */ boolean hasEvent() { return buffer.hasRemaining(); } /** * Returns the position in the stream that this buffer is currently at or {@code -1} if nothing has been * read from the stream. */ long getPosition() { return basePosition >= 0 ? basePosition + buffer.position() : -1L; } /** * Returns the position in the stream that represents the end of this buffer or {@code -1} if nothing has * been read from the stream. */ long getEndPosition() { return basePosition >= 0 ? basePosition + buffer.limit() : -1L; } /** * Decodes a stream event from the buffer. * * @param timestamp timestamp of the {@link PositionStreamEvent} created * @param defaultHeaders the set of headers that will used as the default for the stream event * @param filter filter to apply to decide reading or skipping event * @return A {@link PositionStreamEvent} if the filter accept it, or {@code null} if rejected by the filter * @throws IOException if fails to decode event from the buffer */ PositionStreamEvent nextEvent(long timestamp, Map<String, String> defaultHeaders, ReadFilter filter) throws IOException { if (!hasEvent()) { throw new IOException("No more event in the buffer"); } long eventPos = basePosition + buffer.position(); if (filter.acceptOffset(eventPos)) { return new PositionStreamEvent(StreamEventDataCodec.decode(decoder, defaultHeaders), timestamp, eventPos); } StreamEventDataCodec.skip(decoder); return null; } /** * Ensures that the given {@link ByteBuffer} is of sufficient size. * * @param buffer The buffer to check for capacity * @param size Capacity needed * @return The given buffer if it is of sufficient size; otherwise, a new buffer of the given size will be returned. */ private ByteBuffer ensureCapacity(ByteBuffer buffer, int size) { return (buffer.remaining() >= size) ? buffer : ByteBuffer.allocate(size); } }