View Javadoc

1   /*
2    * $Header: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/EntityEnclosingMethod.java,v 1.18.2.5 2004/06/13 20:24:49 olegk Exp $
3    * $Revision: 1.18.2.5 $
4    * $Date: 2004/06/13 20:24:49 $
5    *
6    * ====================================================================
7    *
8    *  Copyright 2003-2004 The Apache Software Foundation
9    *
10   *  Licensed under the Apache License, Version 2.0 (the "License");
11   *  you may not use this file except in compliance with the License.
12   *  You may obtain a copy of the License at
13   *
14   *      http://www.apache.org/licenses/LICENSE-2.0
15   *
16   *  Unless required by applicable law or agreed to in writing, software
17   *  distributed under the License is distributed on an "AS IS" BASIS,
18   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19   *  See the License for the specific language governing permissions and
20   *  limitations under the License.
21   * ====================================================================
22   *
23   * This software consists of voluntary contributions made by many
24   * individuals on behalf of the Apache Software Foundation.  For more
25   * information on the Apache Software Foundation, please see
26   * <http://www.apache.org/>.
27   *
28   * [Additional notices, if required by prior licensing conditions]
29   *
30   */
31  
32  package org.apache.commons.httpclient.methods;
33  
34  import java.io.ByteArrayInputStream;
35  import java.io.ByteArrayOutputStream;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.io.OutputStream;
39  
40  import org.apache.commons.httpclient.ChunkedOutputStream;
41  import org.apache.commons.httpclient.ContentLengthInputStream;
42  import org.apache.commons.httpclient.HttpConnection;
43  import org.apache.commons.httpclient.HttpConstants;
44  import org.apache.commons.httpclient.HttpException;
45  import org.apache.commons.httpclient.HttpState;
46  import org.apache.commons.logging.Log;
47  import org.apache.commons.logging.LogFactory;
48  
49  /***
50   * This abstract class serves as a foundation for all HTTP methods 
51   * that can enclose an entity within requests 
52   *
53   * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
54   * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
55   *
56   * @since 2.0beta1
57   * @version $Revision: 1.18.2.5 $
58   */
59  public abstract class EntityEnclosingMethod extends ExpectContinueMethod {
60  
61      // ----------------------------------------- Static variables/initializers
62  
63      /***
64       * The content length will be calculated automatically. This implies
65       * buffering of the content.
66       */
67      public static final int CONTENT_LENGTH_AUTO = -2;
68  
69      /***
70       * The request will use chunked transfer encoding. Content length is not
71       * calculated and the content is not buffered.<br>
72       */
73      public static final int CONTENT_LENGTH_CHUNKED = -1;
74  
75      /*** LOG object for this class. */
76      private static final Log LOG = LogFactory.getLog(EntityEnclosingMethod.class);
77  
78      /*** The buffered request body, if any. */
79      private byte[] buffer = null;
80  
81      /*** The unbuffered request body, if any. */
82      private InputStream requestStream = null;
83  
84      /*** The request body as string, if any. */
85      private String requestString = null;
86  
87      /*** for optimization purpose, the generated request body may be
88       * cached when the method is being executed.
89       */
90      private byte[] contentCache = null; 
91      
92      /*** Counts how often the request was sent to the server. */
93      private int repeatCount = 0;
94  
95      /*** The content length of the <code>requestBodyStream</code> or one of
96       *  <code>CONTENT_LENGTH_AUTO</code> and <code>CONTENT_LENGTH_CHUNKED</code>.
97       */
98      private int requestContentLength = CONTENT_LENGTH_AUTO;
99  
100     // ----------------------------------------------------------- Constructors
101 
102     /***
103      * No-arg constructor.
104      *
105      * @since 2.0
106      */
107     public EntityEnclosingMethod() {
108         super();
109         setFollowRedirects(false);
110     }
111 
112     /***
113      * Constructor specifying a URI.
114      *
115      * @param uri either an absolute or relative URI
116      *
117      * @since 2.0
118      */
119     public EntityEnclosingMethod(String uri) {
120         super(uri);
121         setFollowRedirects(false);
122     }
123 
124     /***
125      * Constructor specifying a URI and a tempDir.
126      *
127      * @param uri either an absolute or relative URI
128      * @param tempDir directory to store temp files in
129      *
130      * @deprecated the client is responsible for disk I/O
131      * @since 2.0
132      */
133     public EntityEnclosingMethod(String uri, String tempDir) {
134         super(uri, tempDir);
135         setFollowRedirects(false);
136     }
137 
138     /***
139      * Constructor specifying a URI, tempDir and tempFile.
140      *
141      * @param uri either an absolute or relative URI
142      * @param tempDir directory to store temp files in
143      * @param tempFile file to store temporary data in
144      *
145      * @deprecated the client is responsible for disk I/O
146      * @since 2.0
147      */
148     public EntityEnclosingMethod(String uri, String tempDir, String tempFile) {
149         super(uri, tempDir, tempFile);
150         setFollowRedirects(false);
151     }
152 
153     /***
154      * Returns <tt>true</tt> if there is a request body to be sent.
155      * 
156      * <P>This method must be overridden by sub-classes that implement
157      * alternative request content input methods
158      * </p>
159      * 
160      * @return boolean
161      * 
162      * @since 2.0beta1
163      */
164     protected boolean hasRequestContent() {
165         LOG.trace("enter EntityEnclosingMethod.hasRequestContent()");
166         return (this.buffer != null) 
167             || (this.requestStream != null) 
168             || (this.requestString != null);
169     }
170 
171     /***
172      * Clears request body.
173      * 
174      * <p>This method must be overridden by sub-classes that implement
175      * alternative request content input methods.</p>
176      * 
177      * @since 2.0beta1
178      */
179     protected void clearRequestBody() {
180         LOG.trace("enter EntityEnclosingMethod.clearRequestBody()");
181         this.requestStream = null;
182         this.requestString = null;
183         this.buffer = null;
184         this.contentCache = null;
185     }
186 
187     /***
188      * Generates request body.   
189      * 
190      * <p>This method must be overridden by sub-classes that implement
191      * alternative request content input methods.</p>
192      * 
193      * @return request body as an array of bytes. If the request content 
194      *          has not been set, returns <tt>null</tt>.
195      * 
196      * @since 2.0beta1
197      */
198     protected byte[] generateRequestBody() {
199         LOG.trace("enter EntityEnclosingMethod.renerateRequestBody()");
200         if (this.requestStream != null) {
201             bufferContent();
202         }
203          
204         if (this.buffer != null) { 
205             return this.buffer;
206         } else if (this.requestString != null) {
207             return HttpConstants.getContentBytes(this.requestString, getRequestCharSet());
208         } else {
209             return null;
210         }
211     }
212 
213     /***
214      * Entity enclosing requests cannot be redirected without user intervention
215      * according to RFC 2616.
216      *
217      * @return <code>false</code>.
218      *
219      * @since 2.0
220      */
221     public boolean getFollowRedirects() {
222         return false;
223     }
224 
225 
226     /***
227      * Entity enclosing requests cannot be redirected without user intervention 
228      * according to RFC 2616.
229      *
230      * @param followRedirects must always be <code>false</code>
231      */
232     public void setFollowRedirects(boolean followRedirects) {
233         if (followRedirects == true) {
234             // TODO: EntityEnclosingMethod should inherit from HttpMethodBase rather than GetMethod
235             // Enable exception once the inheritence is fixed
236             //throw new IllegalArgumentException(
237             //  "Entity enclosing requests cannot be redirected without user intervention");
238         }
239         super.setFollowRedirects(false);
240     }
241 
242     /***
243      * Sets length information about the request body.
244      *
245      * <p>
246      * Note: If you specify a content length the request is unbuffered. This
247      * prevents redirection and automatic retry if a request fails the first
248      * time. This means that the HttpClient can not perform authorization
249      * automatically but will throw an Exception. You will have to set the
250      * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
251      * </p>
252      *
253      * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
254      *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
255      *        is specified the content will not be buffered internally and the
256      *        Content-Length header of the request will be used. In this case
257      *        the user is responsible to supply the correct content length.
258      *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
259      *        before it is sent over the network.
260      *
261      */
262     public void setRequestContentLength(int length) {
263         LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
264         this.requestContentLength = length;
265     }
266 
267     /***
268      * Overrides method of {@link org.apache.commons.httpclient.HttpMethodBase}
269      * to return the length of the request body.
270      *
271      * @return number of bytes in the request body
272      */
273     protected int getRequestContentLength() {
274         LOG.trace("enter EntityEnclosingMethod.getRequestContentLength()");
275 
276         if (!hasRequestContent()) {
277             return 0;
278         }
279         if (this.requestContentLength != CONTENT_LENGTH_AUTO) {
280             return this.requestContentLength;
281         }
282         if (this.contentCache == null) {
283             this.contentCache = generateRequestBody(); 
284         }
285         return (this.contentCache == null) ? 0 : this.contentCache.length;
286     }
287 
288     /***
289      * Generates <tt>Content-Length</tt> or <tt>Transfer-Encoding: Chunked</tt>
290      * request header, as long as no <tt>Content-Length</tt> request header
291      * already exists.
292      * 
293      * @param state the {@link HttpState state} information associated with this method
294      * @param conn the {@link HttpConnection connection} used to execute
295      *        this HTTP method
296      *
297      * @throws IOException if an I/O (transport) error occurs
298      * @throws HttpException  if a protocol exception occurs.
299      * @throws HttpRecoverableException if a recoverable transport error occurs. 
300      *                    Usually this kind of exceptions can be recovered from by
301      *                    retrying the HTTP method 
302      */
303     protected void addContentLengthRequestHeader(HttpState state,
304                                                  HttpConnection conn)
305     throws IOException, HttpException {
306         LOG.trace("enter HttpMethodBase.addContentLengthRequestHeader("
307                   + "HttpState, HttpConnection)");
308 
309         if ((getRequestHeader("content-length") == null)
310             && (getRequestHeader("Transfer-Encoding") == null)) {
311             int len = getRequestContentLength();
312             if (len >= 0) {
313                 addRequestHeader("Content-Length", String.valueOf(len));
314             } else if ((len == CONTENT_LENGTH_CHUNKED) && (isHttp11())) {
315                 addRequestHeader("Transfer-Encoding", "chunked");
316             }
317         }
318     }
319 
320     /***
321      * Sets the request body to be the specified inputstream.
322      *
323      * @param body Request body content as {@link java.io.InputStream}
324      */
325     public void setRequestBody(InputStream body) {
326         LOG.trace("enter EntityEnclosingMethod.setRequestBody(InputStream)");
327         clearRequestBody();
328         this.requestStream = body;
329     }
330 
331     /***
332      * Returns the request body as a {@link java.io.InputStream}.
333      * Calling this method will cause the content to be buffered.
334      *
335      * @return The request body as a {@link java.io.InputStream} if it has been set.
336      */
337     public InputStream getRequestBody() {
338         LOG.trace("enter EntityEnclosingMethod.getRequestBody()");
339         byte [] content = generateRequestBody();
340         if (content != null) {
341             return new ByteArrayInputStream(content);
342         } else {
343             return new ByteArrayInputStream(new byte[] {});
344         }
345     }
346 
347     /***
348      * Sets the request body to be the specified string.
349      * The string will be submitted, using the encoding
350      * specified in the Content-Type request header.<br>
351      * Example: <code>setRequestHeader("Content-type", "text/xml; charset=UTF-8");</code><br>
352      * Would use the UTF-8 encoding.
353      * If no charset is specified, the 
354      * {@link org.apache.commons.httpclient.HttpConstants#DEFAULT_CONTENT_CHARSET default}
355      * content encoding is used (ISO-8859-1).
356      *
357      * @param body Request body content as a string
358      */
359     public void setRequestBody(String body) {
360         LOG.trace("enter EntityEnclosingMethod.setRequestBody(String)");
361         clearRequestBody();
362         this.requestString = body;
363     }
364 
365     /***
366      * Returns the request body as a {@link java.lang.String}.
367      * Calling this method will cause the content to be buffered.
368      *
369      * @return the request body as a {@link java.lang.String}
370      * 
371      * @throws IOException when i/o error occurs while reading the request
372      */
373     public String getRequestBodyAsString() throws IOException {
374         LOG.trace("enter EntityEnclosingMethod.getRequestBodyAsString()");
375         byte [] content = generateRequestBody();
376         if (content != null) {
377             return HttpConstants.getContentString(content, getRequestCharSet());
378         } else {
379             return null;
380         }
381     }
382 
383 
384     /***
385      * Writes the request body to the given {@link HttpConnection connection}.
386      *
387      * @param state the {@link HttpState state} information associated with this method
388      * @param conn the {@link HttpConnection connection} used to execute
389      *        this HTTP method
390      *
391      * @return <tt>true</tt>
392      *
393      * @throws IOException if an I/O (transport) error occurs
394      * @throws HttpException  if a protocol exception occurs.
395      * @throws HttpRecoverableException if a recoverable transport error occurs. 
396      *                    Usually this kind of exceptions can be recovered from by
397      *                    retrying the HTTP method 
398      */
399     protected boolean writeRequestBody(HttpState state, HttpConnection conn)
400     throws IOException, HttpException {
401         LOG.trace(
402             "enter EntityEnclosingMethod.writeRequestBody(HttpState, HttpConnection)");
403         
404         if (!hasRequestContent()) {
405             LOG.debug("Request body has not been specified");
406             return true;
407         }
408 
409         int contentLength = getRequestContentLength();
410 
411         if ((contentLength == CONTENT_LENGTH_CHUNKED) && !isHttp11()) {
412             throw new HttpException(
413                 "Chunked transfer encoding not allowed for HTTP/1.0");
414         }
415         
416         InputStream instream = null;
417         if (this.requestStream != null) {
418             LOG.debug("Using unbuffered request body"); 
419             instream = this.requestStream;
420         } else { 
421             if (this.contentCache == null) {
422                 this.contentCache = generateRequestBody();
423             }
424             if (this.contentCache != null) { 
425                 LOG.debug("Using buffered request body"); 
426                 instream = new ByteArrayInputStream(this.contentCache);
427             }
428         }
429 
430         if (instream == null) {
431             LOG.debug("Request body is empty");
432             return true;
433         }
434 
435         if ((this.repeatCount > 0) && (this.contentCache == null)) {
436             throw new HttpException(
437                 "Unbuffered entity enclosing request can not be repeated.");
438         }
439 
440         this.repeatCount++;
441 
442         OutputStream outstream = conn.getRequestOutputStream();
443 
444         if (contentLength == CONTENT_LENGTH_CHUNKED) {
445             outstream = new ChunkedOutputStream(outstream);
446         }
447         if (contentLength >= 0) {
448             // don't need a watcher here - we're reading from something local,
449             // not server-side.
450             instream = new ContentLengthInputStream(instream, contentLength);
451         }
452 
453         byte[] tmp = new byte[4096];
454         int total = 0;
455         int i = 0;
456         while ((i = instream.read(tmp)) >= 0) {
457             outstream.write(tmp, 0, i);
458             total += i;
459         }
460         // This is hardly the most elegant solution to closing chunked stream
461         if (outstream instanceof ChunkedOutputStream) {
462             ((ChunkedOutputStream) outstream).writeClosingChunk();
463         }
464         if ((contentLength > 0) && (total < contentLength)) {
465             throw new IOException("Unexpected end of input stream after "
466                 + total + " bytes (expected " + contentLength + " bytes)");
467         }
468         LOG.debug("Request body sent");
469         return true;
470     }
471 
472     /***
473      * Recycles the HTTP method so that it can be used again.
474      * Note that all of the instance variables will be reset
475      * once this method has been called. This method will also
476      * release the connection being used by this HTTP method.
477      * 
478      * @see #releaseConnection()
479      * 
480      * @deprecated no longer supported and will be removed in the future
481      *             version of HttpClient
482      */
483     public void recycle() {
484         LOG.trace("enter EntityEnclosingMethod.recycle()");
485         clearRequestBody();
486         this.requestContentLength = CONTENT_LENGTH_AUTO;
487         this.repeatCount = 0;
488         super.recycle();
489     }
490 
491     /***
492      * Buffers request body input stream.
493      */
494     private void bufferContent() {
495         LOG.trace("enter EntityEnclosingMethod.bufferContent()");
496 
497         if (this.buffer != null) {
498             // Already been buffered
499             return;
500         }
501         if (this.requestStream != null) {
502             try {
503                 ByteArrayOutputStream tmp = new ByteArrayOutputStream();
504                 byte[] data = new byte[4096];
505                 int l = 0;
506                 while ((l = this.requestStream.read(data)) >= 0) {
507                     tmp.write(data, 0, l);
508                 }
509                 this.buffer = tmp.toByteArray();
510                 this.requestStream = null;
511             } catch (IOException e) {
512                 LOG.error(e.getMessage(), e);
513                 this.buffer = null;
514                 this.requestStream = null;
515             }
516         }
517     }
518 }