001: /*
002: * FilterHandler.java
003: *
004: * Brazil project web application Framework,
005: * export version: 1.1
006: * Copyright (c) 1999-2001 Sun Microsystems, Inc.
007: *
008: * Sun Public License Notice
009: *
010: * The contents of this file are subject to the Sun Public License Version
011: * 1.0 (the "License"). You may not use this file except in compliance with
012: * the License. A copy of the License is included as the file "license.terms",
013: * and also available at http://www.sun.com/
014: *
015: * The Original Code is from:
016: * Brazil project web application Framework release 1.1.
017: * The Initial Developer of the Original Code is: suhler.
018: * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
019: * All Rights Reserved.
020: *
021: * Contributor(s): cstevens, suhler.
022: *
023: * Version: 1.17
024: * Created by suhler on 99/07/29
025: * Last modified by suhler on 01/01/14 14:52:11
026: */
027:
028: package sunlabs.brazil.filter;
029:
030: import sunlabs.brazil.server.Handler;
031: import sunlabs.brazil.server.Request;
032: import sunlabs.brazil.server.Server;
033: import sunlabs.brazil.server.ChainHandler;
034:
035: import sunlabs.brazil.util.http.MimeHeaders;
036:
037: import java.io.ByteArrayOutputStream;
038: import java.io.IOException;
039: import java.io.OutputStream;
040: import java.util.Vector;
041: import java.util.StringTokenizer;
042: import java.util.Properties;
043:
044: /**
045: * The <code>FilterHandler</code> captures the output of some
046: * <code>Handler</code> and then applies an number of
047: * {@link sunlabs.brazil.filter.Filter Filters} to change that output
048: * before it is returned to the client.
049: * <p>
050: * This handler provides one of the core services now associated with
051: * the Brazil Server: the ability to dynamically rewrite web content
052: * obtained from an arbitrary source.
053: * <p>
054: * For instance, the <code>FilterHandler</code> can be used as a proxy for
055: * a PDA. The wrapped <code>Handler</code> would go to the web to
056: * obtain the requested pages on behalf of the PDA. Then, a
057: * <code>Filter</code> would examine all "text/html" pages and rewrite the
058: * pages so they fit into the PDA's 200 pixel wide screen. Another
059: * <code>Filter</code> would examine all requested images and dynamically
060: * dither them to reduce the wireless bandwidth consumed by the PDA.
061: * <p>
062: * The following configuration parameters are used to initialize this
063: * <code>Handler</code>: <dl class=props>
064: *
065: * <dt> <code>prefix</code>
066: * <dd> Only URLs beginning with this string will be candidates for
067: * filtering. The default is "/".
068: *
069: * <dt> <code>handler</code>
070: * <dd> The name of the <code>Handler</code> whose output will be captured
071: * and then filtered. This is called the "wrapped handler".
072: *
073: * <dt> <code>filters</code>
074: * <dd> A list of <code>Filter</code> names. The filters are applied in
075: * the specified order to the output of the wrapped handler.
076: * <dt> <code>exitOnError</code>
077: * <dd> If set, the server's <code>initFailure</code> will set
078: * any of the filters fail to
079: * initialize. No handler prefix is required.
080: * </dl>
081: *
082: * A sample set of configuration parameters illustrating how to use this
083: * handler follows:
084: * <pre>
085: * handler=filter
086: * port=8081
087: *
088: * filter.class=sunlabs.brazil.filter.FilterHandler
089: * filter.handler=proxy
090: * filter.filters=noimg
091: *
092: * proxy.class=sunlabs.brazil.proxy.ProxyHandler
093: *
094: * noimg.class=sunlabs.brazil.filter.TemplateFilter
095: * noimg.template=sunlabs.brazil.template.NoImageTemplate
096: * </pre>
097: * These parameters set up a proxy server running on port 8081. As with a
098: * normal proxy, this proxy server forwards all HTTP requests to the target
099: * machine, but it then examines all HTML pages before they are returned to
100: * the client and strips out all <code><img></code> tags. By applying
101: * different filters, the developer could instead build a server <ul>
102: * <li> to automatically dither embedded images down to grayscale (instead
103: * of simply stripping them all out)
104: * <li> to apply pattern recognition techniques to strip out only the
105: * advertisements
106: * <li> to examine and change arbitrary URLs on the page
107: * <li> to extract the content from an HTML page and dynamically combine it
108: * with another file to produce a different look-and-feel.
109: * </ul>
110: *
111: * @author Stephen Uhler (stephen.uhler@sun.com)
112: * @author Colin Stevens (colin.stevens@sun.com)
113: * @version 1.17 01/01/14
114: */
115:
116: public class FilterHandler implements Handler {
117: private static final String URL_PREFIX = "prefix";
118: private static final String HANDLER = "handler";
119: private static final String FILTERS = "filters";
120:
121: String prefix;
122:
123: String urlPrefix = "/";
124: public Handler handler;
125: public Filter[] filters;
126:
127: /**
128: * Start the handler and filter classes.
129: */
130:
131: public boolean init(Server server, String prefix) {
132: this .prefix = prefix;
133:
134: String str;
135:
136: Properties props = server.props;
137:
138: urlPrefix = props.getProperty(prefix + URL_PREFIX, urlPrefix);
139: boolean exitOnError = (props.getProperty("exitOnError") != null);
140:
141: /*
142: * Start the handler to fetch the content.
143: */
144:
145: str = props.getProperty(prefix + HANDLER, "");
146: handler = ChainHandler.initHandler(server, prefix + HANDLER
147: + ".", str);
148: if (handler == null) {
149: return false;
150: }
151: server.log(Server.LOG_DIAGNOSTIC, prefix, "using handler: "
152: + str);
153:
154: /*
155: * Gather the filters.
156: */
157:
158: str = props.getProperty(prefix + FILTERS, "");
159: StringTokenizer names = new StringTokenizer(str);
160: Vector v = new Vector();
161: while (names.hasMoreTokens()) {
162: String name = names.nextToken();
163: server.log(Server.LOG_DIAGNOSTIC, prefix, "using filter: "
164: + name);
165:
166: Filter f = initFilter(server, prefix, name);
167: if (f != null) {
168: v.addElement(f);
169: } else if (exitOnError) {
170: server.log(Server.LOG_ERROR, prefix, "filter: " + name
171: + " didn't start");
172: server.initFailure = true;
173: }
174: }
175: if (v.size() == 0) {
176: server.log(Server.LOG_DIAGNOSTIC, prefix, "no filters");
177: return false;
178: }
179: filters = new Filter[v.size()];
180: v.copyInto(filters);
181: return true;
182: }
183:
184: private static Filter initFilter(Server server, String prefix,
185: String name) {
186: String className = server.props.getProperty(name + ".class");
187: if (className == null) {
188: className = name;
189: } else {
190: prefix = null;
191: }
192: if (prefix == null) {
193: prefix = name + ".";
194: }
195:
196: try {
197: Filter f = (Filter) Class.forName(className).newInstance();
198: server.log(Server.LOG_DIAGNOSTIC, prefix, "Creating: "
199: + className);
200: if (f.init(server, prefix)) {
201: return f;
202: }
203: server.log(Server.LOG_WARNING, name, "did not initialize");
204: } catch (ClassNotFoundException e) {
205: server
206: .log(Server.LOG_WARNING, prefix, "no such class:"
207: + e);
208: } catch (IllegalArgumentException e) {
209: server.log(Server.LOG_WARNING, prefix, "Invalid argument"
210: + e);
211: } catch (ClassCastException e) {
212: server.log(Server.LOG_WARNING, prefix, "is not a Filter");
213: } catch (Exception e) {
214: server
215: .log(Server.LOG_WARNING, prefix,
216: "error initializing");
217: e.printStackTrace();
218: }
219: return null;
220: }
221:
222: /**
223: * Responds to an HTTP request by the forwarding the request to the
224: * wrapped <code>Handler</code> and filtering the output of that
225: * <code>Handler</code> before sending the output to the client.
226: * <p>
227: * At several stages, the <code>Filters</code> are given a chance to
228: * short-circuit this process: <ul>
229: *
230: * <li> Each <code>Filter</code> is given a chance to examine the
231: * request before it is sent to the <code>Handler</code>. The
232: * <code>Filter</code> may decide to change the request's properties.
233: * A <code>Filter</code> may even return some content to the client now,
234: * in which case, neither the <code>Handler</code> nor any further
235: * <code>Filter</code>s are invoked at all.
236: *
237: * <li> After the <code>Handler</code> has generated the response headers,
238: * but before it has generated any content, each <code>Filter</code> is
239: * asked if it would be interested in filtering the content. If no
240: * <code>Filter</code> is, then the subsequent content from the
241: * <code>Handler</code> will be sent directly to the client.
242: *
243: * <li> On the other hand, if any <code>Filter</code> <b>is</b> interested
244: * in filtering the content, then the output of the <code>Handler</code>
245: * will be sent to each of the interested <code>Filter</code>s in order.
246: * The output of each interested <code>Filter</code> is sent to the
247: * next one; the output of the final <code>Filter</code> is sent to
248: * the client. At this point, any one of the invoked <code>Filter</code>s
249: * can decide to reject the content completely, instead of rewriting it.
250: * </ul>
251: *
252: * @param request
253: * The HTTP request to be forwarded to one of the sub-servers.
254: *
255: * @return <code>true</code> if the request was handled and content
256: * was generated, <code>false</code> otherwise.
257: *
258: * @throws IOException
259: * if there was an I/O error while sending the response to
260: * the client.
261: */
262:
263: public boolean respond(Request request) throws IOException {
264: if (request.url.startsWith(urlPrefix) == false) {
265: return false;
266: }
267:
268: /*
269: * Let each filter get a crack at the request as a handler.
270: */
271:
272: for (int i = 0; i < filters.length; i++) {
273: if (filters[i].respond(request)) {
274: return true;
275: }
276: }
277:
278: /*
279: * Capture output from handler. When the handler is done, run the
280: * filters.
281: */
282:
283: FilterStream out = new FilterStream(request.out);
284: request.out = out;
285:
286: try {
287: if (handler.respond(request) == false) {
288: request.log(Server.LOG_DIAGNOSTIC, prefix,
289: "No output from handler - skipping filters");
290: return false;
291: }
292: if (out.shouldFilter) {
293: return out.applyFilters(request);
294: } else {
295: /*
296: * handler.respond() has already sent the response.
297: */
298: return true;
299: }
300: } finally {
301: out.restore(request);
302: }
303: }
304:
305: private class FilterStream extends Request.HttpOutputStream {
306: boolean shouldFilter;
307: Request.HttpOutputStream old;
308:
309: int count;
310: Filter[] postFilters;
311:
312: public FilterStream(Request.HttpOutputStream old) {
313: super (new ByteArrayOutputStream());
314:
315: this .old = old;
316: }
317:
318: /**
319: * Check if any of the filters want to filter the data, based on
320: * what's in the HTTP headers. If none of them do, then we don't
321: * have to filter the data at all, so restore the request's original
322: * output stream.
323: */
324:
325: public void sendHeaders(Request request) throws IOException {
326: postFilters = new Filter[filters.length];
327:
328: for (int i = 0; i < filters.length; i++) {
329: Filter f = filters[i];
330: if (f.shouldFilter(request, request.responseHeaders)) {
331: postFilters[count++] = f;
332: }
333: }
334:
335: if (count == 0) {
336: /*
337: * Based on the HTTP response headers, no filters want to
338: * process the content, so restore orginal output stream.
339: */
340:
341: request.log(Server.LOG_DIAGNOSTIC, prefix,
342: "no filters activated");
343: restore(request);
344: old.sendHeaders(request);
345: } else {
346: /*
347: * Disable chunked encoding, so we get the content as bytes
348: * not as chunks. We want only bytes so we can later apply
349: * the filters.
350: */
351:
352: request.version = 10;
353: shouldFilter = true;
354: }
355: }
356:
357: public boolean applyFilters(Request request) throws IOException {
358: request.out.flush();
359: restore(request);
360:
361: byte[] content = ((ByteArrayOutputStream) out)
362: .toByteArray();
363: for (int i = 0; i < count; i++) {
364: content = postFilters[i].filter(request,
365: request.responseHeaders, content);
366: if (content == null) {
367: return false;
368: }
369: }
370:
371: request.sendResponse(content, null);
372: return true;
373: }
374:
375: public void restore(Request request) {
376: request.out = old;
377: }
378: }
379: }
|