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: 155418 $
4    * $Date: 2005-02-26 08:01:52 -0500 (Sat, 26 Feb 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: 155418 $
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 = -2;
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      private long requestContentLength = CONTENT_LENGTH_AUTO;
93  
94      // ----------------------------------------------------------- Constructors
95  
96      /***
97       * No-arg constructor.
98       *
99       * @since 2.0
100      */
101     public EntityEnclosingMethod() {
102         super();
103         setFollowRedirects(false);
104     }
105 
106     /***
107      * Constructor specifying a URI.
108      *
109      * @param uri either an absolute or relative URI
110      *
111      * @since 2.0
112      */
113     public EntityEnclosingMethod(String uri) {
114         super(uri);
115         setFollowRedirects(false);
116     }
117 
118     /***
119      * Returns <tt>true</tt> if there is a request body to be sent.
120      * 
121      * <P>This method must be overridden by sub-classes that implement
122      * alternative request content input methods
123      * </p>
124      * 
125      * @return boolean
126      * 
127      * @since 2.0beta1
128      */
129     protected boolean hasRequestContent() {
130         LOG.trace("enter EntityEnclosingMethod.hasRequestContent()");
131         return (this.requestEntity != null) 
132             || (this.requestStream != null) 
133             || (this.requestString != null);
134     }
135 
136     /***
137      * Clears the request body.
138      * 
139      * <p>This method must be overridden by sub-classes that implement
140      * alternative request content input methods.</p>
141      * 
142      * @since 2.0beta1
143      */
144     protected void clearRequestBody() {
145         LOG.trace("enter EntityEnclosingMethod.clearRequestBody()");
146         this.requestStream = null;
147         this.requestString = null;
148         this.requestEntity = null;
149     }
150 
151     /***
152      * Generates the request body.   
153      * 
154      * <p>This method must be overridden by sub-classes that implement
155      * alternative request content input methods.</p>
156      * 
157      * @return request body as an array of bytes. If the request content 
158      *          has not been set, returns <tt>null</tt>.
159      * 
160      * @since 2.0beta1
161      */
162     protected byte[] generateRequestBody() {
163         LOG.trace("enter EntityEnclosingMethod.renerateRequestBody()");
164         return null;
165     }
166 
167     protected RequestEntity generateRequestEntity() {
168         
169         byte[] requestBody = generateRequestBody();
170         if (requestBody != null) {
171             // use the request body, if it exists.
172             // this is just for backwards compatability
173             this.requestEntity = new ByteArrayRequestEntity(requestBody);
174         } else if (this.requestStream != null) {
175             this.requestEntity = new InputStreamRequestEntity(
176                 requestStream, 
177                 requestContentLength);
178             this.requestStream = null;
179         } else if (this.requestString != null) {
180             String charset = getRequestCharSet(); 
181             try {
182                 this.requestEntity = new StringRequestEntity(
183                         requestString, null, charset);
184             } catch (UnsupportedEncodingException e) {
185                 if (LOG.isWarnEnabled()) {
186                     LOG.warn(charset + " not supported");
187                 }
188                 this.requestEntity = new StringRequestEntity(
189                         requestString);
190             }
191         }
192 
193         return this.requestEntity;
194     }
195     
196     /***
197      * Entity enclosing requests cannot be redirected without user intervention
198      * according to RFC 2616.
199      *
200      * @return <code>false</code>.
201      *
202      * @since 2.0
203      */
204     public boolean getFollowRedirects() {
205         return false;
206     }
207 
208 
209     /***
210      * Entity enclosing requests cannot be redirected without user intervention 
211      * according to RFC 2616.
212      *
213      * @param followRedirects must always be <code>false</code>
214      */
215     public void setFollowRedirects(boolean followRedirects) {
216         if (followRedirects == true) {
217             throw new IllegalArgumentException("Entity enclosing requests cannot be redirected without user intervention");
218         }
219         super.setFollowRedirects(false);
220     }
221 
222     /***
223      * Sets length information about the request body.
224      *
225      * <p>
226      * Note: If you specify a content length the request is unbuffered. This
227      * prevents redirection and automatic retry if a request fails the first
228      * time. This means that the HttpClient can not perform authorization
229      * automatically but will throw an Exception. You will have to set the
230      * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
231      * </p>
232      *
233      * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
234      *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
235      *        is specified the content will not be buffered internally and the
236      *        Content-Length header of the request will be used. In this case
237      *        the user is responsible to supply the correct content length.
238      *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
239      *        before it is sent over the network.
240      * 
241      * @deprecated Use {@link #setContentChunked(boolean)} or 
242      * {@link #setRequestEntity(RequestEntity)}
243      */
244     public void setRequestContentLength(int length) {
245         LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
246         this.requestContentLength = length;
247     }
248 
249     /***
250      * Returns the request's charset.  The charset is parsed from the request entity's 
251      * content type, unless the content type header has been set manually. 
252      * 
253      * @see RequestEntity#getContentType()
254      * 
255      * @since 3.0
256      */
257     public String getRequestCharSet() {
258         if (getRequestHeader("Content-Type") == null) {
259             // check the content type from request entity
260             // We can't call getRequestEntity() since it will probably call
261             // this method.
262             if (this.requestEntity != null) {
263                 return getContentCharSet(
264                     new Header("Content-Type", requestEntity.getContentType()));
265             } else {
266                 return super.getRequestCharSet();
267             }
268         } else {
269             return super.getRequestCharSet();
270         }
271     }
272 
273     /***
274      * Sets length information about the request body.
275      *
276      * <p>
277      * Note: If you specify a content length the request is unbuffered. This
278      * prevents redirection and automatic retry if a request fails the first
279      * time. This means that the HttpClient can not perform authorization
280      * automatically but will throw an Exception. You will have to set the
281      * necessary 'Authorization' or 'Proxy-Authorization' headers manually.
282      * </p>
283      *
284      * @param length size in bytes or any of CONTENT_LENGTH_AUTO,
285      *        CONTENT_LENGTH_CHUNKED. If number of bytes or CONTENT_LENGTH_CHUNKED
286      *        is specified the content will not be buffered internally and the
287      *        Content-Length header of the request will be used. In this case
288      *        the user is responsible to supply the correct content length.
289      *        If CONTENT_LENGTH_AUTO is specified the request will be buffered
290      *        before it is sent over the network.
291      * 
292      * @deprecated Use {@link #setContentChunked(boolean)} or 
293      * {@link #setRequestEntity(RequestEntity)}
294      */
295     public void setRequestContentLength(long length) {
296         LOG.trace("enter EntityEnclosingMethod.setRequestContentLength(int)");
297         this.requestContentLength = length;
298     }
299 
300     /***
301      * Sets whether or not the content should be chunked.
302      * 
303      * @param chunked <code>true</code> if the content should be chunked
304      * 
305      * @since 3.0
306      */
307     public void setContentChunked(boolean chunked) {
308         this.requestContentLength = chunked ? CONTENT_LENGTH_CHUNKED : CONTENT_LENGTH_AUTO;
309     }
310     
311     /***
312      * Returns the length of the request body.
313      *
314      * @return number of bytes in the request body
315      */
316     protected long getRequestContentLength() {
317         LOG.trace("enter EntityEnclosingMethod.getRequestContentLength()");
318 
319         if (!hasRequestContent()) {
320             return 0;
321         }
322         // TODO what to do about setting request content and content length
323         if (this.requestContentLength != CONTENT_LENGTH_AUTO) {
324             return this.requestContentLength;
325         }
326         
327         if (this.requestEntity == null) {
328             this.requestEntity = generateRequestEntity(); 
329         }
330         return (this.requestEntity == null) ? 0 : this.requestEntity.getContentLength();
331     }
332 
333     /***
334      * Populates the request headers map to with additional 
335      * {@link org.apache.commons.httpclient.Header headers} to be submitted to 
336      * the given {@link HttpConnection}.
337      *
338      * <p>
339      * This implementation adds tt>Content-Length</tt> or <tt>Transfer-Encoding</tt>
340      * headers.
341      * </p>
342      *
343      * <p>
344      * Subclasses may want to override this method to to add additional
345      * headers, and may choose to invoke this implementation (via
346      * <tt>super</tt>) to add the "standard" headers.
347      * </p>
348      *
349      * @param state the {@link HttpState state} information associated with this method
350      * @param conn the {@link HttpConnection connection} used to execute
351      *        this HTTP method
352      *
353      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
354      *                     can be recovered from.
355      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
356      *                    cannot be recovered from.
357      *
358      * @see #writeRequestHeaders
359      * 
360      * @since 3.0
361      */
362     protected void addRequestHeaders(HttpState state, HttpConnection conn)
363     throws IOException, HttpException {
364         LOG.trace("enter EntityEnclosingMethod.addRequestHeaders(HttpState, "
365             + "HttpConnection)");
366 
367         super.addRequestHeaders(state, conn);
368         addContentLengthRequestHeader(state, conn);
369 
370         // only use the content type of the request entity if it has not already been
371         // set manually
372         if (getRequestHeader("Content-Type") == null) {
373             RequestEntity requestEntity = getRequestEntity();
374             if (requestEntity != null && requestEntity.getContentType() != null) {
375                 setRequestHeader("Content-Type", requestEntity.getContentType());
376             }
377         }
378     }
379     
380     /***
381      * Generates <tt>Content-Length</tt> or <tt>Transfer-Encoding: Chunked</tt>
382      * request header, as long as no <tt>Content-Length</tt> request header
383      * already exists.
384      *
385      * @param state current state of http requests
386      * @param conn the connection to use for I/O
387      *
388      * @throws IOException when errors occur reading or writing to/from the
389      *         connection
390      * @throws HttpException when a recoverable error occurs
391      */
392     protected void addContentLengthRequestHeader(HttpState state,
393                                                  HttpConnection conn)
394     throws IOException, HttpException {
395         LOG.trace("enter EntityEnclosingMethod.addContentLengthRequestHeader("
396                   + "HttpState, HttpConnection)");
397 
398         if ((getRequestHeader("content-length") == null) 
399             && (getRequestHeader("Transfer-Encoding") == null)) {
400             long len = getRequestContentLength();
401             if (len >= 0) {
402                 addRequestHeader("Content-Length", String.valueOf(len));
403             } else if ((len == CONTENT_LENGTH_CHUNKED) 
404                     && (getEffectiveVersion().greaterEquals(HttpVersion.HTTP_1_1))) {
405                 addRequestHeader("Transfer-Encoding", "chunked");
406             }
407         }
408     }
409 
410     /***
411      * Sets the request body to be the specified inputstream.
412      *
413      * @param body Request body content as {@link java.io.InputStream}
414      * 
415      * @deprecated use {@link #setRequestEntity(RequestEntity)}
416      */
417     public void setRequestBody(InputStream body) {
418         LOG.trace("enter EntityEnclosingMethod.setRequestBody(InputStream)");
419         clearRequestBody();
420         this.requestStream = body;
421     }
422 
423     /***
424      * Sets the request body to be the specified string.
425      * The string will be submitted, using the encoding
426      * specified in the Content-Type request header.<br>
427      * Example: <code>setRequestHeader("Content-type", "text/xml; charset=UTF-8");</code><br>
428      * Would use the UTF-8 encoding.
429      * If no charset is specified, the 
430      * {@link org.apache.commons.httpclient.HttpConstants#DEFAULT_CONTENT_CHARSET default}
431      * content encoding is used (ISO-8859-1).
432      *
433      * @param body Request body content as a string
434      * 
435      * @deprecated use {@link #setRequestEntity(RequestEntity)}
436      */
437     public void setRequestBody(String body) {
438         LOG.trace("enter EntityEnclosingMethod.setRequestBody(String)");
439         clearRequestBody();
440         this.requestString = body;
441     }
442 
443     /***
444      * Writes the request body to the given {@link HttpConnection connection}.
445      *
446      * @param state the {@link HttpState state} information associated with this method
447      * @param conn the {@link HttpConnection connection} used to execute
448      *        this HTTP method
449      *
450      * @return <tt>true</tt>
451      *
452      * @throws IOException if an I/O (transport) error occurs. Some transport exceptions
453      *                     can be recovered from.
454      * @throws HttpException  if a protocol exception occurs. Usually protocol exceptions 
455      *                    cannot be recovered from.
456      */
457     protected boolean writeRequestBody(HttpState state, HttpConnection conn)
458     throws IOException, HttpException {
459         LOG.trace(
460             "enter EntityEnclosingMethod.writeRequestBody(HttpState, HttpConnection)");
461         
462         if (!hasRequestContent()) {
463             LOG.debug("Request body has not been specified");
464             return true;
465         }
466 
467         long contentLength = getRequestContentLength();
468 
469         if ((contentLength == CONTENT_LENGTH_CHUNKED) 
470          && getEffectiveVersion().lessEquals(HttpVersion.HTTP_1_0)) {
471             throw new ProtocolException(
472                 "Chunked transfer encoding not allowed for " +
473                 getEffectiveVersion().toString());
474         }
475         
476         this.requestEntity = generateRequestEntity();
477         if (requestEntity == null) {
478             LOG.debug("Request body is empty");
479             return true;
480         }
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 == CONTENT_LENGTH_CHUNKED) {
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 = CONTENT_LENGTH_AUTO;
523         this.repeatCount = 0;
524         super.recycle();
525     }
526 
527     /***
528      * @return Returns the requestEntity.
529      * 
530      * @since 3.0
531      */
532     public RequestEntity getRequestEntity() {
533         return generateRequestEntity();
534     }
535 
536     /***
537      * @param requestEntity The requestEntity to set.
538      * 
539      * @since 3.0
540      */
541     public void setRequestEntity(RequestEntity requestEntity) {
542         clearRequestBody();
543         this.requestEntity = requestEntity;
544     }
545 
546 }