001: /*
002: * RimfaxeHttpServletResponse.java
003: *
004: *
005: * Copyright (c) 2003 Rimfaxe ApS (www.rimfaxe.com).
006: * All rights reserved.
007: *
008: * This package is written by Lars Andersen <lars@rimfaxe.com>
009: * and licensed by Rimfaxe ApS.
010: *
011: * Redistribution and use in source and binary forms, with or without
012: * modification, are permitted provided that the following conditions
013: * are met:
014: *
015: * 1. Redistributions of source code must retain the above copyright
016: * notice, this list of conditions and the following disclaimer.
017: *
018: * 2. Redistributions in binary form must reproduce the above copyright
019: * notice, this list of conditions and the following disclaimer in
020: * the documentation and/or other materials provided with the
021: * distribution.
022: *
023: * 3. The end-user documentation included with the redistribution, if
024: * any, must include the following acknowlegement:
025: * "This product includes software developed by Rimfaxe ApS
026: (www.rimfaxe.com)"
027: * Alternately, this acknowlegement may appear in the software itself,
028: * if and wherever such third-party acknowlegements normally appear.
029: *
030: * 4. The names "Rimfaxe", "Rimfaxe Software", "Lars Andersen" and
031: * "Rimfaxe WebServer" must not be used to endorse or promote products
032: * derived from this software without prior written permission. For written
033: * permission, please contact info@rimfaxe.com
034: *
035: * 5. Products derived from this software may not be called "Rimfaxe"
036: * nor may "Rimfaxe" appear in their names without prior written
037: * permission of the Rimfaxe ApS.
038: *
039: * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
040: * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
041: * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
042: * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
043: * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
044: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
045: * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
046: * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
047: * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
048: * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
049: * SUCH DAMAGE.
050: *
051: */
052:
053: package com.rimfaxe.webserver.servletapi;
054:
055: import java.io.*;
056: import java.net.*;
057: import java.util.*;
058: import javax.servlet.*;
059: import javax.servlet.http.*;
060:
061: import com.rimfaxe.webserver.*;
062: import com.rimfaxe.webserver.servletapi.session.HttpSetCookie;
063: import com.rimfaxe.webserver.seda.SedaHttpResponse;
064:
065: import seda.sandStorm.lib.http.httpRequest;
066: import seda.sandStorm.core.BufferElement;
067:
068: //import com.rimfaxe.server.www.mime.*;
069: //import com.rimfaxe.server.www.http.*;
070: //import com.rimfaxe.server.html.HtmlGenerator;
071:
072: public class RimfaxeHttpServletResponse implements HttpServletResponse {
073:
074: public final static String CHARSET_PARAMETER = "charset";
075: public final static int DEFAULT_BUFFER_SIZE = 8 * 1024; // 8KB buffer
076: public final static int MIN_BUFFER_SIZE = 4 * 1024; // 4KB buffer
077: private final static int STATE_INITIAL = 0;
078: private final static int STATE_HEADERS_DONE = 1;
079: private final static int STATE_ALL_DONE = 2;
080: private final static int STREAM_STATE_INITIAL = 0;
081: private final static int STREAM_WRITER_USED = 1;
082: private final static int OUTPUT_STREAM_USED = 2;
083:
084: private int stream_state = STREAM_STATE_INITIAL;
085:
086: private RimfaxeServletOutputStream output = null;
087: private PrintWriter writer = null;
088:
089: //private MimeTypeFormatException setContentTypeException = null;
090:
091: // servlet has set a fixed content length or not,
092: // and cut (see flushStream) any data which are too much here...
093: private final static int CALC_CONTENT_LENGTH = -1;
094: private int fixedContentLength = CALC_CONTENT_LENGTH;
095:
096: // Our Locale
097: protected Locale locale;
098:
099: protected RimfaxeHttpServletRequest jrequest = null;
100:
101: protected int buffer_size;
102:
103: protected void setServletRequest(RimfaxeHttpServletRequest jrequest) {
104: this .jrequest = jrequest;
105: }
106:
107: public static final String INCLUDED = "com.rimfaxe.webserver.servlet.included";
108: public static final String STREAM = "com.rimfaxe.webserver.servlet.stream";
109: public static final String MONITOR = "com.rimfaxe.webserver.servlet.monitor";
110:
111: int state = STATE_INITIAL;
112:
113: httpRequest request = null;
114:
115: String content_type = null; //"text/html";
116:
117: Hashtable headers = new Hashtable();
118:
119: int status = 200;
120:
121: WebContext webcontext;
122:
123: Vector setCookies = new Vector();
124:
125: /**
126: * Sets the content length for this response.
127: * @param len - the content length
128: */
129: public void setContentLength(int i) {
130: fixedContentLength = i;
131: setHeader("Content-Length", "" + i);
132:
133: }
134:
135: public void setContentType(String spec) {
136: content_type = spec;
137: setHeader("Content-Type", spec);
138: }
139:
140: protected boolean isStreamObtained() {
141: return (stream_state != STREAM_STATE_INITIAL);
142: }
143:
144: public synchronized ServletOutputStream getOutputStream()
145: throws IOException {
146:
147: //System.out.println("RimfaxeHttpServletResponse -> Get Output Stream");
148: if (stream_state == STREAM_WRITER_USED) {
149: System.out
150: .println("RimfaxeHttpServletResponse -> Writer used");
151: throw new IllegalStateException("Writer used");
152: }
153: stream_state = OUTPUT_STREAM_USED;
154: return getRimfaxeOutputStream(false);
155: }
156:
157: protected ServletOutputStream getRimfaxeOutputStream(
158: boolean writerUsed) throws IOException {
159: if (output != null)
160: return output;
161:
162: output = new RimfaxeServletOutputStream(this , writerUsed);
163: return output;
164: }
165:
166: /**
167: * Sets the status code and message for this response. If the field had
168: * already been set, the new value overwrites the previous one. The message
169: * is sent as the body of an HTML page, which is returned to the user to
170: * describe the problem. The page is sent with a default HTML header; the
171: * message is enclosed in simple body tags (<body></body>).
172: * @param i - the status code
173: * @param reason - the status message
174: * @deprecated since jsdk2.1
175: */
176: public void setStatus(int i, String reason) {
177: status = i;
178: }
179:
180: /**
181: * Sets the status code for this response. This method is used to set the
182: * return status code when there is no error (for example, for the status
183: * codes SC_OK or SC_MOVED_TEMPORARILY). If there is an error, the
184: * sendError method should be used instead.
185: * @param i - the status code
186: */
187: public void setStatus(int i) {
188: setStatus(i, "no reason");
189: }
190:
191: /**
192: * Adds a field to the response header with the given name and value. If
193: * the field had already been set, the new value overwrites the previous
194: * one. The containsHeader method can be used to test for the presence of a
195: * header before setting its value.
196: * @param name - the name of the header field
197: * @param value - the header field's value
198: */
199: public void setHeader(String name, String value) {
200: //System.out.println("Response, set header "+name+"="+value);
201: if (headers.get(name) != null) {
202: headers.remove(name);
203: headers.put(name, value);
204: } else {
205: headers.put(name, value);
206: }
207:
208: }
209:
210: /**
211: * Adds a field to the response header with the given name and integer
212: * value. If the field had already been set, the new value overwrites the
213: * previous one. The containsHeader method can be used to test for the
214: * presence of a header before setting its value.
215: * @param name - the name of the header field
216: * @param value - the header field's integer value
217: */
218: public void setIntHeader(String name, int value) {
219: setHeader(name, String.valueOf(value));
220: }
221:
222: /**
223: * Adds a field to the response header with the given name and date-valued
224: * field. The date is specified in terms of milliseconds since the epoch.
225: * If the date field had already been set, the new value overwrites the
226: * previous one. The containsHeader method can be used to test for the
227: * presence of a header before setting its value.
228: * @param name - the name of the header field
229: * @param value - the header field's date value
230: */
231: public void setDateHeader(String name, long date) {
232: com.rimfaxe.util.Calendar cal = new com.rimfaxe.util.Calendar();
233: cal.setTimeInMillis(date);
234: setHeader(name, cal.getHttpDate());
235: }
236:
237: public void unsetHeader(String name) {
238: headers.remove(name);
239: }
240:
241: /**
242: * Sends an error response to the client using the specified status code
243: * and descriptive message. If setStatus has previously been called, it is
244: * reset to the error status code. The message is sent as the body of an
245: * HTML page, which is returned to the user to describe the problem. The
246: * page is sent with a default HTML header; the message is enclosed in
247: * simple body tags (<body></body>).
248: * @param sc - the status code
249: * @param msg - the detail message
250: * @exception IOException If an I/O error has occurred.
251: */
252: public void sendError(int i, String msg) throws IOException {
253: //System.out.println("Send error "+i);
254:
255: status = i;
256:
257: if (output == null)
258: this .getRimfaxeOutputStream(false);
259:
260: com.rimfaxe.webserver.servletapi.standard.ErrorMessage em = new com.rimfaxe.webserver.servletapi.standard.ErrorMessage();
261:
262: String error_found = "Unknown error";
263: String error_msg = msg;
264: String uri = request.getURL();
265:
266: if (i == 404) {
267: error_found = "<b>Error 404</b> - file not found.";
268: error_msg = "<b><i>"
269: + uri
270: + "</i></b> could not be resolved in web context <b><i> "
271: + this .webcontext.getUrlpath() + "</i></b>";
272: }
273:
274: output.print(em.getHTML(uri, error_msg, error_found));
275:
276: }
277:
278: /**
279: * Sends an error response to the client using the specified status
280: * code and a default message.
281: * @param sc - the status code
282: * @exception IOException If an I/O error has occurred.
283: */
284: public void sendError(int i) throws IOException {
285:
286: sendError(i, "error");
287: }
288:
289: /**
290: * Sends a temporary redirect response to the client using the specified
291: * redirect location URL. The URL must be absolute (for example,
292: * https://hostname/path/file.html). Relative URLs are not permitted here.
293: * @param url - the redirect location URL
294: * @exception IOException If an I/O error has occurred.
295: */
296: public void sendRedirect(String url) throws IOException {
297: //System.out.println("####### Send redirect to "+url);
298: status = 301;
299:
300: if (output == null)
301: this .getRimfaxeOutputStream(false);
302:
303: output.print("Redirect page");
304:
305: String location = url;
306:
307: addHeader("Location", location);
308: }
309:
310: /**
311: * Checks whether the response message header has a field with the
312: * specified name.
313: * @param name - the header field name
314: * @return true if the response message header has a field with the
315: * specified name; false otherwise
316: */
317: public boolean containsHeader(String header) {
318: if (headers.get(header) != null)
319: return true;
320: else
321: return false;
322: }
323:
324: /**
325: * Adds the specified cookie to the response. It can be called multiple
326: * times to set more than one cookie.
327: * @param cookie - the Cookie to return to the client
328: */
329: public void addCookie(Cookie cookie) {
330: this .setCookies.addElement(convertCookie(cookie));
331: }
332:
333: private HttpSetCookie convertCookie(Cookie cookie) {
334: HttpSetCookie scookie = new HttpSetCookie(true, cookie
335: .getName(), cookie.getValue());
336: scookie.setComment(cookie.getComment());
337: scookie.setDomain(cookie.getDomain());
338: scookie.setMaxAge(cookie.getMaxAge());
339: scookie.setPath(cookie.getPath());
340: scookie.setSecurity(cookie.getSecure());
341: scookie.setVersion(cookie.getVersion());
342: //System.out.println("Set-Cookie = "+scookie.toString());
343: return scookie;
344: }
345:
346: /**
347: * Encodes the specified URL for use in the sendRedirect method or, if
348: * encoding is not needed, returns the URL unchanged. The implementation
349: * of this method should include the logic to determine whether the
350: * session ID needs to be encoded in the URL.
351: * Because the rules for making this determination differ from those used
352: * to decide whether to encode a normal link, this method is seperate from
353: * the encodeUrl method.
354: * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
355: * be run through this method. Otherwise, URL rewriting canont be used
356: * with browsers which do not support cookies.
357: * @param url - the url to be encoded.
358: * @return the encoded URL if encoding is needed; the unchanged URL
359: * otherwise.
360: * @deprecated since jsdk2.1
361: */
362: public String encodeRedirectUrl(String url) {
363: try {
364: URL redirect = new URL(url);
365: URL requested = new URL(jrequest.getRequestURI());
366: if (redirect.getHost().equals(requested.getHost())
367: && redirect.getPort() == requested.getPort())
368: return encodeUrl(url);
369: } catch (MalformedURLException ex) {
370: //error so return url.
371: return url;
372: }
373: return url;
374: }
375:
376: /**
377: * Encodes the specified URL for use in the sendRedirect method or, if
378: * encoding is not needed, returns the URL unchanged. The implementation
379: * of this method should include the logic to determine whether the
380: * session ID needs to be encoded in the URL.
381: * Because the rules for making this determination differ from those used
382: * to decide whether to encode a normal link, this method is seperate from
383: * the encodeUrl method.
384: * <p>All URLs sent to the HttpServletResponse.sendRedirect method should
385: * be run through this method. Otherwise, URL rewriting canont be used
386: * with browsers which do not support cookies.
387: * @param url - the url to be encoded.
388: * @return the encoded URL if encoding is needed; the unchanged URL
389: * otherwise.
390: */
391: public String encodeRedirectURL(String url) {
392: return encodeRedirectUrl(url);
393: }
394:
395: /**
396: * Encodes the specified URL by including the session ID in it, or, if
397: * encoding is not needed, returns the URL unchanged. The implementation of
398: * this method should include the logic to determine whether the session ID
399: * needs to be encoded in the URL. For example, if the browser supports
400: * cookies, or session tracking is turned off, URL encoding is unnecessary.
401: * <p>All URLs emitted by a Servlet should be run through this method.
402: * Otherwise, URL rewriting cannot be used with browsers which do not
403: * support cookies.
404: * @param url - the url to be encoded.
405: * @return the encoded URL if encoding is needed; the unchanged URL
406: * otherwise.
407: * @deprecated since jsdk2.1
408: */
409: public String encodeUrl(String url) {
410: if (!jrequest.isRequestedSessionIdFromCookie()) {
411: url = url + ((url.indexOf("?") != -1) ? "&" : "?")
412: + jrequest.getCookieName() + "="
413: + jrequest.getSession(true).getId();
414: }
415: return url;
416: }
417:
418: /**
419: * Encodes the specified URL by including the session ID in it, or, if
420: * encoding is not needed, returns the URL unchanged. The implementation of
421: * this method should include the logic to determine whether the session ID
422: * needs to be encoded in the URL. For example, if the browser supports
423: * cookies, or session tracking is turned off, URL encoding is unnecessary.
424: * <p>All URLs emitted by a Servlet should be run through this method.
425: * Otherwise, URL rewriting cannot be used with browsers which do not
426: * support cookies.
427: * @param url - the url to be encoded.
428: * @return the encoded URL if encoding is needed; the unchanged URL
429: * otherwise.
430: */
431: public String encodeURL(String url) {
432: return encodeUrl(url);
433: }
434:
435: /**
436: * Return the Charset parameter of content type
437: * @return A String instance
438: */
439: public String getCharacterEncoding() {
440: return "iso-8859-1";
441: }
442:
443: /**
444: * Returns a print writer for writing formatted text responses.
445: * The MIME type of the response will be modified, if necessary, to
446: * reflect the character encoding used, through the charset=... property.
447: * This means that the content type must be set before calling this
448: * method.
449: * @exception UnsupportedEncodingException if no such encoding can be
450: * provided
451: * @exception IllegalStateException if getOutputStream has been called
452: * on this same request.
453: * @exception IOException on other errors.
454: */
455: public synchronized java.io.PrintWriter getWriter()
456: throws java.io.IOException {
457: //System.out.println("RimfaxeHttpServletResponse -> Get Writer");
458:
459: if (stream_state == OUTPUT_STREAM_USED)
460: throw new IllegalStateException("Output stream used");
461: stream_state = STREAM_WRITER_USED;
462:
463: if (writer == null) {
464: writer = new PrintWriter(new OutputStreamWriter(
465: getRimfaxeOutputStream(true),
466: getCharacterEncoding()));
467: }
468:
469: //System.out.println("RimfaxeHttpServletResponse -> Get Writer - done");
470: return writer;
471: }
472:
473: /**
474: * Flush the output stream.
475: * @param close Close the stream if true.
476: * @exception IOException if an IO error occurs.
477: */
478: protected synchronized void flushStream(boolean close)
479: throws IOException {
480: /*
481: if (state == STATE_ALL_DONE)
482: {
483: return;
484: }
485: int writeLength;
486:
487: if (stream_state == OUTPUT_STREAM_USED) {
488: output.flush();
489: } else if (stream_state == STREAM_WRITER_USED) {
490: writer.flush();
491: if (close) {
492: output.realFlush();
493: } else {
494: output.flush();
495: }
496: } else {
497: // force flush even if no stream are openned
498: getWriter();
499: writer.flush();
500: output.realFlush();
501: }
502:
503: if (request.hasState(INCLUDED)) {
504: if (out == null)
505: return;
506:
507: if( fixedContentLength != CALC_CONTENT_LENGTH ) {
508: writeLength = (out.size() < fixedContentLength)
509: ? (out.size())
510: : (fixedContentLength);
511: } else {
512: writeLength = out.size();
513: }
514: reply.setContentLength(writeLength);
515:
516: OutputStream rout = reply.getOutputStream(false);
517: byte content[] = out.toByteArray();
518: if (close)
519: out.close();
520: else
521: out.reset();
522: rout.write(content);
523: rout.flush();
524: } else {
525: if ((pout != null) && close) {
526: pout.flush();
527: pout.close();
528: }
529: }
530: */
531: }
532:
533: /**
534: * Sets the preferred buffer size for the body of the response.
535: * The servlet container will use a buffer at least as large as
536: * the size requested. The actual buffer size used can be found
537: * using <code>getBufferSize</code>.
538: *
539: * <p>A larger buffer allows more content to be written before anything is
540: * actually sent, thus providing the servlet with more time to set
541: * appropriate status codes and headers. A smaller buffer decreases
542: * server memory load and allows the client to start receiving data more
543: * quickly.
544: *
545: * <p>This method must be called before any response body content is
546: * written; if content has been written, this method throws an
547: * <code>IllegalStateException</code>.
548: * @param size the preferred buffer size
549: * @exception IllegalStateException if this method is called after
550: * content has been written
551: * @see #getBufferSize
552: * @see #flushBuffer
553: * @see #isCommitted
554: * @see #reset
555: */
556: public void setBufferSize(int size) {
557: if (stream_state != STREAM_STATE_INITIAL) {
558: throw new IllegalStateException(
559: "Stream already initialized");
560: }
561: buffer_size = size < MIN_BUFFER_SIZE ? MIN_BUFFER_SIZE : size;
562: }
563:
564: /**
565: * Returns the actual buffer size used for the response. If no buffering
566: * is used, this method returns 0.
567: * @return the actual buffer size used
568: * @see #setBufferSize
569: * @see #flushBuffer
570: * @see #isCommitted
571: * @see #reset
572: */
573: public int getBufferSize() {
574: return buffer_size;
575: }
576:
577: /**
578: * Forces any content in the buffer to be written to the client. A call
579: * to this method automatically commits the response, meaning the status
580: * code and headers will be written.
581: * @see #setBufferSize
582: * @see #getBufferSize
583: * @see #isCommitted
584: * @see #reset
585: */
586: public void flushBuffer() throws IOException {
587: //System.out.println("FLUSH BUFFER");
588: if (output != null) {
589: if (!output.isClosed()) {
590: if (stream_state == STREAM_WRITER_USED) {
591: writer.flush();
592: } else if (stream_state == OUTPUT_STREAM_USED) {
593: output.flush();
594: }
595: }
596:
597: }
598: }
599:
600: /**
601: * Returns a boolean indicating if the response has been
602: * committed. A commited response has already had its status
603: * code and headers written.
604: * @return a boolean indicating if the response has been
605: * committed
606: * @see #setBufferSize
607: * @see #getBufferSize
608: * @see #flushBuffer
609: * @see #reset
610: */
611: public boolean isCommitted() {
612: if (output != null) {
613: return output.isCommitted();
614: } else {
615: return false;
616: }
617: }
618:
619: /**
620: * Clears any data that exists in the buffer as well as the status code and
621: * headers. If the response has been committed, this method throws an
622: * <code>IllegalStateException</code>.
623: * @exception IllegalStateException if the response has already been
624: * committed
625: * @see #setBufferSize
626: * @see #getBufferSize
627: * @see #flushBuffer
628: * @see #isCommitted
629: */
630: public void reset() {
631: if (output != null) {
632: if (stream_state == STREAM_WRITER_USED) {
633: writer.flush();
634: }
635: output.reset();
636: }
637: }
638:
639: /**
640: * Sets the locale of the response, setting the headers (including the
641: * Content-Type's charset) as appropriate. This method should be called
642: * before a call to {@link #getWriter}. By default, the response locale
643: * is the default locale for the server.
644: * @param loc the locale of the response
645: * @see #getLocale
646: */
647: public void setLocale(Locale locale) {
648: System.out.println("Set Locale");
649: setHeader("Content-Type", "text/html");
650: if (locale == null) {
651: return;
652: }
653: this .locale = locale;
654: }
655:
656: /**
657: * Returns the locale assigned to the response.
658: * @see #setLocale
659: */
660: public Locale getLocale() {
661: return locale;
662: }
663:
664: public void addHeader(String name, String value) {
665:
666: setHeader(name, value);
667: }
668:
669: public void addDateHeader(String name, long date) {
670: com.rimfaxe.util.Calendar cal = new com.rimfaxe.util.Calendar();
671: cal.setTimeInMillis(date);
672: addHeader(name, cal.getHttpDate());
673: }
674:
675: public void addIntHeader(String name, int value) {
676: addHeader(name, String.valueOf(value));
677: }
678:
679: public void resetBuffer() {
680: }
681:
682: public SedaHttpResponse getSedaResponse()
683: {
684:
685:
686: if (setCookies.size()>0)
687: {
688: StringBuffer buf = new StringBuffer();
689: Enumeration enum = setCookies.elements();
690: while (enum.hasMoreElements())
691: {
692: HttpSetCookie hsc = (HttpSetCookie) enum.nextElement();
693:
694: if (enum.hasMoreElements()) buf.append( hsc.toString() +", ");
695: else buf.append( hsc.toString() );
696: }
697: headers.put("Set-Cookie", ""+buf);
698: }
699:
700: if (headers.get("Cache-Control")==null)
701: {
702: String maxage = ""+request.getSpecial("MAX-AGE");
703: if (!maxage.equalsIgnoreCase("null"))
704: {
705: if (!maxage.equalsIgnoreCase("-1"))
706: {
707: headers.put("Cache-Control", "max-age="+maxage);
708: }
709: }
710: }
711:
712:
713:
714: SedaHttpResponse seda = null;
715: if (output==null)
716: {
717: seda = new SedaHttpResponse(status,this .content_type, headers);
718: }
719: else
720: {
721:
722: seda = new SedaHttpResponse(status,this .content_type, new BufferElement(output.getBuffer()), headers);
723: }
724:
725: return seda;
726: }
727:
728: RimfaxeHttpServletResponse(httpRequest request,
729: WebContext webcontext) {
730: this.request = request;
731: this.webcontext = webcontext;
732: }
733:
734: }
|