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 }