001: /*
002:
003: This software is OSI Certified Open Source Software.
004: OSI Certified is a certification mark of the Open Source Initiative.
005:
006: The license (Mozilla version 1.0) can be read at the MMBase site.
007: See http://www.MMBase.org/license
008:
009: */
010: package org.mmbase.util;
011:
012: import java.io.*;
013:
014: import javax.servlet.*;
015: import javax.servlet.http.*;
016:
017: import java.util.Locale;
018: import java.util.regex.*;
019:
020: import org.mmbase.util.logging.Logger;
021: import org.mmbase.util.logging.Logging;
022:
023: /**
024: * Wrapper around the response. It collects all data that is sent to it, and makes it available
025: * through a toString() method. It is used by taglib's Include-Tag, but it might find more general
026: * use, outside taglib.
027: *
028: * @author Kees Jongenburger
029: * @author Johannes Verelst
030: * @author Michiel Meeuwissen
031: * @since MMBase-1.7
032: * @version $Id: GenericResponseWrapper.java,v 1.22 2008/02/20 12:01:49 michiel Exp $
033: */
034: public class GenericResponseWrapper extends HttpServletResponseWrapper {
035: private static final Logger log = Logging
036: .getLoggerInstance(GenericResponseWrapper.class);
037:
038: /**
039: * If this pattern matched the first line of an InputStream then it is a XML. The encoding is in
040: * matching group 1 (when using " as quote) or 2 (when using ' as quote)
041: */
042: private static final Pattern XMLHEADER = Pattern
043: .compile(
044: "<\\?xml.*?(?:\\sencoding=(?:\"([^\"]+?)\"|'([^']+?)'))?\\s*\\?>.*",
045: Pattern.DOTALL);
046:
047: private static String UNSET_CHARSET = "iso-8859-1";
048: public static String TEXT_XML_DEFAULT_CHARSET = "US-ASCII";
049:
050: private static String DEFAULT_CONTENTTYPE = "text/html";
051:
052: private static String[] IGNORED_HEADERS = new String[] {
053: "Last-Modified", "ETag" };
054:
055: private PrintWriter writer;
056: private StringWriter string; // wrapped by writer
057:
058: private ServletOutputStream outputStream; // wrapped by outputStream
059: private ByteArrayOutputStream bytes;
060:
061: private String contentType = DEFAULT_CONTENTTYPE;
062: private String characterEncoding = UNSET_CHARSET;
063:
064: private HttpServletResponse wrappedResponse;
065:
066: protected String redirected = null;
067:
068: /**
069: * Public constructor
070: */
071: public GenericResponseWrapper(HttpServletResponse resp) {
072: super (resp);
073: wrappedResponse = resp; // I don't understand why this object is not super.getResponse();
074:
075: }
076:
077: /**
078: * Sets also a value for the characterEncoding which must be supposed.
079: * Normally it would be determined automaticly right, but if for some reason it doesn't you can override it.
080: */
081: public GenericResponseWrapper(HttpServletResponse resp,
082: String encoding) {
083: this (resp);
084: characterEncoding = encoding;
085: wrappedResponse = resp; //
086: }
087:
088: /**
089: * Gets the response object which this wrapper is wrapping. You might need this when giving a
090: * redirect or so.
091: * @since MMBase-1.7.1
092: */
093: public HttpServletResponse getHttpServletResponse() {
094: //return (HttpServletResponse) getResponse(); // should work, I think, but doesn't
095: HttpServletResponse response = wrappedResponse;
096: while (response instanceof HttpServletResponseWrapper) {
097: if (response instanceof GenericResponseWrapper) { // if this happens in an 'mm:included' page.
098: response = ((GenericResponseWrapper) response).wrappedResponse;
099: } else {
100: response = (HttpServletResponse) ((HttpServletResponseWrapper) response)
101: .getResponse();
102: }
103: }
104: return response;
105: }
106:
107: private boolean mayAddHeader(String header) {
108: for (String element : IGNORED_HEADERS) {
109: if (element.equalsIgnoreCase(header)) {
110: return false;
111: }
112: }
113: return true;
114: }
115:
116: public void sendRedirect(String location) throws IOException {
117: redirected = location;
118: getHttpServletResponse().sendRedirect(location);
119: }
120:
121: /**
122: * @since MMBase-1.8.5
123: */
124: public String getRedirected() {
125: return redirected;
126: }
127:
128: public void setStatus(int s) {
129: getHttpServletResponse().setStatus(s);
130: }
131:
132: public void addCookie(Cookie c) {
133: getHttpServletResponse().addCookie(c);
134: }
135:
136: public void setHeader(String header, String value) {
137: if (mayAddHeader(header)) {
138: getHttpServletResponse().setHeader(header, value);
139: }
140: }
141:
142: /**
143: * @see javax.servlet.http.HttpServletResponse#addDateHeader(java.lang.String, long)
144: */
145: public void addDateHeader(String arg0, long arg1) {
146: if (mayAddHeader(arg0)) {
147: getHttpServletResponse().addDateHeader(arg0, arg1);
148: }
149: }
150:
151: /**
152: * @see javax.servlet.http.HttpServletResponse#addHeader(java.lang.String, java.lang.String)
153: */
154: public void addHeader(String arg0, String arg1) {
155: if (mayAddHeader(arg0)) {
156: getHttpServletResponse().addHeader(arg0, arg1);
157: }
158: }
159:
160: /**
161: * @see javax.servlet.http.HttpServletResponse#addIntHeader(java.lang.String, int)
162: */
163: public void addIntHeader(String arg0, int arg1) {
164: if (mayAddHeader(arg0)) {
165: getHttpServletResponse().addIntHeader(arg0, arg1);
166: }
167: }
168:
169: /**
170: * @see javax.servlet.http.HttpServletResponse#containsHeader(java.lang.String)
171: */
172: public boolean containsHeader(String arg0) {
173: return getHttpServletResponse().containsHeader(arg0);
174: }
175:
176: /**
177: * @see javax.servlet.http.HttpServletResponse#encodeRedirectURL(java.lang.String)
178: */
179: public String encodeRedirectURL(String arg0) {
180: return getHttpServletResponse().encodeRedirectURL(arg0);
181: }
182:
183: /**
184: * @see javax.servlet.http.HttpServletResponse#encodeURL(java.lang.String)
185: */
186: public String encodeURL(String arg0) {
187: return getHttpServletResponse().encodeURL(arg0);
188: }
189:
190: /**
191: * @see javax.servlet.ServletResponse#getLocale()
192: */
193: public Locale getLocale() {
194: return getHttpServletResponse().getLocale();
195: }
196:
197: /**
198: * @see javax.servlet.http.HttpServletResponse#sendError(int, java.lang.String)
199: */
200: public void sendError(int arg0, String arg1) throws IOException {
201: getHttpServletResponse().sendError(arg0, arg1);
202: }
203:
204: /**
205: * @see javax.servlet.http.HttpServletResponse#sendError(int)
206: */
207: public void sendError(int arg0) throws IOException {
208: getHttpServletResponse().sendError(arg0);
209: }
210:
211: /**
212: * @see javax.servlet.http.HttpServletResponse#setDateHeader(java.lang.String, long)
213: */
214: public void setDateHeader(String arg0, long arg1) {
215: if (mayAddHeader(arg0)) {
216: getHttpServletResponse().setDateHeader(arg0, arg1);
217: }
218: }
219:
220: /**
221: * @see javax.servlet.http.HttpServletResponse#setIntHeader(java.lang.String, int)
222: */
223: public void setIntHeader(String arg0, int arg1) {
224: if (mayAddHeader(arg0)) {
225: getHttpServletResponse().setIntHeader(arg0, arg1);
226: }
227: }
228:
229: /**
230: * @see javax.servlet.ServletResponse#setLocale(java.util.Locale)
231: */
232: public void setLocale(Locale arg0) {
233: getHttpServletResponse().setLocale(arg0);
234: }
235:
236: /**
237: * Return the OutputStream. This is a 'MyServletOutputStream'.
238: */
239: public ServletOutputStream getOutputStream() throws IOException {
240: if (outputStream != null)
241: return outputStream;
242:
243: if (writer != null) {
244: outputStream = new MyServletOutputStream(
245: new WriterOutputStream(writer, characterEncoding));
246: return outputStream;
247: //throw new RuntimeException("Should use getOutputStream _or_ getWriter");
248: }
249:
250: bytes = new ByteArrayOutputStream();
251: outputStream = new MyServletOutputStream(bytes);
252:
253: return outputStream;
254: }
255:
256: /**
257: * Return the PrintWriter
258: */
259: public PrintWriter getWriter() throws IOException {
260: if (writer != null)
261: return writer;
262:
263: if (outputStream != null) {
264: writer = new PrintWriter(new BufferedWriter(
265: new OutputStreamWriter(outputStream,
266: characterEncoding)));
267: return writer;
268: //throw new RuntimeException("Should use getOutputStream _or_ getWriter");
269: }
270:
271: string = new StringWriter();
272: writer = new PrintWriter(string);
273:
274: return writer;
275: }
276:
277: /**
278: * Sets the content type of the response being sent to the
279: * client. The content type may include the type of character
280: * encoding used, for example, text/html; charset=ISO-8859-4. If
281: * obtaining a PrintWriter, this method should be called first.
282: */
283: public void setContentType(String ct) {
284: if (ct == null) {
285: contentType = DEFAULT_CONTENTTYPE;
286: } else {
287: contentType = ct;
288: characterEncoding = getEncoding(ct); // gets char-encoding from content type
289: if (characterEncoding == null) {
290: characterEncoding = getDefaultEncoding(contentType);
291: }
292: }
293:
294: if (log.isDebugEnabled()) {
295: log.debug("set contenttype of include page to: '"
296: + contentType + "' (and character encoding to '"
297: + characterEncoding + "')");
298: }
299: }
300:
301: /**
302: * Returns the name of the charset used for the MIME body sent in this response.
303: * If no charset has been assigned, it is implicitly set to ISO-8859-1 (Latin-1).
304: * See <a href="http://www.ietf.org/rfc/rfc2047.txt">RFC 2047</a> for more information about character encoding and MIME.
305: * returns the encoding
306: */
307: public String getCharacterEncoding() {
308: log.debug(characterEncoding);
309: /*
310: if (characterEncoding == UNSET_CHARSET && outputStream != null) {
311: determinXMLEncoding();
312: }
313: */
314: return characterEncoding;
315: }
316:
317: protected byte[] determinXMLEncoding() {
318: byte[] allBytes = bytes.toByteArray();
319: characterEncoding = getXMLEncoding(allBytes);
320: if (characterEncoding == null)
321: characterEncoding = "UTF-8"; // missing <?xml header, but we _know_ it is XML.
322: return allBytes;
323: }
324:
325: /**
326: * Return all data that has been written to the PrintWriter.
327: */
328: public String toString() {
329: if (string != null) {
330: return string.toString();
331: } else if (outputStream != null) {
332: try {
333: byte[] allBytes;
334: if (TEXT_XML_DEFAULT_CHARSET.equals(characterEncoding)) {
335: // see comments in getDefaultEncoding
336: allBytes = determinXMLEncoding();
337: } else {
338: allBytes = bytes.toByteArray();
339: }
340: return new String(allBytes, getCharacterEncoding());
341: } catch (Exception e) {
342: return bytes.toString();
343: }
344: } else {
345: return "";
346: }
347: }
348:
349: /**
350: * Takes a String, which is considered to be (the first) part of an XML, and returns the
351: * encoding (the specified one, or the XML default)
352: * @return The XML Encoding, or <code>null</code> if the String was not recognized as XML (no <?xml> header found)
353: * @since MMBase-1.7.1
354: * @see #getXMLEncoding(byte[])
355: */
356: public static final String getXMLEncoding(String xmlString) {
357: Matcher m = XMLHEADER.matcher(xmlString);
358: if (!m.matches()) {
359: return null; // No <? xml header found, this file is probably not XML.
360: } else {
361: String encoding = m.group(1);
362: if (encoding == null)
363: encoding = m.group(2);
364: if (encoding == null)
365: encoding = "UTF-8"; // default encoding for XML.
366: return encoding;
367: }
368: }
369:
370: /**
371: * Takes a ByteArrayInputStream, which is considered to be (the first) part of an XML, and returns the encoding.
372: * @return The XML Encoding, or <code>null</code> if the String was not recognized as XML (not <?xml> header found)
373: * @since MMBase-1.7.1
374: * @see #getXMLEncoding(String)
375: */
376: public static String getXMLEncoding(byte[] allBytes) {
377: byte[] firstBytes = allBytes;
378: if (allBytes.length > 100) {
379: firstBytes = new byte[100];
380: System.arraycopy(allBytes, 0, firstBytes, 0, 100);
381: }
382: try {
383: return getXMLEncoding(new String(firstBytes, "US-ASCII"));
384: } catch (java.io.UnsupportedEncodingException uee) {
385: // cannot happen, US-ASCII is known
386: }
387: return "UTF-8"; // cannot come here.
388: }
389:
390: /**
391: * Takes the value of a Content-Type header, and tries to find the encoding from it.
392: * @since MMBase-1.7.1
393: * @return The found charset if found, otherwise 'null'
394: */
395: public static String getEncoding(String contentType) {
396: String contentTypeLowerCase = contentType.toLowerCase();
397: int cs = contentTypeLowerCase.indexOf("charset=");
398: if (cs > 0) {
399: return contentType.substring(cs + 8);
400: } else {
401: return null;
402: }
403: }
404:
405: /**
406: * Supposes that no explicit charset is mentioned in a contentType, and returns a default. (UTF-8 or US-ASCII
407: * for XML types and ISO-8859-1 otherwise).
408: * @since MMBase-1.7.1
409: * @return A charset.
410: */
411: public static String getDefaultEncoding(String contentType) {
412: if (contentType.equals("text/xml")) {
413: return TEXT_XML_DEFAULT_CHARSET; // = us-ascii, See
414: // http://www.rfc-editor.org/rfc/rfc3023.txt. We will
415: // ignore it, because if not not ascii, it will never
416: // work, and all known charset are superset of us-ascii
417: // (so the response _is_ correct it will work).
418: } else if (contentType.equals("application/xml")
419: || contentType.equals("application/xhtml+xml")) {
420: return "UTF-8";
421: } else {
422: return "iso-8859-1";
423: }
424:
425: }
426: }
427:
428: /**
429: * Implements ServletOutputStream.
430: */
431: class MyServletOutputStream extends ServletOutputStream {
432:
433: private OutputStream stream;
434:
435: public MyServletOutputStream(OutputStream output) {
436: stream = output;
437: }
438:
439: public void write(int b) throws IOException {
440: stream.write(b);
441: }
442:
443: public void write(byte[] b) throws IOException {
444: stream.write(b);
445: }
446:
447: public void write(byte[] b, int off, int len) throws IOException {
448: stream.write(b, off, len);
449: }
450: }
|