View Javadoc

1   /*
2    * Copyright (c) 2005-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 java.io.BufferedReader;
16  import java.io.EOFException;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.InputStreamReader;
20  import java.io.OutputStream;
21  import java.io.PrintWriter;
22  import java.io.Reader;
23  import java.io.StringReader;
24  import java.io.UnsupportedEncodingException;
25  import java.io.Writer;
26  import java.net.URLDecoder;
27  import java.nio.charset.IllegalCharsetNameException;
28  import java.util.HashMap;
29  import java.util.Iterator;
30  import java.util.List;
31  import java.util.Map;
32  
33  import org.abstracthorizon.danube.connection.Connection;
34  import org.abstracthorizon.danube.connection.ConnectionException;
35  import org.abstracthorizon.danube.connection.ConnectionHandler;
36  import org.abstracthorizon.danube.connection.ConnectionWrapper;
37  import org.abstracthorizon.danube.http.util.EncodingPrintWrtier;
38  import org.abstracthorizon.danube.http.util.IOUtils;
39  import org.abstracthorizon.danube.http.util.MultiStringHashMap;
40  import org.abstracthorizon.danube.http.util.MultiStringMap;
41  import org.abstracthorizon.danube.http.util.StringPrintWriter;
42  import org.abstracthorizon.danube.support.RuntimeIOException;
43  
44  /**
45   * <p>
46   * This connection represents one HTTP request and response.
47   * It can be reused over the same underlaying connection
48   * (multiple requests over the same socket).
49   * </p>
50   * <p>
51   * This implementation handles HTTP request string, headers and parameters.
52   * </p>
53   *
54   * @author Daniel Sendula
55   */
56  public class HTTPConnectionImpl extends ConnectionWrapper implements HTTPConnection {
57  
58      /** Helper array of CR and LF characters */
59      protected static final String CRLF = "\r\n";
60  
61      /** Cached underlaying connection's input stream */
62      protected InputStream cachedInputStream;
63  
64      /** Cached underlaying connection's output stream */
65      protected OutputStream cachedOutputStream;
66  
67      /** Request headers */
68      protected MultiStringHashMap requestHeaders = new MultiStringHashMap();
69  
70      /**
71       * Request parameters. If more then one parameter with the same name
72       * is supplied then instead of a {@link String} as a type,
73       * a {@link List} is going to be used.
74       */
75      protected MultiStringHashMap requestParameters;
76  
77      /** Request method (GET, POST, etc) */
78      protected String requestMethod;
79  
80      /** Requested protocol (HTTP/1.0, HTTP/1.1 or null) */
81      protected String requestProtocol;
82  
83      /** Raw URI request. It contains everything, even get parameters... */
84      protected String requestURI;
85  
86      /** Similar as raw URI but without parameters */
87      protected String requestPath;
88  
89      /** Pointer to end of context part of the request path */
90      protected int contextPointer = 0;
91  
92      /** Pointer to end of component part of the request path */
93      protected int componentPointer = 0;
94  
95      /** Current path up to the current component */
96      protected String contextPath = "/";
97  
98      /** Current (processed) requestURI. This URI is only upto parameters */
99      protected String componentPath = "/";
100 
101     /** Current (processed) requestURI. This URI is only upto parameters */
102     protected String componentResourcePath;
103 
104     /** Response headers */
105     protected MultiStringHashMap responseHeaders = new MultiStringHashMap();
106 
107     /** Response status */
108     protected Status responseStatus = Status.OK;
109 
110     /** Response protocol */
111     protected String responseProtocol;
112 
113     /** Have headers been commited already. */
114     protected boolean headersCommitted;
115 
116     /** Writer */
117     protected StringPrintWriter writer;
118 
119     /** Map of attributes */
120     protected Map<String, Object> attributes;
121 
122     /** Reference to creator of this handler so forward can work from that point */
123     protected ConnectionHandler parent;
124 
125     /** Has writer been already returned */
126     protected boolean writerReturned = false;
127 
128     /** Shell content output be suppressed or not. This is needed for HEAD method. */
129     protected boolean suppressOutput = false;
130 
131     /** Buffered output */
132     protected HTTPBufferedOutputStream bufferedOutput;
133 
134     /** Buffered input */
135     protected HTTPBufferedInputStream bufferedInput;
136 
137     /** Cached print writer */
138     protected EncodingPrintWrtier cachedPrintWriter;
139 
140     /** Cached buffered reader */
141     protected BufferedReader cachedBufferedReader;
142 
143     /** Cached readers encoding */
144     protected String cachedReadersEncoding;
145 
146     /** Default buffer size */
147     protected int defaultBufferSize;
148 
149     /** Is expectation header handled */
150     protected boolean expectationIsHandled;
151 
152     /**
153      * Constructor.
154      *
155      * @param connection original connection
156      * @param parent parent connection handler needed for forwarding
157      * @param defaultBufferSize default buffer size
158      */
159     public HTTPConnectionImpl(Connection connection, ConnectionHandler parent, int defaultBufferSize) {
160         super(connection);
161         this.parent = parent;
162         cachedInputStream = (InputStream)connection.adapt(InputStream.class);
163         cachedOutputStream = (OutputStream)connection.adapt(OutputStream.class);
164         bufferedOutput = new HTTPBufferedOutputStream(this, cachedOutputStream, defaultBufferSize);
165         this.defaultBufferSize = defaultBufferSize;
166     }
167 
168     /**
169      * Constructor.
170      *
171      * @param connection original connection
172      * @param parent parent connection handler needed for forwarding
173      * @param inputStream input stream to be read from
174      * @param outputStrema output stream to be written to
175      * @param defaultBufferSize default buffer size
176      */
177     public HTTPConnectionImpl(Connection connection, ConnectionHandler parent, InputStream inputStream, OutputStream outputStream, int defaultBufferSize) {
178         super(connection);
179         this.parent = parent;
180         cachedInputStream = inputStream;
181         cachedOutputStream = outputStream;
182         bufferedOutput = new HTTPBufferedOutputStream(this, cachedOutputStream, defaultBufferSize);
183         this.defaultBufferSize = defaultBufferSize;
184     }
185 
186     /**
187      * This method processes request.
188      * It extracts method, uri, parameters (GET or POST),
189      * protocol version and headers.
190      *
191      * @throws IOException
192      */
193     public void reset() {
194         suppressOutput = false;
195         headersCommitted = false;
196         requestProtocol = null;
197         requestMethod = null;
198         contextPath = "/";
199         componentPath = "/";
200         componentResourcePath = null;
201         requestURI = null;
202         requestPath = null;
203         getRequestHeaders().clear();
204 
205         bufferedOutput.resetInternals();
206         bufferedOutput.setBufferSize(defaultBufferSize);
207         if (bufferedInput != null) {
208             bufferedInput.resetInternals();
209         }
210         if (cachedPrintWriter != null) {
211             cachedPrintWriter.resetInternals();
212         }
213 
214         // if (requestParameters != null) {
215         //    requestParameters.clear();
216         //}
217         requestParameters = null;
218 
219         responseStatus = Status.OK;
220         getResponseHeaders().clear();
221 
222         writerReturned = false;
223 
224         if (writer != null) {
225             writer.reset();
226         }
227 
228         if (attributes != null) {
229             attributes.clear();
230         }
231         expectationIsHandled = false;
232     }
233 
234     /**
235      * Should content output be suppressed or not.
236      *
237      * @return is content output suppressed or not
238      */
239     public boolean isSuppressOutput() {
240         return suppressOutput;
241     }
242 
243     /**
244      * Should output be suppressed or not.
245      *
246      * @param suppressOutput should the output be suppressed or not.
247      */
248     public void setSuppressOutput(boolean suppressOutput) {
249         this.suppressOutput = suppressOutput;
250         bufferedOutput.setSupporessOutput(suppressOutput);
251     }
252 
253     /**
254      * Returns buffer size
255      *
256      * @return buffer size
257      */
258     public int getBufferSize() {
259         return bufferedOutput.getBufferSize();
260     }
261 
262     /**
263      * Sets buffer size
264      *
265      * @param size buffer size
266      */
267     public void setBufferSize(int size) {
268         if (bufferedInput != null) {
269             bufferedInput.setBufferSize(size);
270         }
271         bufferedOutput.setBufferSize(size);
272     }
273 
274     /**
275      * This method processes request.
276      * It extracts method, uri, parameters (GET or POST),
277      * protocol version and headers.
278      *
279      * @throws IOException
280      */
281     public void processRequest() throws IOException {
282         reset();
283 
284         parseHttpRequestLine();
285         if ("HEAD".equals(requestMethod)) {
286             setSuppressOutput(true);
287         }
288         retrieveHeaders();
289         setupRequestPaths();
290         // parseGetParameters();
291         // if ("POST".equals(getRequestMethod())) {
292         //     parsePostParameters();
293         // }
294         responseProtocol = requestProtocol;
295     }
296 
297     protected void parseHttpRequestLine() throws IOException {
298         String request = readLine();
299         if (request == null) {
300             throw new EOFException();
301         }
302         int i = request.indexOf(' ');
303         if (i >= 0) {
304             requestMethod = request.substring(0, i);
305             int j = request.indexOf(' ', i+1);
306             if (i >= 0) {
307                 requestURI = URLDecoder.decode(request.substring(i+1, j), "UTF-8");
308                 requestProtocol = request.substring(j+1);
309             } else {
310                 requestURI = request.substring(i+1);
311             }
312         }
313     }
314 
315     /**
316      * Retrieves get parameters
317      * @throws IOException
318      */
319     protected void parseGetParameters() {
320         if (requestURI != null) {
321             int i = requestURI.indexOf('?');
322             if (i > 0) {
323                 String params = requestURI.substring(i+1);
324                 try {
325                     retrieveParams(requestParameters, new StringReader(params), params.length());
326                 } catch (IOException ignore) {
327                 }
328             }
329         }
330     }
331     /**
332      * Retrieves get parameters
333      * @throws IOException
334      */
335     protected void setupRequestPaths() {
336         contextPointer = 0;
337         componentPointer = 0;
338         contextPath = "/";
339         if (requestURI != null) {
340             int i = requestURI.indexOf('?');
341             if (i > 0) {
342                 componentResourcePath = requestURI.substring(0, i);
343             } else {
344                 componentResourcePath = requestURI;
345             }
346             requestPath = componentResourcePath;
347         }
348     }
349 
350     /**
351      * Retrieves post parameters
352      * @throws IOException
353      */
354     protected void parsePostParameters() throws IOException {
355         MultiStringMap requestHeaders = getRequestHeaders();
356         String contentLenString = requestHeaders.getOnly("Content-Length");
357         if (contentLenString != null) {
358             int len = Integer.parseInt(contentLenString);
359             if ("application/x-www-form-urlencoded".equals(requestHeaders.getOnly("Content-Type"))) {
360                 retrieveParams(requestParameters, new InputStreamReader(getContentInputStream()), len);
361             }
362         }
363     }
364 
365     /**
366      * Retrieves headers
367      * @throws IOException
368      */
369     protected void retrieveHeaders() throws IOException {
370         MultiStringMap requestHeaders = getRequestHeaders();
371         String line = readLine();
372         while ((line != null) && !"".equals(line)) {
373             int i = line.indexOf(':');
374             if (i >= 0) {
375                 String header = line.substring(0, i);
376                 String value = line.substring(i+2);
377                 requestHeaders.add(header, value);
378             }
379 
380             line = readLine();
381         }
382         if (bufferedInput != null) {
383             updateInputStreamLen();
384         }
385     }
386 
387     /**
388      * This method extracts parameters from givem reader.
389      * @param params parameters map
390      * @param r reader
391      * @param len number of chars to be read from reader
392      * @throws IOException
393      */
394     protected static void retrieveParams(MultiStringMap params, Reader r, int len) throws IOException {
395         // TODO optimise this for speed and resources...
396         // TODO URLDecode!
397         StringBuffer name = new StringBuffer();
398         StringBuffer value = new StringBuffer();
399         boolean retrieveName = true;
400         int i = 0;
401         while ((len > 0) && (i >= 0)) {
402             i = r.read();
403             len = len - 1;
404             if (i != -1) {
405                 char c = (char)i;
406                 if (c == '&') {
407                     if (!retrieveName) {
408                         addParam(params, name.toString(), value.toString());
409                         name = new StringBuffer();
410                         value = new StringBuffer();
411                         retrieveName = true;
412                     } else {
413                         // ERROR
414                     }
415                 } else if (retrieveName && (c == '=')) {
416                     retrieveName = false;
417                 } else if (retrieveName) {
418                     name.append(c);
419                 } else {
420                     value.append(c);
421                 }
422             }
423             if ((i == -1) || (len == 0)) {
424                 if (!retrieveName) {
425                     addParam(params, name.toString(), value.toString());
426                 } else {
427                     // ERROR
428                 }
429             }
430         }
431     }
432 
433     /**
434      * Adds parameter to the map. If parameter already exists and is
435      * of {@link String} type then it is replaced with a {@link List}
436      * and then old and new parameter stored under it.
437      * @param params parameter map
438      * @param name name of parameter
439      * @param value parameter's value
440      */
441     public static void addParam(MultiStringMap params, String name, String value) {
442         try {
443             name = URLDecoder.decode(name, "UTF-8");
444             value = URLDecoder.decode(value, "UTF-8");
445         } catch (UnsupportedEncodingException ignore) {
446         }
447 
448         params.add(name, value);
449     }
450 
451     /**
452      * Utility method that reads a line from input stream
453      * @return line string or <code>null</code> if <code>EOF</code> is reached
454      * @throws IOException
455      */
456     public String readLine() throws IOException {
457         // TODO - this blocks socket and thread. It needs to have a timeout!!!
458         InputStream in = cachedInputStream;
459         StringBuffer result = new StringBuffer();
460         int r = in.read();
461         if (r < 0) {
462             return null;
463         }
464         while ((r >= 0) && (r != '\n')) {
465             if (r >= ' ') {
466                 result.append((char)r);
467             }
468             r = in.read();
469         }
470         return result.toString();
471     }
472 
473 
474     /**
475      * Returns request headers map
476      * @return request headers map
477      */
478     public MultiStringMap getRequestHeaders() {
479         if (requestHeaders == null) {
480             createRequestHeaders();
481         }
482         return requestHeaders;
483     }
484 
485     protected void createRequestHeaders() {
486         requestHeaders = new MultiStringHashMap();
487     }
488 
489     /**
490      * Returns request parameters map. If more
491      * then one parameter is supplied with the same name
492      * then {@link List} returned with all parameter
493      * values in it.
494      * @return request parameters map
495      */
496     public MultiStringMap getRequestParameters() {
497         if (requestParameters == null) {
498             requestParameters = new MultiStringHashMap();
499             parseGetParameters();
500             if ("POST".equalsIgnoreCase(getRequestMethod())) {
501                 try {
502                     parsePostParameters();
503                 } catch (IOException e) {
504                     throw new ConnectionException(e);
505                 }
506             }
507         }
508         return requestParameters;
509     }
510 
511     /**
512      * Returns request method
513      * @return request method
514      */
515     public String getRequestMethod() {
516         return requestMethod;
517     }
518 
519     /**
520      * Returns request protocol
521      * @return request protocol
522      */
523     public String getRequestProtocol() {
524         return requestProtocol;
525     }
526 
527     /**
528      * Returns portion of request path up to component path
529      * @return portion of request path up to component path
530      */
531     public String getContextPath() {
532         return contextPath;
533     }
534 
535     /**
536      * Updates context path adding new path element to it
537      */
538     public void addComponentPathToContextPath() {
539 //        contextPath = requestPath.substring(0, componentPointer);
540 //        contextPointer = componentPointer;
541 //        componentPath = "/";
542 
543         contextPath = IOUtils.addPaths(contextPath, componentPath);
544 
545 //        if (contextPath.endsWith("/")) {
546 //            if (subpath.startsWith("/")) {
547 //                contextPath = contextPath + subpath.substring(1);
548 //            } else {
549 //                contextPath = contextPath + subpath;
550 //            }
551 //        } else {
552 //            if (subpath.startsWith("/")) {
553 //                contextPath = contextPath + subpath;
554 //            } else {
555 //                contextPath = contextPath + "/" + subpath;
556 //            }
557 //
558 //        }
559     }
560 
561     /**
562      * Returns request uri
563      * @return request uri
564      */
565     public String getComponentPath() {
566         return componentPath;
567     }
568 
569     /**
570      * Sets request uri.
571      * This is to be called from selectors not applicaiton code.
572      *
573      * @param requestURI
574      */
575     public void setComponentPath(String requestURI) {
576         this.componentPath = requestURI;
577     }
578 
579     /**
580      * Returns remainder of path after context path and component path is removed
581      * @return remainder of path after context path and component path is removed
582      */
583     public String getComponentResourcePath() {
584         return componentResourcePath;
585     }
586 
587 
588     /**
589      * Sets component resource path
590      * @param resourcePath component resource path
591      */
592     public void setComponentResourcePath(String resourcePath) {
593         this.componentResourcePath = resourcePath;
594     }
595 
596     /**
597      * This is similar to {@link #getRequestURI()} but without parameters part
598      * @return full (unchanged) uri
599      */
600     public String getRequestPath() {
601         return requestPath;
602     }
603 
604     /**
605      * Returns raw requested uri along with all parameters if supplied
606      * (GET method)
607      * @return raw requested uri
608      */
609     public String getRequestURI() {
610         return requestURI;
611     }
612 
613     /**
614      * Returns response headers map
615      * @return response headers map
616      */
617     public MultiStringMap getResponseHeaders() {
618         if (responseHeaders == null) {
619             createResponseHeaders();
620         }
621         return responseHeaders;
622     }
623 
624     protected void createResponseHeaders() {
625         responseHeaders = new MultiStringHashMap();
626     }
627 
628     /**
629      * Returns response status
630      * @return response status
631      */
632     public Status getResponseStatus() {
633         return responseStatus;
634     }
635 
636     /**
637      * Sets response status
638      * @param status response status
639      */
640     public void setResponseStatus(Status status) {
641         this.responseStatus = status;
642     }
643 
644     /**
645      * Returns response protocol
646      * @return response protocol
647      */
648     public String getResponseProtocol() {
649         return responseProtocol;
650     }
651 
652     /**
653      * Sets response protocol
654      * @param protocol response protocol
655      */
656     public void setResponseProtocol(String protocol) {
657         this.responseProtocol = protocol;
658     }
659 
660     /**
661      * Returns <code>true</code> if headers are already send back to the client
662      * @return <code>true</code> if headers are already send back to the client
663      */
664     public boolean isCommited() {
665         return headersCommitted;
666     }
667 
668     /**
669      * This method output response string and headers
670      * @throws IOException
671      */
672     public void commitHeaders() {
673         if (!expectationIsHandled) {
674             handleExpectationHeader();
675         }
676         if (!headersCommitted) {
677             try {
678                 if (requestProtocol.equals("HTTP/1.0")) {
679                     if (!responseProtocol.equals(requestMethod)) {
680                         responseProtocol = requestMethod;
681                     }
682                 }
683 
684                 MultiStringMap responseHeaders = getResponseHeaders();
685 
686                 String contentLength = responseHeaders.getOnly("Content-Length");
687                 String responseStatusCode = responseStatus.getCode();
688 
689                 if (responseStatusCode.startsWith("1") || responseStatusCode.equals("204") || responseStatusCode.equals("304")) {
690                     responseHeaders.putOnly("Content-Length", "0");
691                     responseHeaders.removeAll("Transfer-Encoding");
692                     bufferedOutput.setLimitedContentLength(0);
693                 } else if (responseStatusCode.equals("200") && responseHeaders.containsKey("Content-Range")) {
694                     responseStatus = Status.PARTIAL_CONTENT;
695                 } else if ("HEAD".equals(requestMethod)) {
696                     // TODO check this!!!
697                     bufferedOutput.setLimitedContentLength(0);
698                 } else {
699 
700                     int len = -1;
701                     if (contentLength != null) {
702                         try {
703                             len = Integer.parseInt(contentLength);
704                         } catch (NumberFormatException e) {
705                             responseHeaders.removeAll("Content-Length");
706                             len = -1;
707                         }
708                     }
709                     bufferedOutput.setLimitedContentLength(len);
710                     boolean chunkedEncoding = (len < 0) && "HTTP/1.1".equals(responseProtocol);
711                     bufferedOutput.setChunkEncoding(chunkedEncoding);
712                     if (chunkedEncoding) {
713                         responseHeaders.removeAll("Content-Length");
714                         responseHeaders.putOnly("Transfer-Encoding", "chunked");
715                     }
716                 }
717 
718                 StringBuffer headersBuffer = new StringBuffer(1024);
719                 headersBuffer.append(responseProtocol).append(' ').append(responseStatus.getFullStatus()).append(CRLF);
720 
721                 Iterator<String> it = responseHeaders.keySet().iterator();
722                 while (it.hasNext()) {
723                     String key = it.next();
724                     String[] headers = responseHeaders.getAsArray(key);
725                     for (String header : headers) {
726                         headersBuffer.append(key).append(": ").append(header).append(CRLF);
727                     }
728                 }
729                 headersBuffer.append(CRLF);
730                 cachedOutputStream.write(headersBuffer.toString().getBytes());
731                 cachedOutputStream.flush();
732                 headersCommitted = true;
733             } catch (IOException ioException) {
734                 throw new RuntimeIOException(ioException);
735             }
736         }
737     }
738 
739     /**
740      * Returns output stream but creates and commits headers before.
741      * @return output stream obtained from {@link Connection#getOutputStream}
742      */
743     public HTTPBufferedOutputStream getContentOutputStream() {
744         return bufferedOutput;
745     }
746 
747     /**
748      * Returns content input stream
749      * @return content input stream
750      */
751     public HTTPBufferedInputStream getContentInputStream() {
752         if (!expectationIsHandled) {
753             handleExpectationHeader();
754         }
755         if (bufferedInput == null) {
756             bufferedInput = new HTTPBufferedInputStream(cachedInputStream, defaultBufferSize);
757             updateInputStreamLen();
758         }
759 
760         return bufferedInput;
761     }
762 
763     protected void handleExpectationHeader() {
764         if (!expectationIsHandled) {
765             MultiStringMap requestHeaders = getRequestHeaders();
766             String expect = requestHeaders.getOnly("Expect");
767             if ((expect != null) && ("100-continue".equals(expect))) {
768                 String code = responseStatus.getCode();
769                 StringBuffer headersBuffer = new StringBuffer(1024);
770                 if (code.startsWith("2")) {
771                     headersBuffer.append(responseProtocol).append(' ').append(Status.CONTINUE.getFullStatus()).append(CRLF);
772                     // send continue
773                 } else {
774                     headersBuffer.append(responseProtocol).append(' ').append(Status.EXPECTATION_FAILED.getFullStatus()).append(CRLF);
775                     headersCommitted = true;
776                 }
777                 // TODO check this CRLF
778                 headersBuffer.append(CRLF);
779                 try {
780                     cachedOutputStream.write(headersBuffer.toString().getBytes());
781                     cachedOutputStream.flush();
782                 } catch (IOException e) {
783                     throw new RuntimeIOException(e);
784                 }
785             }
786             expectationIsHandled = true;
787         }
788     }
789 
790     /**
791      * Updates content input stream's length or chunked encoding
792      *
793      */
794     protected void updateInputStreamLen() {
795         MultiStringMap requestHeaders = getRequestHeaders();
796 
797         boolean chunkedEncoding = "chunked".equals(requestHeaders.getOnly("Transfer-Encoding"));
798         bufferedInput.setChunkEncoding(chunkedEncoding);
799         if (!chunkedEncoding) {
800             long len = -1;
801             String contentLength = requestHeaders.getOnly("Content-Length");
802             if (contentLength != null) {
803                 try {
804                     len = Long.parseLong(contentLength);
805                 } catch (NumberFormatException ignore) {
806                 }
807             }
808             if (len >= 0) {
809                 bufferedInput.setContentLength(len);
810             }
811         }
812     }
813 
814     /**
815      * Returns writer. If writer is not created yet it will be created on the fly.
816      * @return writer
817      * @throws RuntimeIOException
818      */
819     public PrintWriter getContentWriter() throws RuntimeIOException {
820         String encoding = getOutputEncoding();
821         if (cachedPrintWriter == null) {
822             try {
823                 cachedPrintWriter = new EncodingPrintWrtier(getContentOutputStream(), encoding);
824             } catch (IllegalCharsetNameException e) {
825                 throw new RuntimeException(e);
826             } catch (UnsupportedEncodingException e) {
827                 throw new RuntimeIOException(e);
828             }
829         } else {
830             try {
831                 cachedPrintWriter.setEncoding(encoding);
832             } catch (IllegalCharsetNameException e) {
833                 throw new RuntimeException(e);
834             } catch (UnsupportedEncodingException e) {
835                 throw new RuntimeIOException(e);
836             }
837         }
838         return cachedPrintWriter;
839     }
840 
841     /**
842      * Retuns response encoding
843      * @return response encoding
844      */
845     protected String getOutputEncoding() {
846         return null;
847     }
848 
849     /**
850      * Returns content reader
851      * @return content reader
852      */
853     public BufferedReader getContentReader() {
854         String encoding = getInputEncoding();
855         if ((encoding != cachedReadersEncoding)
856                 && ((encoding == null) || !encoding.equals(cachedReadersEncoding))) {
857             cachedBufferedReader = null;
858         }
859         if (cachedBufferedReader == null) {
860             if (encoding != null) {
861                 try {
862                     cachedBufferedReader = new BufferedReader(new InputStreamReader(getContentInputStream(), encoding));
863                 } catch (UnsupportedEncodingException e) {
864                     throw new RuntimeIOException(e);
865                 }
866             } else {
867                 cachedBufferedReader = new BufferedReader(new InputStreamReader(getContentInputStream()));
868             }
869             cachedReadersEncoding = encoding;
870         }
871         return cachedBufferedReader;
872     }
873 
874     /**
875      * Returns request encoding
876      * @return request encoding
877      */
878     protected String getInputEncoding() {
879         return null;
880     }
881 
882     /**
883      * Returns map of attributes. This method performs lazy instantition of the map.
884      *
885      * @return map of attribtues
886      */
887     public Map<String, Object> getAttributes() {
888         if (attributes == null) {
889             attributes = new HashMap<String, Object>();
890         }
891         return attributes;
892     }
893 
894     /**
895      * Redirects request
896      * @param uri uri to be redirected to
897      */
898     public void forward(String uri) {
899         // TODO check if url is local and if is not then perform client redirection
900         // TODO check if all connection parameters are set correctly (paths, uri, etc..)
901 
902         requestURI = uri;
903 
904         componentResourcePath = null;
905         contextPath = "/";
906         componentPath = "/";
907         parseGetParameters();
908 
909         parent.handleConnection(this);
910     }
911 
912     /**
913      * Adapts this class to {@link HTTPConnection}
914      * @param cls class
915      * @return adapter
916      */
917     @SuppressWarnings("unchecked")
918     public <T> T adapt(Class<T> cls) {
919         if (cls == HTTPConnection.class) {
920             return (T)this;
921         } else if ((cls == OutputStream.class) || (cls == HTTPBufferedOutputStream.class)) {
922             return (T)getContentOutputStream();
923         } else if ((cls == PrintWriter.class) || (cls == Writer.class) || (cls == EncodingPrintWrtier.class)) {
924             return (T)getContentWriter();
925         } else if ((cls == InputStream.class) || (cls == HTTPBufferedInputStream.class)) {
926             return (T)getContentInputStream();
927         } else if ((cls == Reader.class) || (cls == BufferedReader.class)) {
928             return (T)getContentReader();
929         } else {
930             return super.adapt(cls);
931         }
932     }
933 
934 }