| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.websocket.common.message; |
| |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.nio.ByteBuffer; |
| import java.util.concurrent.BlockingDeque; |
| import java.util.concurrent.LinkedBlockingDeque; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| |
| import org.eclipse.jetty.util.BufferUtil; |
| import org.eclipse.jetty.util.log.Log; |
| import org.eclipse.jetty.util.log.Logger; |
| |
| /** |
| * Support class for reading a (single) WebSocket BINARY message via a InputStream. |
| * <p> |
| * An InputStream that can access a queue of ByteBuffer payloads, along with expected InputStream blocking behavior. |
| */ |
| public class MessageInputStream extends InputStream implements MessageAppender |
| { |
| private static final Logger LOG = Log.getLogger(MessageInputStream.class); |
| private static final ByteBuffer EOF = ByteBuffer.allocate(0).asReadOnlyBuffer(); |
| |
| private final BlockingDeque<ByteBuffer> buffers = new LinkedBlockingDeque<>(); |
| private AtomicBoolean closed = new AtomicBoolean(false); |
| private final long timeoutMs; |
| private ByteBuffer activeBuffer = null; |
| |
| public MessageInputStream() |
| { |
| this(-1); |
| } |
| |
| public MessageInputStream(int timeoutMs) |
| { |
| this.timeoutMs = timeoutMs; |
| } |
| |
| @Override |
| public void appendFrame(ByteBuffer framePayload, boolean fin) throws IOException |
| { |
| if (LOG.isDebugEnabled()) |
| { |
| LOG.debug("Appending {} chunk: {}",fin?"final":"non-final",BufferUtil.toDetailString(framePayload)); |
| } |
| |
| // If closed, we should just toss incoming payloads into the bit bucket. |
| if (closed.get()) |
| { |
| return; |
| } |
| |
| // Put the payload into the queue, by copying it. |
| // Copying is necessary because the payload will |
| // be processed after this method returns. |
| try |
| { |
| if (framePayload == null) |
| { |
| // skip if no payload |
| return; |
| } |
| |
| int capacity = framePayload.remaining(); |
| if (capacity <= 0) |
| { |
| // skip if no payload data to copy |
| return; |
| } |
| // TODO: the copy buffer should be pooled too, but no buffer pool available from here. |
| ByteBuffer copy = framePayload.isDirect()?ByteBuffer.allocateDirect(capacity):ByteBuffer.allocate(capacity); |
| copy.put(framePayload).flip(); |
| buffers.put(copy); |
| } |
| catch (InterruptedException e) |
| { |
| throw new IOException(e); |
| } |
| finally |
| { |
| if (fin) |
| { |
| buffers.offer(EOF); |
| } |
| } |
| } |
| |
| @Override |
| public void close() throws IOException |
| { |
| if (closed.compareAndSet(false, true)) |
| { |
| buffers.offer(EOF); |
| super.close(); |
| } |
| } |
| |
| @Override |
| public void mark(int readlimit) |
| { |
| // Not supported. |
| } |
| |
| @Override |
| public boolean markSupported() |
| { |
| return false; |
| } |
| |
| @Override |
| public void messageComplete() |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Message completed"); |
| buffers.offer(EOF); |
| } |
| |
| @Override |
| public int read() throws IOException |
| { |
| try |
| { |
| if (closed.get()) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Stream closed"); |
| return -1; |
| } |
| |
| // grab a fresh buffer |
| while (activeBuffer == null || !activeBuffer.hasRemaining()) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Waiting {} ms to read", timeoutMs); |
| if (timeoutMs < 0) |
| { |
| // Wait forever until a buffer is available. |
| activeBuffer = buffers.take(); |
| } |
| else |
| { |
| // Wait at most for the given timeout. |
| activeBuffer = buffers.poll(timeoutMs, TimeUnit.MILLISECONDS); |
| if (activeBuffer == null) |
| { |
| throw new IOException(String.format("Read timeout: %,dms expired", timeoutMs)); |
| } |
| } |
| |
| if (activeBuffer == EOF) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Reached EOF"); |
| // Be sure that this stream cannot be reused. |
| closed.set(true); |
| // Removed buffers that may have remained in the queue. |
| buffers.clear(); |
| return -1; |
| } |
| } |
| |
| return activeBuffer.get() & 0xFF; |
| } |
| catch (InterruptedException x) |
| { |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Interrupted while waiting to read", x); |
| closed.set(true); |
| return -1; |
| } |
| } |
| |
| @Override |
| public void reset() throws IOException |
| { |
| throw new IOException("reset() not supported"); |
| } |
| } |