/* * ResponseEncoder.java February 2007 * * Copyright (C) 2001, Niall Gallagher <niallg@users.sf.net> * * 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 org.simpleframework.http.core; import static org.simpleframework.http.core.ContainerEvent.WRITE_BODY; import java.io.IOException; import java.nio.ByteBuffer; import org.simpleframework.http.Response; import org.simpleframework.transport.Channel; import org.simpleframework.transport.trace.Trace; /** * The <code>ResponseEncoder</code> object acts as a means to determine * the transfer encoding for the response body. This will ensure that * the correct HTTP headers are used when the transfer of the body begins. * In order to determine what headers to use this can be provided * with a content length value. If the <code>start</code> method is * provided with the content length then the HTTP headers will use a * Content-Length header as the message delimiter. If there is no * content length provided then the chunked encoding is used for * HTTP/1.1 and connection close is used for HTTP/1.0. * * @author Niall Gallagher * * @see org.simpleframework.http.core.BodyEncoder */ class ResponseEncoder { /** * This is used to create a encoder based on the HTTP headers. */ private BodyEncoderFactory factory; /** * This is used to determine the type of transfer required. */ private Conversation support; /** * This is the response message that is to be committed. */ private Response response; /** * Once the header is committed this is used to produce data. */ private BodyEncoder encoder; /** * This is the trace used to monitor events in the data transfer. */ private Trace trace; /** * Constructor for the <code>ResponseEncoder</code> object, this is * used to create an object used to transfer a response body. This * must be given a <code>Conversation</code> that can be used to set * and get information regarding the type of transfer required. * * @param observer this is used to signal for response completion * @param response this is the actual response message * @param support this is used to determine the semantics * @param channel this is the connected TCP channel for the response */ public ResponseEncoder(BodyObserver observer, Response response, Conversation support, Channel channel) { this.factory = new BodyEncoderFactory(observer, support, channel); this.trace = channel.getTrace(); this.response = response; this.support = support; } /** * This is used to determine if the transfer has started. It has * started when a encoder is created and the HTTP headers have * been sent, or at least handed to the underlying transport. * Once started the semantics of the connection can not change. * * @return this returns whether the transfer has started */ public boolean isStarted() { return encoder != null; } /** * This starts the transfer with no specific content length set. * This is typically used when dynamic data is emitted ans will * require chunked encoding for HTTP/1.1 and connection close * for HTTP/1.0. Once invoked the HTTP headers are committed. */ public void start() throws IOException { if(encoder != null) { throw new ResponseException("Transfer has already started"); } clear(); configure(); commit(); } /** * This starts the transfer with a known content length. This is * used when there is a Content-Length header set. This will not * encode the content for HTTP/1.1 however, HTTP/1.0 may need * a connection close if it does not have keep alive semantics. * * @param length this is the length of the response body */ public void start(int length) throws IOException { if(encoder != null) { throw new ResponseException("Transfer has already started"); } clear(); configure(length); commit(); } /** * This method is used to write content to the underlying socket. * This will make use of the <code>Producer</code> object to * encode the response body as required. If the encoder has not * been created then this will throw an exception. * * @param array this is the array of bytes to send to the client */ public void write(byte[] array) throws IOException { write(array, 0, array.length); } /** * This method is used to write content to the underlying socket. * This will make use of the <code>Producer</code> object to * encode the response body as required. If the encoder has not * been created then this will throw an exception. * * @param array this is the array of bytes to send to the client * @param off this is the offset within the array to send from * @param len this is the number of bytes that are to be sent */ public void write(byte[] array, int off, int len) throws IOException { if(encoder == null) { throw new ResponseException("Conversation details not ready"); } trace.trace(WRITE_BODY, len); encoder.encode(array, off, len); } /** * This method is used to write content to the underlying socket. * This will make use of the <code>Producer</code> object to * encode the response body as required. If the encoder has not * been created then this will throw an exception. * * @param buffer this is the buffer of bytes to send to the client */ public void write(ByteBuffer buffer) throws IOException { int mark = buffer.position(); int size = buffer.limit(); if(mark > size) { throw new ResponseException("Buffer position greater than limit"); } write(buffer, 0, size - mark); } /** * This method is used to write content to the underlying socket. * This will make use of the <code>Producer</code> object to * encode the response body as required. If the encoder has not * been created then this will throw an exception. * * @param buffer this is the buffer of bytes to send to the client * @param off this is the offset within the buffer to send from * @param len this is the number of bytes that are to be sent */ public void write(ByteBuffer buffer, int off, int len) throws IOException { if(encoder == null) { throw new ResponseException("Conversation details not ready"); } trace.trace(WRITE_BODY, len); encoder.encode(buffer, off, len); } /** * This method is used to flush the contents of the buffer to * the client. This method will block until such time as all of * the data has been sent to the client. If at any point there * is an error sending the content an exception is thrown. */ public void flush() throws IOException { if(encoder == null) { throw new ResponseException("Conversation details not ready"); } encoder.flush(); } /** * This is used to signal to the encoder that all content has * been written and the user no longer needs to write. This will * either close the underlying transport or it will notify the * monitor that the response has completed and the next request * can begin. This ensures the content is flushed to the client. */ public void close() throws IOException { if(encoder == null) { throw new ResponseException("Conversation details not ready"); } encoder.close(); } /** * This method is used to set the required HTTP headers on the * response. This will check the existing HTTP headers, and if * there is insufficient data chunked encoding will be used for * HTTP/1.1 and connection close will be used for HTTP/1.0. */ private void configure() throws IOException { long length = support.getContentLength(); boolean empty = support.isEmpty(); boolean tunnel = support.isTunnel(); if(tunnel) { support.setConnectionUpgrade(); } else if(empty) { support.setContentLength(0); } else if(length >= 0) { support.setContentLength(length); } else { support.setChunkedEncoded(); } encoder = factory.getInstance(); } /** * This method is used to set the required HTTP headers on the * response. This will check the existing HTTP headers, and if * there is insufficient data chunked encoding will be used for * HTTP/1.1 and connection close will be used for HTTP/1.0. * * @param count this is the number of bytes to be transferred */ private void configure(long count) throws IOException { long length = support.getContentLength(); if(support.isHead()) { if(count > 0) { configure(count, count); } else { configure(count, length); } } else { configure(count, count); } } /** * This method is used to set the required HTTP headers on the * response. This will check the existing HTTP headers, and if * there is insufficient data chunked encoding will be used for * HTTP/1.1 and connection close will be used for HTTP/1.0. * * @param count this is the number of bytes to be transferred * @param length this is the actual length value to be used */ private void configure(long count, long length) throws IOException { boolean empty = support.isEmpty(); boolean tunnel = support.isTunnel(); if(tunnel) { support.setConnectionUpgrade(); } else if(empty) { support.setContentLength(0); } else if(length >= 0) { support.setContentLength(length); } else { support.setChunkedEncoded(); } encoder = factory.getInstance(); } /** * This is used to clear any previous encoding that has been set * in the event that content length may be used instead. This is * used so that an override can be made to the transfer encoding * such that content length can be used instead. */ private void clear() throws IOException { support.setIdentityEncoded(); } /** * This is used to compose the HTTP header and send it over the * transport to the client. Once done the response is committed * and no more headers can be set, also the semantics of the * response have been committed and the encoder is created. */ private void commit() throws IOException { try { response.commit(); } catch(Exception cause) { throw new ResponseException("Unable to commit", cause); } } }