001: /******************************************************************************
002: * ResponderCache.java
003: * ****************************************************************************/package org.openlaszlo.servlets.responders;
004:
005: import java.io.*;
006: import java.net.UnknownHostException;
007: import java.net.MalformedURLException;
008: import java.net.URL;
009: import java.util.Properties;
010: import java.util.HashMap;
011: import java.util.Iterator;
012: import javax.servlet.ServletConfig;
013: import javax.servlet.ServletException;
014: import javax.servlet.http.HttpServletRequest;
015: import javax.servlet.http.HttpServletResponse;
016: import javax.servlet.ServletOutputStream;
017: import org.openlaszlo.cache.RequestCache;
018: import org.openlaszlo.data.*;
019: import org.openlaszlo.media.MimeType;
020: import org.openlaszlo.server.LPS;
021: import org.openlaszlo.utils.LZHttpUtils;
022: import org.openlaszlo.utils.ChainedException;
023: import org.openlaszlo.xml.internal.XMLUtils;
024: import org.apache.commons.httpclient.URI;
025: import org.apache.commons.httpclient.URIException;
026: import org.apache.log4j.Logger;
027:
028: public abstract class ResponderCache extends Responder {
029: private static boolean mIsInitialized = false;
030:
031: private static HashMap mDataSourceMap = new HashMap();
032: private static DataSource mHTTPDataSource = null;
033:
034: private static Logger mLogger = Logger
035: .getLogger(ResponderCache.class);
036:
037: protected RequestCache mCache = null;
038:
039: /**
040: * Keeps track of url stats.
041: */
042: public class URLStat {
043: String mName;
044:
045: final static public int ERRTYPE_NONE = -1;
046: final static public int ERRTYPE_CONVERSION = 0;
047: final static public int ERRTYPE_DATA_SOURCE = 1;
048: final static public int ERRTYPE_UNKNOWN_HOST = 2;
049: final static public int ERRTYPE_MALFORMED_URL = 3;
050: final static public int ERRTYPE_IO = 4;
051: final static public int ERRTYPE_ILLEGAL_ARGUMENT = 5;
052: final static public int ERRTYPE_TIMEOUT = 6;
053: final static public int ERRTYPE_FORBIDDEN = 7;
054: final static public int ERRTYPE_OTHER = 8;
055: final static public int NUM_ERRTYPES = 9;
056:
057: HashMap mURLs = new HashMap();
058: HashMap mErrorURLs = new HashMap();
059:
060: int mSuccessCount;
061: /**
062: * 0: mConversionException, 1: mDataSourceException, 2: mUnknownHostException,
063: * 3: mMalformedURLException, 4: mIOException, 5: mIllegalArgumentException,
064: * 6: mInterrupedIOException, 7 mException
065: */
066: int[] mErrorCount = new int[NUM_ERRTYPES];
067:
068: boolean mDoURLCollection = false;
069:
070: /**
071: * Create an URLStat with a name.
072: */
073: public URLStat(String name) {
074: mName = name;
075: clear();
076: }
077:
078: /**
079: * @param doCollection whether the object should collect unique url
080: * info.
081: */
082: void doURLCollection(boolean doCollection) {
083: if (mDoURLCollection != doCollection) {
084: mDoURLCollection = doCollection;
085: clear();
086: }
087: }
088:
089: /**
090: * Increment stat on url with successful status.
091: * @param url successful url.
092: */
093: void success(String url) {
094: int x = url.indexOf('?');
095: if (x != -1) {
096: url = url.substring(0, x);
097: }
098: synchronized (mURLs) {
099: if (mDoURLCollection) {
100: // Add unique urls
101: int[] s = (int[]) mURLs.get(url);
102: if (s == null) {
103: s = new int[1];
104: mURLStat.mURLs.put(url, s);
105: }
106: ++s[0];
107: }
108: ++mSuccessCount;
109: }
110: }
111:
112: /**
113: * Increment stat on url with error status.
114: *
115: * @param errType see ERRTYPEs.
116: * @param url error url.
117: */
118: void error(int errType, String url) {
119: int x = url.indexOf('?');
120: if (x != -1) {
121: url = url.substring(0, x);
122: }
123: synchronized (mErrorURLs) {
124: if (mDoURLCollection) {
125: int[] e = (int[]) mErrorURLs.get(url);
126: if (e == null) {
127: e = new int[NUM_ERRTYPES];
128: mErrorURLs.put(url, e);
129: }
130: ++e[errType];
131: }
132: ++mErrorCount[errType];
133: }
134:
135: }
136:
137: /**
138: * Clear URL stats.
139: */
140: void clear() {
141: synchronized (mURLs) {
142: mURLs.clear();
143: mSuccessCount = 0;
144: }
145:
146: synchronized (mErrorURLs) {
147: mErrorURLs.clear();
148: for (int i = 0; i < mErrorCount.length; i++)
149: mErrorCount[i] = 0;
150: }
151: }
152:
153: /**
154: * Return url statistics in XML string.
155: * @return xml info.
156: */
157: public String toXML() {
158: StringBuffer buf = new StringBuffer();
159: synchronized (mURLs) {
160: buf.append("<").append(mName).append("-urls ").append(
161: " unique=\"").append(mURLs.size()).append("\"")
162: .append(">\n");
163:
164: buf.append("<success").append(" total-requests=\"")
165: .append(mSuccessCount).append("\"").append(
166: ">\n");
167: if (mDoURLCollection) {
168: Iterator iter = mURLs.keySet().iterator();
169: while (iter.hasNext()) {
170: String k = (String) iter.next();
171: int[] success = (int[]) mURLs.get(k);
172: buf.append("<url").append(" requests=\"")
173: .append(success[0]).append("\"")
174: .append(" href=\"").append(
175: XMLUtils.escapeXml(k)).append(
176: "\" />");
177: }
178: }
179: buf.append("</success>\n");
180: }
181:
182: synchronized (mErrorURLs) {
183: int errTotal = 0;
184: for (int i = 0; i < mErrorCount.length; i++)
185: errTotal += mErrorCount[i];
186: buf.append("<errors").append(" total-errors=\"")
187: .append(errTotal).append("\"").append(
188: " conversion=\"").append(
189: mErrorCount[ERRTYPE_CONVERSION])
190: .append("\"").append(" datasource=\"").append(
191: mErrorCount[ERRTYPE_DATA_SOURCE])
192: .append("\"").append(" unknownhost=\"").append(
193: mErrorCount[ERRTYPE_UNKNOWN_HOST])
194: .append("\"").append(" malformedurl=\"")
195: .append(mErrorCount[ERRTYPE_MALFORMED_URL])
196: .append("\"").append(" ioexception=\"").append(
197: mErrorCount[ERRTYPE_IO]).append("\"")
198: .append(" illegalargument=\"").append(
199: mErrorCount[ERRTYPE_ILLEGAL_ARGUMENT])
200: .append("\"").append(" timeout=\"").append(
201: mErrorCount[ERRTYPE_TIMEOUT]).append(
202: "\"").append(" forbidden=\"").append(
203: mErrorCount[ERRTYPE_FORBIDDEN]).append(
204: "\"").append(" uncaught-exception=\"")
205: .append(mErrorCount[ERRTYPE_OTHER])
206: .append("\"").append(">\n");
207: if (mDoURLCollection) {
208: Iterator iter = mErrorURLs.keySet().iterator();
209: while (iter.hasNext()) {
210: String k = (String) iter.next();
211: int[] e = (int[]) mErrorURLs.get(k);
212: buf.append("<url").append(" conversion=\"")
213: .append(e[ERRTYPE_CONVERSION]).append(
214: "\"").append(" datasource=\"")
215: .append(e[ERRTYPE_DATA_SOURCE]).append(
216: "\"").append(" unknownhost=\"")
217: .append(e[ERRTYPE_UNKNOWN_HOST])
218: .append("\"")
219: .append(" malformedurl=\"").append(
220: e[ERRTYPE_MALFORMED_URL])
221: .append("\"").append(" ioexception=\"")
222: .append(e[ERRTYPE_IO]).append("\"")
223: .append(" illegalargument=\"").append(
224: e[ERRTYPE_ILLEGAL_ARGUMENT])
225: .append("\"").append(" timeout=\"")
226: .append(e[ERRTYPE_TIMEOUT])
227: .append("\"").append(" forbidden=\"")
228: .append(e[ERRTYPE_FORBIDDEN]).append(
229: "\"").append(
230: " uncaught-exception=\"")
231: .append(e[ERRTYPE_OTHER]).append("\"")
232: .append(" href=\"").append(
233: XMLUtils.escapeXml(k)).append(
234: "\"").append(" />\n");
235: }
236: }
237: buf.append("</errors>\n");
238: }
239:
240: buf.append("</").append(mName).append("-urls>\n");
241:
242: return buf.toString();
243: }
244: }
245:
246: public URLStat mURLStat = null;
247:
248: synchronized public void init(String reqName, ServletConfig config,
249: RequestCache cache, Properties prop)
250: throws ServletException, IOException {
251: super .init(reqName, config, prop);
252:
253: String reqProp = reqName.toLowerCase() + "Request.collectURL";
254: boolean doURLCollection = prop.getProperty(reqProp, "false")
255: .intern() == "true";
256:
257: mURLStat = new URLStat(reqName);
258: mURLStat.doURLCollection(doURLCollection);
259:
260: if (!mIsInitialized) {
261: //------------------------------------------------------------
262: // Well-known data sources
263: //------------------------------------------------------------
264: mHTTPDataSource = new HTTPDataSource();
265:
266: mDataSourceMap.put("http", mHTTPDataSource);
267:
268: // For security reasons, we are now mapping file => http
269: mDataSourceMap.put("file", mHTTPDataSource);
270:
271: /*
272: try {
273: mDataSourceMap.put("file", new FileDataSource());
274: } catch (Throwable e) {
275: mLogger.warn("can't load file datasource", e);
276: }
277: */
278:
279: try {
280: mDataSourceMap.put("java", new JavaDataSource());
281: } catch (Throwable e) {
282: mLogger.warn("can't load java datasource", e);
283: }
284:
285: try {
286: mDataSourceMap.put("soap-json",
287: new org.openlaszlo.data.json.SOAPDataSource());
288: } catch (Throwable e) {
289: mLogger.warn("can't load soap datasource", e);
290: }
291:
292: try {
293: mDataSourceMap.put("soap-swf",
294: new org.openlaszlo.data.swf.SOAPDataSource());
295: } catch (Throwable e) {
296: mLogger.warn("can't load soap datasource", e);
297: }
298:
299: try {
300: mDataSourceMap.put("xmlrpc", new XMLRPCDataSource());
301: } catch (Throwable e) {
302: mLogger.warn("can't load xmlrpc datasource", e);
303: }
304:
305: mIsInitialized = true;
306: }
307:
308: mCache = cache;
309: }
310:
311: protected void respondImpl(HttpServletRequest req,
312: HttpServletResponse res) {
313:
314: String path = req.getServletPath();
315: String url;
316: try {
317: url = DataSource.getURL(req);
318: } catch (java.net.MalformedURLException e) {
319: respondWithErrorSWF(res, "bad url: " + e.getMessage());
320: if (mCollectStat) {
321: mURLStat
322: .error(URLStat.ERRTYPE_MALFORMED_URL, "bad-url");
323: }
324: return;
325: }
326:
327: if (path.endsWith(".lzo")) {
328: path = path.substring(0, path.length() - 1) + "x";
329: }
330:
331: if (req.getMethod().intern() == "POST") {
332: float fpv = getFlashPlayerVersion(req);
333: String ua = req.getHeader(LZHttpUtils.USER_AGENT);
334: mLogger.debug(
335: /* (non-Javadoc)
336: * @i18n.test
337: * @org-mes="POST request, flash player version: " + p[0]
338: */
339: org.openlaszlo.i18n.LaszloMessages.getMessage(
340: ResponderCache.class.getName(), "051018-328",
341: new Object[] { new Float(fpv) }));
342: if (fpv < 6.47
343: && LPS.configuration.optionAllows(
344: "disable-post-keep-alive", ua)) {
345: // Prevent browser keep-alive to get around bug 4048.
346: mLogger.debug(
347: /* (non-Javadoc)
348: * @i18n.test
349: * @org-mes="Disabling keep-alive for " + p[0]
350: */
351: org.openlaszlo.i18n.LaszloMessages.getMessage(
352: ResponderCache.class.getName(), "051018-339",
353: new Object[] { ua }));
354: res.setHeader("Connection", "close");
355: res.setHeader("Keep-Alive", "close");
356: }
357: }
358:
359: if (!LPS.configuration.optionAllows(path,
360: "proxy-security-urls", url)) {
361: String err = "Forbidden url: " + url;
362: respondWithError(res, err, HttpServletResponse.SC_FORBIDDEN);
363: mLogger.error(err);
364: if (mCollectStat) {
365: mURLStat.error(URLStat.ERRTYPE_FORBIDDEN, url);
366: }
367: return;
368: }
369:
370: int errType = URLStat.ERRTYPE_NONE;
371:
372: try {
373:
374: DataSource source = getDataSource(req, res);
375: if (source == null) {
376: return;
377: }
378:
379: res.setContentType(MimeType.SWF);
380:
381: String app = LZHttpUtils.getRealPath(mContext, req);
382: boolean isClientCacheable = DataSource
383: .isClientCacheable(req);
384: if (mCache.isCacheable(req)) {
385: if (isClientCacheable) {
386: mLogger.info(
387: /* (non-Javadoc)
388: * @i18n.test
389: * @org-mes="proxying " + p[0] + ", cacheable on server and client"
390: */
391: org.openlaszlo.i18n.LaszloMessages.getMessage(
392: ResponderCache.class.getName(),
393: "051018-377", new Object[] { url }));
394: } else {
395: mLogger.info(
396: /* (non-Javadoc)
397: * @i18n.test
398: * @org-mes="proxying " + p[0] + ", cacheable on server and not client"
399: */
400: org.openlaszlo.i18n.LaszloMessages.getMessage(
401: ResponderCache.class.getName(),
402: "051018-386", new Object[] { url }));
403: }
404: mCache.getAsSWF(app, req, res, source);
405: } else {
406: if (isClientCacheable) {
407: mLogger.info(
408: /* (non-Javadoc)
409: * @i18n.test
410: * @org-mes="proxying " + p[0] + ", not cacheable on server and cacheable on the client"
411: */
412: org.openlaszlo.i18n.LaszloMessages.getMessage(
413: ResponderCache.class.getName(),
414: "051018-398", new Object[] { url }));
415: } else {
416: mLogger.info(
417: /* (non-Javadoc)
418: * @i18n.test
419: * @org-mes="proxying " + p[0] + ", not cacheable on server or client"
420: */
421: org.openlaszlo.i18n.LaszloMessages.getMessage(
422: ResponderCache.class.getName(),
423: "051018-407", new Object[] { url }));
424: }
425: source.getAsSWF(app, req, res, getConverter());
426: }
427: } catch (ConversionException e) {
428: respondWithErrorSWF(res,
429: /* (non-Javadoc)
430: * @i18n.test
431: * @org-mes="data conversion error for " + p[0] + ": " + p[1]
432: */
433: org.openlaszlo.i18n.LaszloMessages.getMessage(
434: ResponderCache.class.getName(), "051018-419",
435: new Object[] { url, e.getMessage() }));
436: errType = URLStat.ERRTYPE_CONVERSION;
437: } catch (DataSourceException e) {
438: respondWithErrorSWF(res,
439: /* (non-Javadoc)
440: * @i18n.test
441: * @org-mes="data source error for " + p[0] + ": " + p[1]
442: */
443: org.openlaszlo.i18n.LaszloMessages.getMessage(
444: ResponderCache.class.getName(), "051018-428",
445: new Object[] { url, e.getMessage() }));
446: errType = URLStat.ERRTYPE_DATA_SOURCE;
447: } catch (UnknownHostException e) {
448: respondWithErrorSWF(res,
449: /* (non-Javadoc)
450: * @i18n.test
451: * @org-mes="unknown host for " + p[0] + ": " + p[1]
452: */
453: org.openlaszlo.i18n.LaszloMessages.getMessage(
454: ResponderCache.class.getName(), "051018-437",
455: new Object[] { url, e.getMessage() }));
456: errType = URLStat.ERRTYPE_UNKNOWN_HOST;
457: } catch (URIException e) {
458: respondWithErrorSWF(res,
459: /* (non-Javadoc)
460: * @i18n.test
461: * @org-mes="bad url: " + p[0]
462: */
463: org.openlaszlo.i18n.LaszloMessages.getMessage(
464: ResponderCache.class.getName(), "051018-446",
465: new Object[] { e.getMessage() }));
466: errType = URLStat.ERRTYPE_MALFORMED_URL;
467: } catch (MalformedURLException e) {
468: respondWithErrorSWF(res,
469: /* (non-Javadoc)
470: * @i18n.test
471: * @org-mes="bad url: " + p[0]
472: */
473: org.openlaszlo.i18n.LaszloMessages.getMessage(
474: ResponderCache.class.getName(), "051018-446",
475: new Object[] { e.getMessage() }));
476: errType = URLStat.ERRTYPE_MALFORMED_URL;
477: } catch (InterruptedIOException e) {
478: respondWithErrorSWF(res,
479: /* (non-Javadoc)
480: * @i18n.test
481: * @org-mes="backend timeout for " + p[0] + ": " + p[1]
482: */
483: org.openlaszlo.i18n.LaszloMessages.getMessage(
484: ResponderCache.class.getName(), "051018-466",
485: new Object[] { url, e.getMessage() }));
486: errType = URLStat.ERRTYPE_TIMEOUT;
487: } catch (IOException e) {
488: // Handle SocketTimeoutExceptions as timeouts instead of IO issues
489: Class stec = null;
490: try {
491: stec = Class.forName("java.net.SocketTimeoutException");
492: } catch (ClassNotFoundException cfne) {
493: }
494: if (stec != null && stec.isAssignableFrom(e.getClass())) {
495: errType = URLStat.ERRTYPE_TIMEOUT;
496: respondWithErrorSWF(res,
497: /* (non-Javadoc)
498: * @i18n.test
499: * @org-mes="backend timeout for " + p[0] + ": " + p[1]
500: */
501: org.openlaszlo.i18n.LaszloMessages.getMessage(
502: ResponderCache.class.getName(), "051018-466",
503: new Object[] { url, e.getMessage() }));
504: } else {
505: respondWithExceptionSWF(res, e);
506: errType = URLStat.ERRTYPE_IO;
507: }
508: } catch (IllegalArgumentException e) {
509: respondWithExceptionSWF(res, e);
510: errType = URLStat.ERRTYPE_ILLEGAL_ARGUMENT;
511: } catch (Throwable e) {
512: // Makes much easier to debug runtime exceptions
513: // but perhaps not strictly correct.
514: respondWithExceptionSWF(res, e);
515: errType = URLStat.ERRTYPE_OTHER;
516: }
517:
518: if (mCollectStat) {
519: if (errType == URLStat.ERRTYPE_NONE)
520: mURLStat.success(url);
521: else
522: mURLStat.error(errType, url);
523: }
524: }
525:
526: /**
527: * @return the datasource for this request
528: */
529: protected DataSource getDataSource(HttpServletRequest req,
530: HttpServletResponse res) throws MalformedURLException,
531: URIException {
532: String ds = "http";
533: String urlstr = DataSource.getURL(req);
534: if (urlstr != null) {
535: mLogger.debug("urlstr " + urlstr);
536: URI uri = LZHttpUtils.newURI(urlstr);
537: String protocol = uri.getScheme();
538: if (protocol != null && protocol.equals("https")) {
539: protocol = "http";
540: }
541:
542: ds = protocol;
543: }
544:
545: mLogger.debug("ds is " + ds);
546:
547: DataSource source = null;
548: if (ds == null) {
549: source = mHTTPDataSource;
550: } else {
551: if ("soap".equals(ds)) {
552: // SOAP: dispatch on runtime ('lzr' query arg ) to figure out what
553: // kind of datatype the client wants (SWF or JSON).
554: // lzr == "dhtml" ? JSON : SWF
555:
556: String runtime = req.getParameter("lzr");
557: if ("dhtml".equals(runtime)) {
558: ds = "soap-json";
559: } else {
560: ds = "soap-swf";
561: }
562: }
563:
564: source = (DataSource) mDataSourceMap.get(ds);
565: if (source == null)
566: respondWithErrorSWF(res,
567: /* (non-Javadoc)
568: * @i18n.test
569: * @org-mes="Can't find a data source for " + p[0]
570: */
571: org.openlaszlo.i18n.LaszloMessages.getMessage(
572: ResponderCache.class.getName(), "051018-540",
573: new Object[] { urlstr }));
574: }
575: return source;
576: }
577:
578: public int getMimeType() {
579: return MIME_TYPE_SWF;
580: }
581:
582: public float getFlashPlayerVersion(HttpServletRequest req) {
583: float fpv = (float) -1.0;
584: try {
585: String _fpv = req.getParameter("fpv");
586: if (_fpv != null)
587: fpv = Float.parseFloat(_fpv);
588: } catch (NumberFormatException e) {
589: mLogger.debug(e.getMessage());
590: }
591: return fpv;
592: }
593:
594: /**
595: * @return the converter to be used by this cache
596: */
597: public Converter getConverter() {
598: return mCache.getConverter();
599: }
600: }
|