/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.jdk.connector; import java.io.IOException; import java.nio.ByteBuffer; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; /** * Body stream that can operate either synchronously or asynchronously. See {@link BodyOutputStream} for details. * * @author Petr Janouch (petr.janouch at oracle.com) */ class ChunkedBodyOutputStream extends BodyOutputStream { private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0); private final int chunkSize; private final int encodedFullChunkSize; // this stream is buffering by default; it has pending data up to dataBuffer.capacity() private final ByteBuffer dataBuffer; // in sync. mode, the write operations will block until the stream is opened for data private final CountDownLatch initialBlockingLatch = new CountDownLatch(1); private volatile Filter<ByteBuffer, ?, ?, ?> downstreamFilter; private volatile WriteListener writeListener = null; // an internal listener, so the connector can be notified when the stream has been closed (=body has been sent) private volatile Listener closeListener; // mode this stream operates in private volatile Mode mode = Mode.UNDECIDED; private volatile boolean ready = false; // flag to make sure that a listener is called only for the first time or after isReady() returned false private volatile boolean callListener = true; private volatile boolean closed = false; ChunkedBodyOutputStream(int chunkSize) { this.chunkSize = chunkSize; this.dataBuffer = ByteBuffer.allocate(chunkSize); this.encodedFullChunkSize = HttpRequestEncoder.getChunkSize(chunkSize); } @Override public synchronized void setWriteListener(WriteListener writeListener) { if (this.writeListener != null) { throw new IllegalStateException(LocalizationMessages.WRITE_LISTENER_SET_ONLY_ONCE()); } assertAsynchronousOperation(); this.writeListener = writeListener; commitToMode(); if (ready && callListener) { callOnWritePossible(); } } @Override public void write(byte[] b, int off, int len) throws IOException { commitToMode(); // input validation borrowed from OutputStream if (b == null) { throw new NullPointerException(); } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return; } assertValidState(); doInitialBlocking(); if (len < dataBuffer.remaining()) { // if the data fit into the buffer, use write per byte for (int i = off; i < off + len; i++) { write(b[i]); } } else { // if the data overflow the buffer, send a multiple of the buffer size and buffer the remainder int currentDataLength = dataBuffer.position() + len; int remainder = currentDataLength % dataBuffer.capacity(); // buffer that will be send ByteBuffer buffer = ByteBuffer.allocate(currentDataLength - remainder); dataBuffer.flip(); // put currently buffered data buffer.put(dataBuffer); // fill the rest with passed data buffer.put(b, off, len - remainder); buffer.flip(); dataBuffer.clear(); // buffer remaining data dataBuffer.put(b, off + len - remainder, remainder); // send the to-be-written buffer write(buffer); } } @Override public void flush() throws IOException { super.flush(); if (mode == Mode.UNDECIDED) { // if we are not committed to any mode, any of the write operations has not been invoked yet return; } if (mode == Mode.ASYNCHRONOUS) { assertValidState(); } if (dataBuffer.position() == 0) { // there is nothing buffered, so don't bother return; } dataBuffer.flip(); write(dataBuffer); } @Override public void write(int b) throws IOException { commitToMode(); assertValidState(); doInitialBlocking(); dataBuffer.put((byte) b); if (!dataBuffer.hasRemaining()) { // send the buffer if we have just filled it. dataBuffer.flip(); write(dataBuffer); } } @Override public boolean isReady() { // TODO we might support this in synchronous mode too assertAsynchronousOperation(); if (!ready) { callListener = true; } return ready; } private void assertValidState() { if (closed) { throw new IllegalStateException(LocalizationMessages.STREAM_CLOSED()); } if (mode == Mode.ASYNCHRONOUS && !ready) { // we are in asynchronous mode, but the user called write when the stream in non-ready state throw new IllegalStateException(LocalizationMessages.WRITE_WHEN_NOT_READY()); } } protected void write(final ByteBuffer byteBuffer) throws IOException { // do transport encoding on the raw data ByteBuffer httpChunk = encodeToHttp(byteBuffer); if (mode == Mode.SYNCHRONOUS) { final CountDownLatch writeLatch = new CountDownLatch(1); final AtomicReference<Throwable> error = new AtomicReference<>(); downstreamFilter.write(httpChunk, new CompletionHandler<ByteBuffer>() { @Override public void completed(ByteBuffer result) { writeLatch.countDown(); } @Override public void failed(Throwable t) { error.set(t); writeLatch.countDown(); } }); try { // block until the operation has completed writeLatch.await(); } catch (InterruptedException e) { throw new IOException(LocalizationMessages.WRITING_FAILED(), e); } byteBuffer.clear(); Throwable t = error.get(); // check fo any errors if (t != null) { throw new IOException(LocalizationMessages.WRITING_FAILED(), t); } } else { ready = false; downstreamFilter.write(httpChunk, new CompletionHandler<ByteBuffer>() { @Override public void completed(ByteBuffer result) { ready = true; byteBuffer.clear(); if (callListener) { callOnWritePossible(); } } @Override public void failed(Throwable throwable) { ready = false; writeListener.onError(throwable); } }); } } synchronized void open(Filter<ByteBuffer, ?, ?, ?> downstreamFilter) { this.downstreamFilter = downstreamFilter; initialBlockingLatch.countDown(); ready = true; if (mode == Mode.ASYNCHRONOUS && writeListener != null) { callOnWritePossible(); } } protected void doInitialBlocking() throws IOException { if (mode != Mode.SYNCHRONOUS || downstreamFilter != null) { return; } try { initialBlockingLatch.await(); } catch (InterruptedException e) { throw new IOException(e); } } protected synchronized void commitToMode() { // return if the mode has already been committed if (mode != Mode.UNDECIDED) { return; } // go asynchronous, if the user has made any move suggesting asynchronous mode if (writeListener != null) { mode = Mode.ASYNCHRONOUS; return; } // go synchronous, if the user has not made any suggesting asynchronous mode mode = Mode.SYNCHRONOUS; } private void assertAsynchronousOperation() { if (mode == Mode.SYNCHRONOUS) { throw new UnsupportedOperationException(LocalizationMessages.ASYNC_OPERATION_NOT_SUPPORTED()); } } private void callOnWritePossible() { callListener = false; try { writeListener.onWritePossible(); } catch (IOException e) { writeListener.onError(e); } } /** * Set a close listener which will be called when the user closes the stream. * <p/> * This is used to indicate that the body has been completely written. * * @param closeListener close listener. */ synchronized void setCloseListener(Listener closeListener) { this.closeListener = closeListener; } /** * Transform raw application data into HTTP body. * * @param byteBuffer application data. * @return http body part. */ protected ByteBuffer encodeToHttp(ByteBuffer byteBuffer) { // we expect the size of the buffer to be either a multiple of chunkSize // or smaller than chunkSize in case of the last content-carrying chunk and closing chunk (the one sent by close()) if (byteBuffer.remaining() < chunkSize) { return HttpRequestEncoder.encodeChunk(byteBuffer); } if (byteBuffer.remaining() % chunkSize != 0) { // the buffer is neither a multiple of chunkSize nor smaller than chunkSize throw new IllegalStateException(LocalizationMessages.BUFFER_INCORRECT_LENGTH()); } int numberOfChunks = byteBuffer.remaining() / chunkSize; ByteBuffer encodedChunks = ByteBuffer.allocate(numberOfChunks * encodedFullChunkSize); for (int i = 0; i < numberOfChunks; i++) { byteBuffer.position(i * chunkSize); byteBuffer.limit(i * chunkSize + chunkSize); ByteBuffer encodeChunk = HttpRequestEncoder.encodeChunk(byteBuffer); encodedChunks.put(encodeChunk); } encodedChunks.flip(); return encodedChunks; } @Override public void close() throws IOException { if (closed) { return; } commitToMode(); // just in case close is invoked without any data being written doInitialBlocking(); flush(); // chunk-encoded message is finished with an empty chunk write(EMPTY_BUFFER); super.close(); closed = true; synchronized (this) { if (closeListener != null) { closeListener.onClosed(); } } } /** * Set a close listener which will be called when the user closes the stream. * <p/> * This is used to indicate that the body has been completely written. */ interface Listener { void onClosed(); } private enum Mode { SYNCHRONOUS, ASYNCHRONOUS, UNDECIDED } }