1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package org.apache.coyote; 19 20 import java.io.IOException; 21 import java.util.Locale; 22 23 import org.apache.tomcat.util.buf.ByteChunk; 24 import org.apache.tomcat.util.http.MimeHeaders; 25 26 /** 27 * Response object. 28 * 29 * @author James Duncan Davidson [duncan@eng.sun.com] 30 * @author Jason Hunter [jch@eng.sun.com] 31 * @author James Todd [gonzo@eng.sun.com] 32 * @author Harish Prabandham 33 * @author Hans Bergsten <hans@gefionsoftware.com> 34 * @author Remy Maucherat 35 */ 36 public final class Response { 37 38 39 // ----------------------------------------------------------- Constructors 40 41 42 public Response() { 43 } 44 45 46 // ----------------------------------------------------- Class Variables 47 48 /** 49 * Default locale as mandated by the spec. 50 */ 51 private static Locale DEFAULT_LOCALE = Locale.getDefault(); 52 53 54 // ----------------------------------------------------- Instance Variables 55 56 /** 57 * Status code. 58 */ 59 protected int status = 200; 60 61 62 /** 63 * Status message. 64 */ 65 protected String message = null; 66 67 68 /** 69 * Response headers. 70 */ 71 protected MimeHeaders headers = new MimeHeaders(); 72 73 74 /** 75 * Associated output buffer. 76 */ 77 protected OutputBuffer outputBuffer; 78 79 80 /** 81 * Notes. 82 */ 83 protected Object notes[] = new Object[Constants.MAX_NOTES]; 84 85 86 /** 87 * Committed flag. 88 */ 89 protected boolean commited = false; 90 91 92 /** 93 * Action hook. 94 */ 95 public ActionHook hook; 96 97 98 /** 99 * HTTP specific fields. 100 */ 101 protected String contentType = null; 102 protected String contentLanguage = null; 103 protected String characterEncoding = Constants.DEFAULT_CHARACTER_ENCODING; 104 protected long contentLength = -1; 105 private Locale locale = DEFAULT_LOCALE; 106 107 // General informations 108 private long bytesWritten=0; 109 110 /** 111 * Holds request error exception. 112 */ 113 protected Exception errorException = null; 114 115 /** 116 * Has the charset been explicitly set. 117 */ 118 protected boolean charsetSet = false; 119 120 /** 121 * Request error URI. 122 */ 123 protected String errorURI = null; 124 125 protected Request req; 126 127 // ------------------------------------------------------------- Properties 128 129 public Request getRequest() { 130 return req; 131 } 132 133 public void setRequest( Request req ) { 134 this.req=req; 135 } 136 137 public OutputBuffer getOutputBuffer() { 138 return outputBuffer; 139 } 140 141 142 public void setOutputBuffer(OutputBuffer outputBuffer) { 143 this.outputBuffer = outputBuffer; 144 } 145 146 147 public MimeHeaders getMimeHeaders() { 148 return headers; 149 } 150 151 152 public ActionHook getHook() { 153 return hook; 154 } 155 156 157 public void setHook(ActionHook hook) { 158 this.hook = hook; 159 } 160 161 162 // -------------------- Per-Response "notes" -------------------- 163 164 165 public final void setNote(int pos, Object value) { 166 notes[pos] = value; 167 } 168 169 170 public final Object getNote(int pos) { 171 return notes[pos]; 172 } 173 174 175 // -------------------- Actions -------------------- 176 177 178 public void action(ActionCode actionCode, Object param) { 179 if (hook != null) { 180 if( param==null ) 181 hook.action(actionCode, this); 182 else 183 hook.action(actionCode, param); 184 } 185 } 186 187 188 // -------------------- State -------------------- 189 190 191 public int getStatus() { 192 return status; 193 } 194 195 196 /** 197 * Set the response status 198 */ 199 public void setStatus( int status ) { 200 this.status = status; 201 } 202 203 204 /** 205 * Get the status message. 206 */ 207 public String getMessage() { 208 return message; 209 } 210 211 212 /** 213 * Set the status message. 214 */ 215 public void setMessage(String message) { 216 this.message = message; 217 } 218 219 220 public boolean isCommitted() { 221 return commited; 222 } 223 224 225 public void setCommitted(boolean v) { 226 this.commited = v; 227 } 228 229 230 // -----------------Error State -------------------- 231 232 233 /** 234 * Set the error Exception that occurred during 235 * request processing. 236 */ 237 public void setErrorException(Exception ex) { 238 errorException = ex; 239 } 240 241 242 /** 243 * Get the Exception that occurred during request 244 * processing. 245 */ 246 public Exception getErrorException() { 247 return errorException; 248 } 249 250 251 public boolean isExceptionPresent() { 252 return ( errorException != null ); 253 } 254 255 256 /** 257 * Set request URI that caused an error during 258 * request processing. 259 */ 260 public void setErrorURI(String uri) { 261 errorURI = uri; 262 } 263 264 265 /** Get the request URI that caused the original error. 266 */ 267 public String getErrorURI() { 268 return errorURI; 269 } 270 271 272 // -------------------- Methods -------------------- 273 274 275 public void reset() 276 throws IllegalStateException { 277 278 // Reset the headers only if this is the main request, 279 // not for included 280 contentType = null; 281 locale = DEFAULT_LOCALE; 282 contentLanguage = null; 283 characterEncoding = Constants.DEFAULT_CHARACTER_ENCODING; 284 contentLength = -1; 285 charsetSet = false; 286 287 status = 200; 288 message = null; 289 headers.clear(); 290 291 // Force the PrintWriter to flush its data to the output 292 // stream before resetting the output stream 293 // 294 // Reset the stream 295 if (commited) { 296 //String msg = sm.getString("servletOutputStreamImpl.reset.ise"); 297 throw new IllegalStateException(); 298 } 299 300 action(ActionCode.ACTION_RESET, this); 301 } 302 303 304 public void finish() throws IOException { 305 action(ActionCode.ACTION_CLOSE, this); 306 } 307 308 309 public void acknowledge() throws IOException { 310 action(ActionCode.ACTION_ACK, this); 311 } 312 313 314 // -------------------- Headers -------------------- 315 /** 316 * Warning: This method always returns <code>false<code> for Content-Type 317 * and Content-Length. 318 */ 319 public boolean containsHeader(String name) { 320 return headers.getHeader(name) != null; 321 } 322 323 324 public void setHeader(String name, String value) { 325 char cc=name.charAt(0); 326 if( cc=='C' || cc=='c' ) { 327 if( checkSpecialHeader(name, value) ) 328 return; 329 } 330 headers.setValue(name).setString( value); 331 } 332 333 334 public void addHeader(String name, String value) { 335 char cc=name.charAt(0); 336 if( cc=='C' || cc=='c' ) { 337 if( checkSpecialHeader(name, value) ) 338 return; 339 } 340 headers.addValue(name).setString( value ); 341 } 342 343 344 /** 345 * Set internal fields for special header names. 346 * Called from set/addHeader. 347 * Return true if the header is special, no need to set the header. 348 */ 349 private boolean checkSpecialHeader( String name, String value) { 350 // XXX Eliminate redundant fields !!! 351 // ( both header and in special fields ) 352 if( name.equalsIgnoreCase( "Content-Type" ) ) { 353 setContentType( value ); 354 return true; 355 } 356 if( name.equalsIgnoreCase( "Content-Length" ) ) { 357 try { 358 long cL=Long.parseLong( value ); 359 setContentLength( cL ); 360 return true; 361 } catch( NumberFormatException ex ) { 362 // Do nothing - the spec doesn't have any "throws" 363 // and the user might know what he's doing 364 return false; 365 } 366 } 367 if( name.equalsIgnoreCase( "Content-Language" ) ) { 368 // XXX XXX Need to construct Locale or something else 369 } 370 return false; 371 } 372 373 374 /** Signal that we're done with the headers, and body will follow. 375 * Any implementation needs to notify ContextManager, to allow 376 * interceptors to fix headers. 377 */ 378 public void sendHeaders() throws IOException { 379 action(ActionCode.ACTION_COMMIT, this); 380 commited = true; 381 } 382 383 384 // -------------------- I18N -------------------- 385 386 387 public Locale getLocale() { 388 return locale; 389 } 390 391 /** 392 * Called explicitely by user to set the Content-Language and 393 * the default encoding 394 */ 395 public void setLocale(Locale locale) { 396 397 if (locale == null) { 398 return; // throw an exception? 399 } 400 401 // Save the locale for use by getLocale() 402 this.locale = locale; 403 404 // Set the contentLanguage for header output 405 contentLanguage = locale.getLanguage(); 406 if ((contentLanguage != null) && (contentLanguage.length() > 0)) { 407 String country = locale.getCountry(); 408 StringBuffer value = new StringBuffer(contentLanguage); 409 if ((country != null) && (country.length() > 0)) { 410 value.append('-'); 411 value.append(country); 412 } 413 contentLanguage = value.toString(); 414 } 415 416 } 417 418 /** 419 * Return the content language. 420 */ 421 public String getContentLanguage() { 422 return contentLanguage; 423 } 424 425 /* 426 * Overrides the name of the character encoding used in the body 427 * of the response. This method must be called prior to writing output 428 * using getWriter(). 429 * 430 * @param charset String containing the name of the chararacter encoding. 431 */ 432 public void setCharacterEncoding(String charset) { 433 434 if (isCommitted()) 435 return; 436 if (charset == null) 437 return; 438 439 characterEncoding = charset; 440 charsetSet=true; 441 } 442 443 public String getCharacterEncoding() { 444 return characterEncoding; 445 } 446 447 /** 448 * Sets the content type. 449 * 450 * This method must preserve any response charset that may already have 451 * been set via a call to response.setContentType(), response.setLocale(), 452 * or response.setCharacterEncoding(). 453 * 454 * @param type the content type 455 */ 456 public void setContentType(String type) { 457 458 int semicolonIndex = -1; 459 460 if (type == null) { 461 this.contentType = null; 462 return; 463 } 464 465 /* 466 * Remove the charset param (if any) from the Content-Type, and use it 467 * to set the response encoding. 468 * The most recent response encoding setting will be appended to the 469 * response's Content-Type (as its charset param) by getContentType(); 470 */ 471 boolean hasCharset = false; 472 int len = type.length(); 473 int index = type.indexOf(';'); 474 while (index != -1) { 475 semicolonIndex = index; 476 index++; 477 while (index < len && Character.isSpace(type.charAt(index))) { 478 index++; 479 } 480 if (index+8 < len 481 && type.charAt(index) == 'c' 482 && type.charAt(index+1) == 'h' 483 && type.charAt(index+2) == 'a' 484 && type.charAt(index+3) == 'r' 485 && type.charAt(index+4) == 's' 486 && type.charAt(index+5) == 'e' 487 && type.charAt(index+6) == 't' 488 && type.charAt(index+7) == '=') { 489 hasCharset = true; 490 break; 491 } 492 index = type.indexOf(';', index); 493 } 494 495 if (!hasCharset) { 496 this.contentType = type; 497 return; 498 } 499 500 this.contentType = type.substring(0, semicolonIndex); 501 String tail = type.substring(index+8); 502 int nextParam = tail.indexOf(';'); 503 String charsetValue = null; 504 if (nextParam != -1) { 505 this.contentType += tail.substring(nextParam); 506 charsetValue = tail.substring(0, nextParam); 507 } else { 508 charsetValue = tail; 509 } 510 511 // The charset value may be quoted, but must not contain any quotes. 512 if (charsetValue != null && charsetValue.length() > 0) { 513 charsetSet=true; 514 charsetValue = charsetValue.replace('"', ' '); 515 this.characterEncoding = charsetValue.trim(); 516 } 517 } 518 519 public String getContentType() { 520 521 String ret = contentType; 522 523 if (ret != null 524 && characterEncoding != null 525 && charsetSet) { 526 ret = ret + ";charset=" + characterEncoding; 527 } 528 529 return ret; 530 } 531 532 public void setContentLength(int contentLength) { 533 this.contentLength = contentLength; 534 } 535 536 public void setContentLength(long contentLength) { 537 this.contentLength = contentLength; 538 } 539 540 public int getContentLength() { 541 long length = getContentLengthLong(); 542 543 if (length < Integer.MAX_VALUE) { 544 return (int) length; 545 } 546 return -1; 547 } 548 549 public long getContentLengthLong() { 550 return contentLength; 551 } 552 553 554 /** 555 * Write a chunk of bytes. 556 */ 557 public void doWrite(ByteChunk chunk/*byte buffer[], int pos, int count*/) 558 throws IOException 559 { 560 outputBuffer.doWrite(chunk, this); 561 bytesWritten+=chunk.getLength(); 562 } 563 564 // -------------------- 565 566 public void recycle() { 567 568 contentType = null; 569 contentLanguage = null; 570 locale = DEFAULT_LOCALE; 571 characterEncoding = Constants.DEFAULT_CHARACTER_ENCODING; 572 charsetSet = false; 573 contentLength = -1; 574 status = 200; 575 message = null; 576 commited = false; 577 errorException = null; 578 errorURI = null; 579 headers.clear(); 580 581 // update counters 582 bytesWritten=0; 583 } 584 585 public long getBytesWritten() { 586 return bytesWritten; 587 } 588 589 public void setBytesWritten(long bytesWritten) { 590 this.bytesWritten = bytesWritten; 591 } 592 }