View Javadoc

1   /*
2    * $Header: /home/jerenkrantz/tmp/commons/commons-convert/cvs/home/cvs/jakarta-commons//httpclient/src/java/org/apache/commons/httpclient/methods/EntityEnclosingMethod.java,v 1.39 2004/07/03 14:27:03 olegk Exp $
3    * $Revision: 161963 $
4    * $Date: 2005-04-19 16:25:06 -0400 (Tue, 19 Apr 2005) $
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   */
29  
30  package org.apache.commons.httpclient.methods;
31  
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.OutputStream;
35  import java.io.UnsupportedEncodingException;
36  
37  import org.apache.commons.httpclient.ChunkedOutputStream;
38  import org.apache.commons.httpclient.Header;
39  import org.apache.commons.httpclient.HttpConnection;
40  import org.apache.commons.httpclient.HttpException;
41  import org.apache.commons.httpclient.HttpState;
42  import org.apache.commons.httpclient.HttpVersion;
43  import org.apache.commons.httpclient.ProtocolException;
44  import org.apache.commons.logging.Log;
45  import org.apache.commons.logging.LogFactory;
46  
47  /***
48   * This abstract class serves as a foundation for all HTTP methods 
49   * that can enclose an entity within requests 
50   *
51   * @author <a href="mailto:oleg@ural.ru">Oleg Kalnichevski</a>
52   * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
53   *
54   * @since 2.0beta1
55   * @version $Revision: 161963 $
56   */
57  public abstract class EntityEnclosingMethod extends ExpectContinueMethod {
58  
59      // ----------------------------------------- Static variables/initializers
60  
61      /***
62       * The content length will be calculated automatically. This implies
63       * buffering of the content.
64       * @deprecated Use {@link InputStreamRequestEntity#CONTENT_LENGTH_AUTO}.
65       */
66      public static final long CONTENT_LENGTH_AUTO = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
67  
68      /***
69       * The request will use chunked transfer encoding. Content length is not
70       * calculated and the content is not buffered.<br>
71       * @deprecated Use {@link #setContentChunked(boolean)}.
72       */
73      public static final long CONTENT_LENGTH_CHUNKED = -1;
74  
75      /*** LOG object for this class. */
76      private static final Log LOG = LogFactory.getLog(EntityEnclosingMethod.class);
77  
78      /*** The unbuffered request body, if any. */
79      private InputStream requestStream = null;
80  
81      /*** The request body as string, if any. */
82      private String requestString = null;
83  
84      private RequestEntity requestEntity;
85      
86      /*** Counts how often the request was sent to the server. */
87      private int repeatCount = 0;
88  
89      /*** The content length of the <code>requestBodyStream</code> or one of
90       *  <code>CONTENT_LENGTH_AUTO</code> and <code>CONTENT_LENGTH_CHUNKED</code>.
91       * 
92       * @deprecated
93       */
94      private long requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
95      
96      private boolean chunked = false;
97  
98      // ----------------------------------------------------------- Constructors
99  
100     /***
101      * No-arg constructor.
102      *
103      * @since 2.0
104      */
105     public EntityEnclosingMethod() {
106         super();
107         setFollowRedirects(false);
108     }
109 
110     /***
111      * Constructor specifying a URI.
112      *
113      * @param uri either an absolute or relative URI
114      *
115      * @since 2.0
116      */
117     public EntityEnclosingMethod(String uri) {
118         super(uri);
119         setFollowRedirects(false);
120     }
121 
122     /***
123      * Returns <tt>true</tt> if there is a request body to be sent.
124      * 
125      * <P>This method must be overridden by sub-classes that implement
126      * alternative request content input methods
127      * </p>
128      * 
129      * @return boolean
130      * 
131      * @since 2.0beta1
132      */
133     protected boolean hasRequestContent() {
134         LOG.trace("enter EntityEnclosingMethod.hasRequestContent()");
135         return (this.requestEntity != null) 
136             || (this.requestStream != null) 
137             || (this.requestString != null);
138     }
139 
140     /***
141      * Clears the request body.
142      * 
143      * <p>This method must be overridden by sub-classes that implement
144      * alternative request content input methods.</p>
145      * 
146      * @since 2.0beta1
147      */
148     protected void clearRequestBody() {
149         LOG.trace("enter EntityEnclosingMethod.clearRequestBody()");
150         this.requestStream = null;
151         this.requestString = null;
152         this.requestEntity = null;
153     }
154 
155     /***
156      * Generates the request body.   
157      * 
158      * <p>This method must be overridden by sub-classes that implement
159      * alternative request content input methods.</p>
160      * 
161      * @return request body as an array of bytes. If the request content 
162      *          has not been set, returns <tt>null</tt>.
163      * 
164      * @since 2.0beta1
165      */
166     protected byte[] generateRequestBody() {
167         LOG.trace("enter EntityEnclosingMethod.renerateRequestBody()");
168         return null;
169     }
170 
171     protected RequestEntity generateRequestEntity() {
172         
173         byte[] requestBody = generateRequestBody();
174         if (requestBody != null) {
175             // use the request body, if it exists.
176             // this is just for backwards compatability
177             this.requestEntity = new ByteArrayRequestEntity(requestBody);
178         } else if (this.requestStream != null) {
179             this.requestEntity = new InputStreamRequestEntity(
180                 requestStream, 
181                 requestContentLength);
182             this.requestStream = null;
183         } else if (this.requestString != null) {
184             String charset = getRequestCharSet(); 
185             try {
186                 this.requestEntity = new StringRequestEntity(
187                         requestString, null, charset);
188             } catch (UnsupportedEncodingException e) {
189                 if (LOG.isWarnEnabled()) {
190                     LOG.warn(charset + " not supported");
191                 }
192                 this.requestEntity = new StringRequestEntity(
193                         requestString);
194             }
195         }
196 
197         return this.requestEntity;
198     }
199     
200     /***
201      * Entity enclosing requests cannot be redirected without user intervention
202      * according to RFC 2616.
203      *
204      * @return <code>false</code>.
205      *
206      * @since 2.0
207      */
208     public boolean getFollowRedirects() {
209         return false;
210     }
211 
212 
213     /***
214      * Entity enclosing requests cannot be redirected without user intervention 
215      * according to RFC 2616.
216      *
217      * @param followRedirects must always be <code>false</code>
218      */
219     public void setFollowRedirects(boolean followRedirects) {
220         if (followRedirects == true) {
221             throw new IllegalArgumentException("Entity enclosing requests cannot be redirected without user intervention");
222         }
223         super.setFollowRedirects(false);
224     }
225 
226     /***
227      * Sets length information about the request body.
228      *
229      * <p>
230      * Note: If you specify a content length the request is unbuffered. This
231      * prevents redirection and automatic retry if a request fails the first
232      * time. This means that the HttpClient can not perform authorization
233      * automatically but will throw an Exception. You will have to set the
234      * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
235      * </p>
236      *
237      * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
238      *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
239      *        is specified the content will not be buffered internally and the
240      *        Content-Length header of the request will be used. In this case
241      *        the user is responsible to supply the correct content length.
242      *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
243      *        before it is sent over the network.
244      * 
245      * @deprecated Use {@link #setContentChunked(boolean)} or 
246      * {@link #setRequestEntity(RequestEntity)}
247      */
248     public void setRequestContentLength(int length) {
249         LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
250         this.requestContentLength = length;
251     }
252 
253     /***
254      * Returns the request's charset.  The charset is parsed from the request entity's 
255      * content type, unless the content type header has been set manually. 
256      * 
257      * @see RequestEntity#getContentType()
258      * 
259      * @since 3.0
260      */
261     public String getRequestCharSet() {
262         if (getRequestHeader("Content-Type") == null) {
263             // check the content type from request entity
264             // We can't call getRequestEntity() since it will probably call
265             // this method.
266             if (this.requestEntity != null) {
267                 return getContentCharSet(
268                     new Header("Content-Type", requestEntity.getContentType()));
269             } else {
270                 return super.getRequestCharSet();
271             }
272         } else {
273             return super.getRequestCharSet();
274         }
275     }
276 
277     /***
278      * Sets length information about the request body.
279      *
280      * <p>
281      * Note: If you specify a content length the request is unbuffered. This
282      * prevents redirection and automatic retry if a request fails the first
283      * time. This means that the HttpClient can not perform authorization
284      * automatically but will throw an Exception. You will have to set the
285      * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
286      * </p>
287      *
288      * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
289      *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
290      *        is specified the content will not be buffered internally and the
291      *        Content-Length header of the request will be used. In this case
292      *        the user is responsible to supply the correct content length.
293      *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
294      *        before it is sent over the network.
295      * 
296      * @deprecated Use {@link #setContentChunked(boolean)} or 
297      * {@link #setRequestEntity(RequestEntity)}
298      */
299     public void setRequestContentLength(long length) {
300         LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
301         this.requestContentLength = length;
302     }
303 
304     /***
305      * Sets whether or not the content should be chunked.
306      * 
307      * @param chunked <code>true</code> if the content should be chunked
308      * 
309      * @since 3.0
310      */
311     public void setContentChunked(boolean chunked) {
312         this.chunked = chunked;
313     }
314     
315     /***
316      * Returns the length of the request body.
317      *
318      * @return number of bytes in the request body
319      */
320     protected long getRequestContentLength() {
321         LOG.trace("enter EntityEnclosingMethod.getRequestContentLength()");
322 
323         if (!hasRequestContent()) {
324             return 0;
325         }
326         if (this.chunked) {
327             return -1;
328         }
329         if (this.requestEntity == null) {
330             this.requestEntity = generateRequestEntity(); 
331         }
332         return (this.requestEntity == null) ? 0 : this.requestEntity.getContentLength();
333     }
334 
335     /***
336      * Populates the request headers map to with additional 
337      * {@link org.apache.commons.httpclient.Header headers} to be submitted to 
338      * the given {@link HttpConnection}.
339      *
340      * <p>
341      * This implementation adds tt>Content-Length</tt> or <tt>Transfer-Encoding</tt>
342      * headers.
343      * </p>
344      *
345      * <p>
346      * Subclasses may want to override this method to to add additional
347      * headers, and may choose to invoke this implementation (via
348      * <tt>super</tt>) to add the "standard" headers.
349      * </p>
350      *
351      * @param state the {@link HttpState state} information associated with this method
352      * @param conn the {@link HttpConnection connection} used to execute
353      *        this HTTP method
354      *
355      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
356      *                     can be recovered from.
357      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
358      *                    cannot be recovered from.
359      *
360      * @see #writeRequestHeaders
361      * 
362      * @since 3.0
363      */
364     protected void addRequestHeaders(HttpState state, HttpConnection conn)
365     throws IOException, HttpException {
366         LOG.trace("enter EntityEnclosingMethod.addRequestHeaders(HttpState, "
367             + "HttpConnection)");
368 
369         super.addRequestHeaders(state, conn);
370         addContentLengthRequestHeader(state, conn);
371 
372         // only use the content type of the request entity if it has not already been
373         // set manually
374         if (getRequestHeader("Content-Type") == null) {
375             RequestEntity requestEntity = getRequestEntity();
376             if (requestEntity != null && requestEntity.getContentType() != null) {
377                 setRequestHeader("Content-Type", requestEntity.getContentType());
378             }
379         }
380     }
381     
382     /***
383      * Generates <tt>Content-Length</tt> or <tt>Transfer-Encoding: Chunked</tt>
384      * request header, as long as no <tt>Content-Length</tt> request header
385      * already exists.
386      *
387      * @param state current state of http requests
388      * @param conn the connection to use for I/O
389      *
390      * @throws IOException when errors occur reading or writing to/from the
391      *         connection
392      * @throws HttpException when a recoverable error occurs
393      */
394     protected void addContentLengthRequestHeader(HttpState state,
395                                                  HttpConnection conn)
396     throws IOException, HttpException {
397         LOG.trace("enter EntityEnclosingMethod.addContentLengthRequestHeader("
398                   + "HttpState, HttpConnection)");
399 
400         if ((getRequestHeader("content-length") == null) 
401             && (getRequestHeader("Transfer-Encoding") == null)) {
402             long len = getRequestContentLength();
403             if (len < 0) {
404                 if (getEffectiveVersion().greaterEquals(HttpVersion.HTTP_1_1)) {
405                     addRequestHeader("Transfer-Encoding", "chunked");
406                 } else {
407                     throw new ProtocolException(getEffectiveVersion() + 
408                         " does not support chunk encoding");
409                 }
410             } else {
411                 addRequestHeader("Content-Length", String.valueOf(len));
412             }
413         }
414     }
415 
416     /***
417      * Sets the request body to be the specified inputstream.
418      *
419      * @param body Request body content as {@link java.io.InputStream}
420      * 
421      * @deprecated use {@link #setRequestEntity(RequestEntity)}
422      */
423     public void setRequestBody(InputStream body) {
424         LOG.trace("enter EntityEnclosingMethod.setRequestBody(InputStream)");
425         clearRequestBody();
426         this.requestStream = body;
427     }
428 
429     /***
430      * Sets the request body to be the specified string.
431      * The string will be submitted, using the encoding
432      * specified in the Content-Type request header.<br>
433      * Example: <code>setRequestHeader("Content-type", "text/xml; charset=UTF-8");</code><br>
434      * Would use the UTF-8 encoding.
435      * If no charset is specified, the 
436      * {@link org.apache.commons.httpclient.HttpConstants#DEFAULT_CONTENT_CHARSET default}
437      * content encoding is used (ISO-8859-1).
438      *
439      * @param body Request body content as a string
440      * 
441      * @deprecated use {@link #setRequestEntity(RequestEntity)}
442      */
443     public void setRequestBody(String body) {
444         LOG.trace("enter EntityEnclosingMethod.setRequestBody(String)");
445         clearRequestBody();
446         this.requestString = body;
447     }
448 
449     /***
450      * Writes the request body to the given {@link HttpConnection connection}.
451      *
452      * @param state the {@link HttpState state} information associated with this method
453      * @param conn the {@link HttpConnection connection} used to execute
454      *        this HTTP method
455      *
456      * @return <tt>true</tt>
457      *
458      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
459      *                     can be recovered from.
460      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
461      *                    cannot be recovered from.
462      */
463     protected boolean writeRequestBody(HttpState state, HttpConnection conn)
464     throws IOException, HttpException {
465         LOG.trace(
466             "enter EntityEnclosingMethod.writeRequestBody(HttpState, HttpConnection)");
467         
468         if (!hasRequestContent()) {
469             LOG.debug("Request body has not been specified");
470             return true;
471         }
472         if (this.requestEntity == null) {
473             this.requestEntity = generateRequestEntity(); 
474         }
475         if (requestEntity == null) {
476             LOG.debug("Request body is empty");
477             return true;
478         }
479 
480         long contentLength = getRequestContentLength();
481 
482         if ((this.repeatCount > 0) && !requestEntity.isRepeatable()) {
483             throw new ProtocolException(
484                 "Unbuffered entity enclosing request can not be repeated.");
485         }
486 
487         this.repeatCount++;
488 
489         OutputStream outstream = conn.getRequestOutputStream();
490         
491         if (contentLength < 0) {
492             outstream = new ChunkedOutputStream(outstream);
493         }
494         
495         requestEntity.writeRequest(outstream);
496         
497         // This is hardly the most elegant solution to closing chunked stream
498         if (outstream instanceof ChunkedOutputStream) {
499             ((ChunkedOutputStream) outstream).finish();
500         }
501         
502         outstream.flush();
503         
504         LOG.debug("Request body sent");
505         return true;
506     }
507 
508     /***
509      * Recycles the HTTP method so that it can be used again.
510      * Note that all of the instance variables will be reset
511      * once this method has been called. This method will also
512      * release the connection being used by this HTTP method.
513      * 
514      * @see #releaseConnection()
515      * 
516      * @deprecated no longer supported and will be removed in the future
517      *             version of HttpClient
518      */
519     public void recycle() {
520         LOG.trace("enter EntityEnclosingMethod.recycle()");
521         clearRequestBody();
522         this.requestContentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
523         this.repeatCount = 0;
524         this.chunked = false;
525         super.recycle();
526     }
527 
528     /***
529      * @return Returns the requestEntity.
530      * 
531      * @since 3.0
532      */
533     public RequestEntity getRequestEntity() {
534         return generateRequestEntity();
535     }
536 
537     /***
538      * @param requestEntity The requestEntity to set.
539      * 
540      * @since 3.0
541      */
542     public void setRequestEntity(RequestEntity requestEntity) {
543         clearRequestBody();
544         this.requestEntity = requestEntity;
545     }
546 
547 }