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>
67 * <LI>
68 * Call methods to set the status code and headers as necessary.
69 * <BR>
70 * <LI>
71 * Call the <code>getPrintWriter()</code> method, and write the entity body to the
72 * print writer that is returned.
73 * <BR>
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 }