View Javadoc

1   /*
2    * Copyright (c) 2006-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.danube.http;
14  
15  import org.abstracthorizon.danube.support.RuntimeIOException;
16  
17  import java.io.IOException;
18  import java.io.OutputStream;
19  
20  /**
21   * This is buffered http output stream. It knows how to emit chuncked encoding.
22   *
23   * @author Daniel Sendula
24   */
25  public class HTTPBufferedOutputStream extends OutputStream {
26  
27      /** CR, LF */
28      public static final String CRLF = "\r\n";
29  
30      /** Buffer */
31      protected byte[] buffer;
32  
33      /** Pointer in the buffer */
34      protected int ptr;
35  
36      /** Connection */
37      protected HTTPConnectionImpl connection;
38  
39      /** Wrapped output stream */
40      protected OutputStream outputStream;
41  
42      /** Should output be suppressed */
43      protected boolean supporessOutput;
44  
45      /** Buffer size */
46      protected int bufferSize;
47  
48      /** Is closed */
49      protected boolean closed = false;
50  
51      /** Content length limit */
52      protected long limitedContentLength = -1;
53  
54      /** Number of sent bytes */
55      protected int sentbytes = 0;
56  
57      /** Chunk encoding */
58      protected boolean chunkEncoding = false;
59  
60      /**
61       * Constructor
62       *
63       * @param connection connection
64       * @param outputStream wrapped output stream
65       * @param defaultBufferSize default buffer size
66       */
67      public HTTPBufferedOutputStream(HTTPConnectionImpl connection, OutputStream outputStream, int defaultBufferSize) {
68          this.connection = connection;
69          this.outputStream = outputStream;
70          this.bufferSize = defaultBufferSize;
71      }
72  
73      /**
74       * Resets internals
75       */
76      public void resetInternals() {
77          closed = false;
78          supporessOutput = false;
79          ptr = 0;
80          limitedContentLength = -1;
81          sentbytes = 0;
82          chunkEncoding = false;
83      }
84  
85      /**
86       * Sets if output should be suppressed or not
87       *
88       * @param suppressOutput should output be suppressed
89       */
90      public void setSupporessOutput(boolean suppressOutput) {
91          this.supporessOutput = suppressOutput;
92      }
93  
94      /**
95       * Returns <code>true</code> if output should be suppressed
96       * @return <code>true</code> if output should be suppressed
97       */
98      public boolean isSupporessOutput() {
99          return supporessOutput;
100     }
101 
102     /**
103      * Returns buffer size
104      *
105      * @return buffer size
106      */
107     public int getBufferSize() {
108         return bufferSize;
109     }
110 
111     /**
112      * Sets buffer size
113      *
114      * @param size buffer size
115      * @throws IOException io exception
116      */
117     public void setBufferSize(int size) {
118         bufferSize = size;
119         if (buffer != null) {
120             if (size > buffer.length) {
121                 byte[] newBuffer = new byte[size];
122                 System.arraycopy(buffer, 0, newBuffer, 0, buffer.length);
123                 buffer = newBuffer;
124             } else if (size < buffer.length) {
125                 try {
126                     flush();
127                 } catch (IOException e) {
128                     throw new RuntimeIOException(e);
129                 }
130                 buffer = new byte[size];
131             }
132         }
133     }
134 
135     /**
136      * Returns limited content length
137      * @return limited content length
138      */
139     public long getLimitedContentLength() {
140         return limitedContentLength;
141     }
142 
143     /**
144      * Sets limited content length
145      * @param limitedContentLength limited content length
146      */
147     public void setLimitedContentLength(long limitedContentLength) {
148         this.limitedContentLength = limitedContentLength;
149     }
150 
151     /**
152      * Returns if chunk encoding
153      * @return if chunk encoding
154      */
155     public boolean isChunkEncoding() {
156         return chunkEncoding;
157     }
158 
159     /**
160      * Sets chunk encoding
161      * @param chunkEncoding is chunk encoding
162      */
163     public void setChunkEncoding(boolean chunkEncoding) {
164         this.chunkEncoding = chunkEncoding;
165     }
166 
167     /**
168      * Checks if buffer is created
169      *
170      * @throws IOException
171      */
172     protected void checkBuffer() throws IOException {
173         if (buffer == null) {
174             buffer = new byte[bufferSize];
175             ptr = 0;
176         }
177     }
178 
179     /**
180      * Implements flushing of the buffer
181      * @throws IOException
182      */
183     public void flushImpl() throws IOException {
184         if (closed) {
185             throw new IOException("Already closed");
186         }
187         if (ptr > 0) {
188             if (!connection.isCommited()) {
189                 connection.commitHeaders();
190             }
191 
192             if ((limitedContentLength >= 0) && ((sentbytes + ptr) > limitedContentLength)) {
193                 if (chunkEncoding) {
194                     outputStream.write((Long.toString(limitedContentLength - sentbytes, 16) + CRLF).getBytes());
195                     outputStream.write(buffer, 0, (int)(limitedContentLength - sentbytes));
196                     outputStream.write(CRLF.getBytes());
197                     // TODO is flush needed here?
198                 } else {
199                     outputStream.write(buffer, 0, (int)(limitedContentLength - sentbytes));
200                     // TODO or flush needed here?
201                 }
202                 throw new IOException("Content length exceeded asked value; requested to be sent " + (sentbytes + ptr) + ", limit " + limitedContentLength);
203             } else {
204                 if (chunkEncoding) {
205                     outputStream.write((Integer.toString(ptr, 16) + "\r\n").getBytes());
206                     outputStream.write(buffer, 0, ptr);
207                     outputStream.write(CRLF.getBytes());
208                     // TODO is flush needed here?
209                 } else {
210                     outputStream.write(buffer, 0, ptr);
211                     // TODO or flush needed here?
212                 }
213             }
214             ptr = 0;
215         }
216     }
217 
218     @Override
219     public void close() throws IOException {
220         if (!connection.isCommited()) {
221             // Convert Transfer-Encoding: chunked to Content-Length since nothing is commited yet!
222             connection.getResponseHeaders().removeAll("Transfer-Encoding");
223             chunkEncoding = false;
224             if (!supporessOutput) {
225                 connection.getResponseHeaders().putOnly("Content-Length", Integer.toString(ptr));
226             }
227         }
228         flushImpl();
229         if (!connection.isCommited()) {
230             connection.commitHeaders();
231         }
232         if (chunkEncoding) {
233             outputStream.write("0\r\n\r\n".getBytes());
234         }
235         outputStream.flush();
236         closed = true;
237     }
238 
239     @Override
240     public void write(int b) throws IOException {
241         if (closed) {
242             throw new IOException("Already closed");
243         }
244         if (!supporessOutput) {
245             if ((limitedContentLength >= 0) && ((sentbytes + 1) > limitedContentLength)) {
246                 throw new IOException("Content length exceeded asked value; requested to be sent " + (sentbytes + 1) + ", limit " + limitedContentLength);
247             }
248 
249             checkBuffer();
250             buffer[ptr] = (byte)b;
251             if (ptr == buffer.length) {
252                 flushImpl();
253             }
254         }
255     }
256 
257     @Override
258     public void write(byte[] buf, int start, int len) throws IOException {
259         if (closed) {
260             throw new IOException("Already closed");
261         }
262         if (!supporessOutput) {
263             if ((limitedContentLength >= 0) && ((sentbytes + len) > limitedContentLength)) {
264                 throw new IOException("Content length exceeded asked value; requested to be sent " + (sentbytes + len) + ", limit " + limitedContentLength);
265             }
266 
267             checkBuffer();
268             if (len > (buffer.length - ptr)) {
269                 flushImpl();
270                 if (!connection.isCommited()) {
271                     connection.commitHeaders();
272                 }
273                 if (chunkEncoding) {
274                     outputStream.write((Integer.toString(len, 16) + "\r\n").getBytes());
275                     outputStream.write(buf, start, len);
276                     outputStream.write(CRLF.getBytes());
277                     // TODO is flush needed here?
278                 } else {
279                     outputStream.write(buf, start, len);
280                 }
281             } else {
282                 System.arraycopy(buf, start, buffer, ptr, len);
283                 ptr = ptr + len;
284                 if (ptr == buffer.length) {
285                     flushImpl();
286                 }
287             }
288         }
289     }
290 
291     @Override
292     public void write(byte[] buf) throws IOException {
293         if (closed) {
294             throw new IOException("Already closed");
295         }
296         if (!supporessOutput) {
297             write(buf, 0, buf.length);
298         }
299     }
300 }