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 }