/*
* FilterHandler.java
*
* Brazil project web application Framework,
* export version: 1.1
* Copyright (c) 1999-2001 Sun Microsystems, Inc.
*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License Version
* 1.0 (the "License"). You may not use this file except in compliance with
* the License. A copy of the License is included as the file "license.terms",
* and also available at http://www.sun.com/
*
* The Original Code is from:
* Brazil project web application Framework release 1.1.
* The Initial Developer of the Original Code is: suhler.
* Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
* All Rights Reserved.
*
* Contributor(s): cstevens, suhler.
*
* Version: 1.17
* Created by suhler on 99/07/29
* Last modified by suhler on 01/01/14 14:52:11
*/
package sunlabs.brazil.filter;
import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.server.ChainHandler;
import sunlabs.brazil.util.http.MimeHeaders;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Vector;
import java.util.StringTokenizer;
import java.util.Properties;
/**
* The <code>FilterHandler</code> captures the output of some
* <code>Handler</code> and then applies an number of
* {@link sunlabs.brazil.filter.Filter Filters} to change that output
* before it is returned to the client.
* <p>
* This handler provides one of the core services now associated with
* the Brazil Server: the ability to dynamically rewrite web content
* obtained from an arbitrary source.
* <p>
* For instance, the <code>FilterHandler</code> can be used as a proxy for
* a PDA. The wrapped <code>Handler</code> would go to the web to
* obtain the requested pages on behalf of the PDA. Then, a
* <code>Filter</code> would examine all "text/html" pages and rewrite the
* pages so they fit into the PDA's 200 pixel wide screen. Another
* <code>Filter</code> would examine all requested images and dynamically
* dither them to reduce the wireless bandwidth consumed by the PDA.
* <p>
* The following configuration parameters are used to initialize this
* <code>Handler</code>: <dl class=props>
*
* <dt> <code>prefix</code>
* <dd> Only URLs beginning with this string will be candidates for
* filtering. The default is "/".
*
* <dt> <code>handler</code>
* <dd> The name of the <code>Handler</code> whose output will be captured
* and then filtered. This is called the "wrapped handler".
*
* <dt> <code>filters</code>
* <dd> A list of <code>Filter</code> names. The filters are applied in
* the specified order to the output of the wrapped handler.
* <dt> <code>exitOnError</code>
* <dd> If set, the server's <code>initFailure</code> will set
* any of the filters fail to
* initialize. No handler prefix is required.
* </dl>
*
* A sample set of configuration parameters illustrating how to use this
* handler follows:
* <pre>
* handler=filter
* port=8081
*
* filter.class=sunlabs.brazil.filter.FilterHandler
* filter.handler=proxy
* filter.filters=noimg
*
* proxy.class=sunlabs.brazil.proxy.ProxyHandler
*
* noimg.class=sunlabs.brazil.filter.TemplateFilter
* noimg.template=sunlabs.brazil.template.NoImageTemplate
* </pre>
* These parameters set up a proxy server running on port 8081. As with a
* normal proxy, this proxy server forwards all HTTP requests to the target
* machine, but it then examines all HTML pages before they are returned to
* the client and strips out all <code><img></code> tags. By applying
* different filters, the developer could instead build a server <ul>
* <li> to automatically dither embedded images down to grayscale (instead
* of simply stripping them all out)
* <li> to apply pattern recognition techniques to strip out only the
* advertisements
* <li> to examine and change arbitrary URLs on the page
* <li> to extract the content from an HTML page and dynamically combine it
* with another file to produce a different look-and-feel.
* </ul>
*
* @author Stephen Uhler (stephen.uhler@sun.com)
* @author Colin Stevens (colin.stevens@sun.com)
* @version 1.17 01/01/14
*/
public class FilterHandler
implements Handler
{
private static final String URL_PREFIX = "prefix";
private static final String HANDLER = "handler";
private static final String FILTERS = "filters";
String prefix;
String urlPrefix = "/";
public Handler handler;
public Filter[] filters;
/**
* Start the handler and filter classes.
*/
public boolean
init(Server server, String prefix)
{
this.prefix = prefix;
String str;
Properties props = server.props;
urlPrefix = props.getProperty(prefix + URL_PREFIX, urlPrefix);
boolean exitOnError= (props.getProperty("exitOnError") != null);
/*
* Start the handler to fetch the content.
*/
str = props.getProperty(prefix + HANDLER, "");
handler = ChainHandler.initHandler(server, prefix + HANDLER + ".",
str);
if (handler == null) {
return false;
}
server.log(Server.LOG_DIAGNOSTIC, prefix, "using handler: " + str);
/*
* Gather the filters.
*/
str = props.getProperty(prefix + FILTERS, "");
StringTokenizer names = new StringTokenizer(str);
Vector v = new Vector();
while (names.hasMoreTokens()) {
String name = names.nextToken();
server.log(Server.LOG_DIAGNOSTIC, prefix, "using filter: " + name);
Filter f = initFilter(server, prefix, name);
if (f != null) {
v.addElement(f);
} else if (exitOnError) {
server.log(Server.LOG_ERROR, prefix,
"filter: " + name + " didn't start");
server.initFailure=true;
}
}
if (v.size() == 0) {
server.log(Server.LOG_DIAGNOSTIC, prefix, "no filters");
return false;
}
filters = new Filter[v.size()];
v.copyInto(filters);
return true;
}
private static Filter
initFilter(Server server, String prefix, String name)
{
String className = server.props.getProperty(name + ".class");
if (className == null) {
className = name;
} else {
prefix = null;
}
if (prefix == null) {
prefix = name + ".";
}
try {
Filter f = (Filter) Class.forName(className).newInstance();
server.log(Server.LOG_DIAGNOSTIC, prefix, "Creating: " + className);
if (f.init(server, prefix)) {
return f;
}
server.log(Server.LOG_WARNING, name, "did not initialize");
} catch (ClassNotFoundException e) {
server.log(Server.LOG_WARNING, prefix, "no such class:" + e);
} catch (IllegalArgumentException e) {
server.log(Server.LOG_WARNING, prefix, "Invalid argument" + e);
} catch (ClassCastException e) {
server.log(Server.LOG_WARNING, prefix, "is not a Filter");
} catch (Exception e) {
server.log(Server.LOG_WARNING, prefix, "error initializing");
e.printStackTrace();
}
return null;
}
/**
* Responds to an HTTP request by the forwarding the request to the
* wrapped <code>Handler</code> and filtering the output of that
* <code>Handler</code> before sending the output to the client.
* <p>
* At several stages, the <code>Filters</code> are given a chance to
* short-circuit this process: <ul>
*
* <li> Each <code>Filter</code> is given a chance to examine the
* request before it is sent to the <code>Handler</code>. The
* <code>Filter</code> may decide to change the request's properties.
* A <code>Filter</code> may even return some content to the client now,
* in which case, neither the <code>Handler</code> nor any further
* <code>Filter</code>s are invoked at all.
*
* <li> After the <code>Handler</code> has generated the response headers,
* but before it has generated any content, each <code>Filter</code> is
* asked if it would be interested in filtering the content. If no
* <code>Filter</code> is, then the subsequent content from the
* <code>Handler</code> will be sent directly to the client.
*
* <li> On the other hand, if any <code>Filter</code> <b>is</b> interested
* in filtering the content, then the output of the <code>Handler</code>
* will be sent to each of the interested <code>Filter</code>s in order.
* The output of each interested <code>Filter</code> is sent to the
* next one; the output of the final <code>Filter</code> is sent to
* the client. At this point, any one of the invoked <code>Filter</code>s
* can decide to reject the content completely, instead of rewriting it.
* </ul>
*
* @param request
* The HTTP request to be forwarded to one of the sub-servers.
*
* @return <code>true</code> if the request was handled and content
* was generated, <code>false</code> otherwise.
*
* @throws IOException
* if there was an I/O error while sending the response to
* the client.
*/
public boolean
respond(Request request)
throws IOException
{
if (request.url.startsWith(urlPrefix) == false) {
return false;
}
/*
* Let each filter get a crack at the request as a handler.
*/
for (int i = 0; i< filters.length; i++) {
if (filters[i].respond(request)) {
return true;
}
}
/*
* Capture output from handler. When the handler is done, run the
* filters.
*/
FilterStream out = new FilterStream(request.out);
request.out = out;
try {
if (handler.respond(request) == false) {
request.log(Server.LOG_DIAGNOSTIC, prefix,
"No output from handler - skipping filters");
return false;
}
if (out.shouldFilter) {
return out.applyFilters(request);
} else {
/*
* handler.respond() has already sent the response.
*/
return true;
}
} finally {
out.restore(request);
}
}
private class FilterStream
extends Request.HttpOutputStream
{
boolean shouldFilter;
Request.HttpOutputStream old;
int count;
Filter[] postFilters;
public
FilterStream(Request.HttpOutputStream old)
{
super(new ByteArrayOutputStream());
this.old = old;
}
/**
* Check if any of the filters want to filter the data, based on
* what's in the HTTP headers. If none of them do, then we don't
* have to filter the data at all, so restore the request's original
* output stream.
*/
public void
sendHeaders(Request request)
throws IOException
{
postFilters = new Filter[filters.length];
for (int i = 0; i < filters.length; i++) {
Filter f = filters[i];
if (f.shouldFilter(request, request.responseHeaders)) {
postFilters[count++] = f;
}
}
if (count == 0) {
/*
* Based on the HTTP response headers, no filters want to
* process the content, so restore orginal output stream.
*/
request.log(Server.LOG_DIAGNOSTIC, prefix,
"no filters activated");
restore(request);
old.sendHeaders(request);
} else {
/*
* Disable chunked encoding, so we get the content as bytes
* not as chunks. We want only bytes so we can later apply
* the filters.
*/
request.version = 10;
shouldFilter = true;
}
}
public boolean
applyFilters(Request request)
throws IOException
{
request.out.flush();
restore(request);
byte[] content = ((ByteArrayOutputStream) out).toByteArray();
for (int i = 0; i < count; i++) {
content = postFilters[i].filter(request,
request.responseHeaders, content);
if (content == null) {
return false;
}
}
request.sendResponse(content, null);
return true;
}
public void
restore(Request request)
{
request.out = old;
}
}
}
|