View Javadoc
1   //******************************************************************************
2   //
3   // File:    HttpResponse.java
4   // Package: edu.rit.http
5   // Unit:    Class edu.rit.http.HttpResponse
6   //
7   // This Java source file is copyright (C) 2006 by Alan Kaminsky. All rights
8   // reserved. For further information, contact the author, Alan Kaminsky, at
9   // ark@cs.rit.edu.
10  //
11  // This Java source file is part of the Parallel Java Library ("PJ"). PJ is free
12  // software; you can redistribute it and/or modify it under the terms of the GNU
13  // General Public License as published by the Free Software Foundation; either
14  // version 3 of the License, or (at your option) any later version.
15  //
16  // PJ is distributed in the hope that it will be useful, but WITHOUT ANY
17  // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
18  // A PARTICULAR PURPOSE. See the GNU General Public License for more details.
19  //
20  // Linking this library statically or dynamically with other modules is making a
21  // combined work based on this library. Thus, the terms and conditions of the GNU
22  // General Public License cover the whole combination.
23  //
24  // As a special exception, the copyright holders of this library give you
25  // permission to link this library with independent modules to produce an
26  // executable, regardless of the license terms of these independent modules, and
27  // to copy and distribute the resulting executable under terms of your choice,
28  // provided that you also meet, for each linked independent module, the terms
29  // and conditions of the license of that module. An independent module is a module
30  // which is not derived from or based on this library. If you modify this library,
31  // you may extend this exception to your version of the library, but you are not
32  // obligated to do so. If you do not wish to do so, delete this exception
33  // statement from your version.
34  //
35  // A copy of the GNU General Public License is provided in the file gpl.txt. You
36  // may also obtain a copy of the GNU General Public License on the World Wide
37  // Web at http://www.gnu.org/licenses/gpl.html.
38  //
39  //******************************************************************************
40  package edu.rit.http;
41  
42  import javax.annotation.Nullable;
43  import java.io.IOException;
44  import java.io.OutputStreamWriter;
45  import java.io.PrintWriter;
46  import java.net.Socket;
47  import java.nio.charset.Charset;
48  import java.util.HashMap;
49  import java.util.Map;
50  
51  // For unit test main program
52  // import java.net.InetSocketAddress;
53  // import java.net.ServerSocket;
54  /**
55   * Class HttpResponse encapsulates an HTTP response returned to a web browser.
56   * <P>
57   * Only HTTP/1.0 responses are supported. This means that only one HTTP response
58   * message can be sent over the connection to the web browser; the connection is
59   * closed after sending the HTTP response message.
60   * <P>
61   * To send an HTTP response message:
62   * <OL TYPE=1>
63   * <LI>
64   * Create an instance of class HttpResponse, giving the socket object
65   * representing the open connection to the web browser.
66   * <BR>&nbsp;
67   * <LI>
68   * Call methods to set the status code and headers as necessary.
69   * <BR>&nbsp;
70   * <LI>
71   * Call the <code>getPrintWriter()</code> method, and write the entity body to the
72   * print writer that is returned.
73   * <BR>&nbsp;
74   * <LI>
75   * Call the <code>close()</code> method.
76   * </OL>
77   *
78   * @author Alan Kaminsky
79   * @version 29-Jul-2010
80   */
81  public class HttpResponse {
82  
83  // Exported enumerations.
84      /**
85       * Enumeration HttpResponse.Status enumerates the status codes for an HTTP
86       * response message.
87       *
88       * @author Alan Kaminsky
89       * @version 10-Oct-2006
90       */
91      public static enum Status {
92  
93          /**
94           * The request has succeeded.
95           */
96          STATUS_200_OK("200 OK"),
97          /**
98           * The request has been fulfilled and resulted in a new resource being
99           * created.
100          */
101         STATUS_201_CREATED("201 Created"),
102         /**
103          * The request has been accepted for processing, but the processing has
104          * not been completed.
105          */
106         STATUS_202_ACCEPTED("202 Accepted"),
107         /**
108          * The server has fulfilled the request but there is no new information
109          * to send back.
110          */
111         STATUS_204_NO_CONTENT("204 No content"),
112         /**
113          * The requested resource has been assigned a new permanent URL and any
114          * future references to this resource should be done using that URL.
115          */
116         STATUS_301_MOVED_PERMANENTLY("301 Moved Permanently"),
117         /**
118          * The requested resource resides temporarily under a different URL.
119          */
120         STATUS_302_MOVED_TEMPORARILY("302 Moved Temporarily"),
121         /**
122          * If the client has performed a conditional GET request and access is
123          * allowed, but the document has not been modified since the date and
124          * time specified in the If-Modified-Since field, the server must
125          * respond with this status code and not send an Entity-Body to the
126          * client.
127          */
128         STATUS_304_NOT_MODIFIED("304 Not Modified"),
129         /**
130          * The request could not be understood by the server due to malformed
131          * syntax.
132          */
133         STATUS_400_BAD_REQUEST("400 Bad Request"),
134         /**
135          * The request requires user authentication.
136          */
137         STATUS_401_UNAUTHORIZED("401 Unauthorized"),
138         /**
139          * The server understood the request, but is refusing to fulfill it.
140          */
141         STATUS_403_FORBIDDEN("403 Forbidden"),
142         /**
143          * The server has not found anything matching the Request-URI.
144          */
145         STATUS_404_NOT_FOUND("404 Not Found"),
146         /**
147          * The server encountered an unexpected condition which prevented it
148          * from fulfilling the request.
149          */
150         STATUS_500_INTERNAL_SERVER_ERROR("500 Internal Server Error"),
151         /**
152          * The server does not support the functionality required to fulfill the
153          * request.
154          */
155         STATUS_501_NOT_IMPLEMENTED("501 Not Implemented"),
156         /**
157          * The server, while acting as a gateway or proxy, received an invalid
158          * response from the upstream server it accessed in attempting to
159          * fulfill the request.
160          */
161         STATUS_502_BAD_GATEWAY("502 Bad Gateway"),
162         /**
163          * The server is currently unable to handle the request due to a
164          * temporary overloading or maintenance of the server.
165          */
166         STATUS_503_SERVICE_UNAVAILABLE("503 Service Unavailable");
167 
168         private final String stringForm;
169 
170         /**
171          * Construct a new Status value.
172          *
173          * @param stringForm String form.
174          */
175         Status(String stringForm) {
176             this.stringForm = stringForm;
177         }
178 
179         /**
180          * Returns a string version of this Status value.
181          *
182          * @return String version.
183          */
184         public String toString() {
185             return stringForm;
186         }
187     }
188 
189 // Hidden data members.
190     private Socket mySocket;
191 
192     private Status myStatusCode = Status.STATUS_200_OK;
193     private String myContentType = "text/html";
194     private Charset myCharset = Charset.defaultCharset();
195 
196     private Map<String, String> myHeaderMap = new HashMap<String, String>();
197 
198     private PrintWriter myPrintWriter;
199 
200 // Exported constructors.
201     /**
202      * Construct a new HTTP response. The response is written to the output
203      * stream of the given socket.
204      *
205      * @param theSocket Socket.
206      * @exception NullPointerException (unchecked exception) Thrown if
207      * <code>theSocket</code> is null.
208      */
209     public HttpResponse(Socket theSocket) {
210         if (theSocket == null) {
211             throw new NullPointerException("HttpResponse(): theSocket is null");
212         }
213         mySocket = theSocket;
214         recordContentType();
215     }
216 
217 // Exported operations.
218     /**
219      * Set this HTTP response's status code. If not set, the default status code
220      * is STATUS_200_OK.
221      *
222      * @param theStatusCode Status code.
223      * @exception NullPointerException (unchecked exception) Thrown if
224      * <code>theStatusCode</code> is null.
225      * @exception IllegalStateException (unchecked exception) Thrown if the HTTP
226      * response headers have already been written to the socket output stream.
227      */
228     public void setStatusCode(Status theStatusCode) {
229         if (theStatusCode == null) {
230             throw new NullPointerException("HttpResponse.setStatusCode(): theStatusCode is null");
231         }
232         if (myPrintWriter != null) {
233             throw new IllegalStateException("HttpResponse.setStatusCode(): Headers already written");
234         }
235         myStatusCode = theStatusCode;
236     }
237 
238     /**
239      * Set this HTTP response's content type. If not set, the default content
240      * type is <code>"text/html"</code>.
241      *
242      * @param theContentType Content type.
243      * @exception NullPointerException (unchecked exception) Thrown if
244      * <code>theContentType</code> is null.
245      * @exception IllegalArgumentException (unchecked exception) Thrown if
246      * <code>theContentType</code> is zero length.
247      * @exception IllegalStateException (unchecked exception) Thrown if the HTTP
248      * response headers have already been written to the socket output stream.
249      */
250     public void setContentType(@Nullable String theContentType) {
251         if (theContentType == null) {
252             throw new NullPointerException("HttpResponse.setContentType(): theContentType is null");
253         }
254         if (theContentType.isEmpty()) {
255             throw new IllegalArgumentException("HttpResponse.setContentType(): theContentType is zero length");
256         }
257         if (myPrintWriter != null) {
258             throw new IllegalStateException("HttpResponse.setContentType(): Headers already written");
259         }
260         myContentType = theContentType;
261         recordContentType();
262     }
263 
264     /**
265      * Set this HTTP response's character set. If not set, the default character
266      * set is the platform default character set.
267      *
268      * @param theCharset Character set.
269      * @exception NullPointerException (unchecked exception) Thrown if
270      * <code>theCharset</code> is null.
271      * @exception IllegalStateException (unchecked exception) Thrown if the HTTP
272      * response headers have already been written to the socket output stream.
273      */
274     public void setCharset(Charset theCharset) {
275         if (theCharset == null) {
276             throw new NullPointerException("HttpResponse.setCharset(): theCharset is null");
277         }
278         if (myPrintWriter != null) {
279             throw new IllegalStateException("HttpResponse.setCharset(): Headers already written");
280         }
281         myCharset = theCharset;
282         recordContentType();
283     }
284 
285     /**
286      * Set this HTTP response's content length. If not set, the default is to
287      * omit the Content-Length header; closing this HTTP response marks the end
288      * of the entity body.
289      *
290      * @param theContentLength Content length.
291      * @exception IllegalArgumentException (unchecked exception) Thrown if
292      * <code>theContentLength</code> is less than 0.
293      * @exception IllegalStateException (unchecked exception) Thrown if the HTTP
294      * response headers have already been written to the socket output stream.
295      */
296     public void setContentLength(int theContentLength) {
297         if (theContentLength < 0) {
298             throw new IllegalArgumentException("HttpResponse.setContentLength(): theContentLength < 0");
299         }
300         if (myPrintWriter != null) {
301             throw new IllegalStateException("HttpResponse.setContentLength(): Headers already written");
302         }
303         myHeaderMap.put("Content-Length", "" + theContentLength);
304     }
305 
306     /**
307      * Set the given header in this HTTP response.
308      *
309      * @param theHeaderName Header name.
310      * @param theHeaderValue Header value.
311      * @exception NullPointerException (unchecked exception) Thrown if
312      * <code>theHeaderName</code> or
313      * <code>theHeaderValue</code> is null.
314      * @exception IllegalArgumentException (unchecked exception) Thrown if
315      * <code>theHeaderName</code> or
316      * <code>theHeaderValue</code> is zero length.
317      * @exception IllegalStateException (unchecked exception) Thrown if the HTTP
318      * response headers have already been written to the socket output stream.
319      */
320     public void setHeader(@Nullable String theHeaderName, @Nullable String theHeaderValue) {
321         if (theHeaderName == null) {
322             throw new NullPointerException("HttpResponse.setHeader(): theHeaderName is null");
323         }
324         if (theHeaderName.isEmpty()) {
325             throw new IllegalArgumentException("HttpResponse.setHeader(): theHeaderName is zero length");
326         }
327         if (theHeaderValue == null) {
328             throw new NullPointerException("HttpResponse.setHeader(): theHeaderValue is null");
329         }
330         if (theHeaderValue.isEmpty()) {
331             throw new IllegalArgumentException("HttpResponse.setHeader(): theHeaderValue is zero length");
332         }
333         if (myPrintWriter != null) {
334             throw new IllegalStateException("HttpResponse.setHeader(): Headers already written");
335         }
336         myHeaderMap.put(theHeaderName, theHeaderValue);
337     }
338 
339     /**
340      * Obtain the print writer for writing the entity body to this HTTP
341      * response. As a side effect, the HTTP response headers are written to the
342      * socket output stream.
343      *
344      * @return Print writer.
345      * @exception IOException Thrown if an I/O error occurred.
346      * @throws java.io.IOException if any.
347      */
348     public PrintWriter getPrintWriter()
349             throws IOException {
350         return writeHeaders();
351     }
352 
353     /**
354      * Close this HTTP response. If necessary, the HTTP response headers are
355      * written to the socket output stream.
356      *
357      * @exception IOException Thrown if an I/O error occurred.
358      * @throws java.io.IOException if any.
359      */
360     public void close()
361             throws IOException {
362         writeHeaders();
363         myPrintWriter.close();
364         mySocket.close();
365     }
366 
367 // Hidden operations.
368     /**
369      * Record the Content-Type header.
370      */
371     private void recordContentType() {
372         myHeaderMap.put("Content-Type",
373                 myContentType + "; charset=" + myCharset);
374     }
375 
376     /**
377      * Write the headers to the socket output stream if not already written, and
378      * return the print writer for writing to the socket output stream.
379      *
380      * @return Print writer.
381      *
382      * @exception IOException Thrown if an I/O error occurred.
383      */
384     private PrintWriter writeHeaders()
385             throws IOException {
386         if (myPrintWriter == null) {
387             myPrintWriter
388                     = new PrintWriter(new OutputStreamWriter(mySocket.getOutputStream(),
389                                     myCharset));
390             myPrintWriter.write("HTTP/1.0 " + myStatusCode + "\r\n");
391             for (Map.Entry<String, String> entry : myHeaderMap.entrySet()) {
392                 myPrintWriter.write(entry.getKey() + ": " + entry.getValue() + "\r\n");
393             }
394             myPrintWriter.write("\r\n");
395         }
396         return myPrintWriter;
397     }
398 
399 // Unit test main program.
400 //	/**
401 //	 * Unit test main program. The program listens for connections to
402 //	 * localhost:8080. The program reads each HTTP request from a web browser
403 //	 * and merely echoes the request data back to the browser.
404 //	 * <P>
405 //	 * Usage: java edu.rit.http.HttpResponse
406 //	 */
407 //	public static void main
408 //		(String[] args)
409 //		throws Exception
410 //		{
411 //		ServerSocket serversocket = new ServerSocket();
412 //		serversocket.bind (new InetSocketAddress ("localhost", 8080));
413 //		for (;;)
414 //			{
415 //			Socket socket = serversocket.accept();
416 //			HttpRequest request = new HttpRequest (socket);
417 //			HttpResponse response = new HttpResponse (socket);
418 //			if (request.isValid())
419 //				{
420 //				PrintWriter out = response.getPrintWriter();
421 //				out.println ("<HTML>");
422 //				out.println ("<HEAD>");
423 //				out.println ("</HEAD>");
424 //				out.println ("<BODY>");
425 //				out.println ("<UL>");
426 //				out.println ("<LI>");
427 //				out.print   ("Method = <code>\"");
428 //				out.print   (request.getMethod());
429 //				out.println ("\"</code>");
430 //				out.println ("<LI>");
431 //				out.print   ("URI = <code>\"");
432 //				out.print   (request.getUri());
433 //				out.println ("\"</code>");
434 //				out.println ("<LI>");
435 //				out.print   ("Version = <code>\"");
436 //				out.print   (request.getHttpVersion());
437 //				out.println ("\"</code>");
438 //				for (Map.Entry<String,String> entry : request.getHeaders())
439 //					{
440 //					out.println ("<LI>");
441 //					out.print   ("Header name = <code>\"");
442 //					out.print   (entry.getKey());
443 //					out.print   ("\"</code>, value = <code>\"");
444 //					out.print   (entry.getValue());
445 //					out.println ("\"</code>");
446 //					}
447 //				out.println ("</UL>");
448 //				out.println ("</BODY>");
449 //				out.println ("</HTML>");
450 //				}
451 //			else
452 //				{
453 //				response.setStatusCode (Status.STATUS_400_BAD_REQUEST);
454 //				PrintWriter out = response.getPrintWriter();
455 //				out.println ("<HTML>");
456 //				out.println ("<HEAD>");
457 //				out.println ("</HEAD>");
458 //				out.println ("<BODY>");
459 //				out.println ("<P>");
460 //				out.println ("400 Bad Request");
461 //				out.println ("<P>");
462 //				out.println ("You idiot.");
463 //				out.println ("</BODY>");
464 //				out.println ("</HTML>");
465 //				}
466 //			response.close();
467 //			}
468 //		}
469 }