/*
* FilterHandler.java
*
* Brazil project web application toolkit,
* export version: 2.3
* Copyright (c) 1999-2009 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 toolkit release 2.3.
* 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: 2.7
* Created by suhler on 99/07/29
* Last modified by suhler on 09/02/12 10:02:14
*
* Version Histories:
*
* 2.7 09/02/12-10:02:14 (suhler)
* add "typePrefix" parameter to restrict filters to run only on specified
* document types
*
* 2.6 08/02/15-16:22:29 (suhler)
* doc clarification
* .
*
* 2.5 08/02/15-16:16:41 (suhler)
* add matchString() capability to all filters
*
* 2.4 07/06/21-15:35:46 (suhler)
* Turn off chunked encoding when filtering by overriding the canChunk() method
* of HttpOutputStream
* .
*
* 2.3 06/11/13-15:07:14 (suhler)
* move MatchString to package "util" from "handler"
*
* 2.2 04/11/30-15:19:38 (suhler)
* fixed sccs version string
*
* 2.1 02/10/01-16:39:04 (suhler)
* version change
*
* 1.22 02/07/24-10:44:24 (suhler)
* doc updates
*
* 1.21 01/08/27-12:13:49 (suhler)
* doc changes
*
* 1.20 01/08/03-18:21:46 (suhler)
* remove training ws from classnames before trying to instantiate
*
* 1.19 01/07/20-11:32:58 (suhler)
* MatchUrl -> MatchString
*
* 1.18 01/07/17-14:14:07 (suhler)
* Use MatchUrl
*
* 1.17 01/01/14-14:52:11 (suhler)
* make fields public to allow scripting
*
* 1.16 00/12/11-13:26:22 (suhler)
* add class=props for automatic property extraction
*
* 1.15 00/12/08-16:47:29 (suhler)
* added exitOnError flag
* .
*
* 1.14 00/11/15-09:46:58 (suhler)
* better error messages
*
* 1.13 00/04/12-15:52:00 (cstevens)
* configuration property "wrap" -> "handler"
* configuration property "filter" -> "filters"
*
* 1.12 00/03/29-14:30:04 (cstevens)
* Disable chunked encoding before requesting data that will be filtered.
*
* 1.11 00/03/10-17:01:08 (cstevens)
* Now that the HTTP response headers is a first-class object, rewrote
* TailHandler/FilterHandler so that it doesn't have to re-extract the
* headers from the output stream of the wrapped handler.
*
* 1.10 00/02/11-08:57:01 (suhler)
* more diagnostics
*
* 1.9 99/10/18-10:26:28 (suhler)
* remove diagnostics
*
* 1.8 99/10/11-12:33:16 (suhler)
* use new MimeHeader class
*
* 1.7 99/10/06-12:38:08 (suhler)
* Merged changes between child workspace "/home/suhler/brazil/naws" and
* parent workspace "/net/mack.eng/export/ws/brazil/naws".
*
* 1.5.1.2 99/10/06-12:29:48 (suhler)
* use mime headers
*
* 1.6 99/10/01-11:27:21 (cstevens)
* Change logging to show prefix of Handler generating the log message.
*
* 1.5.1.1 99/09/17-13:55:24 (suhler)
* lint
*
* 1.5 99/09/01-15:31:15 (suhler)
* fix diagnostics
* handle handlers that return false
*
* 1.4 99/08/30-09:37:52 (suhler)
* redo to manage a list of filters (needs more testing)
*
* 1.3 99/08/06-08:31:12 (suhler)
* added call to filter's respond method
*
* 1.2 99/07/30-10:48:44 (suhler)
* added call to filter init method
*
* 1.2 99/07/29-16:17:21 (Codemgr)
* SunPro Code Manager data about conflicts, renames, etc...
* Name history : 2 1 filter/FilterHandler.java
* Name history : 1 0 tail/TailHandler.java
*
* 1.1 99/07/29-16:17:20 (suhler)
* date and time created 99/07/29 16:17:20 by suhler
*
*/
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.MatchString;
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 FilterHandler
captures the output of another
* Handler
and allows the ouput to
* be modified. One or more
* {@link sunlabs.brazil.filter.Filter Filters}
* may be specified to change that output
* before it is returned to the client.
*
* 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. *
* For instance, the FilterHandler
can be used as a proxy for
* a PDA. The wrapped Handler
would go to the web to
* obtain the requested pages on behalf of the PDA. Then, a
* Filter
would examine all "text/html" pages and rewrite the
* pages so they fit into the PDA's 200 pixel wide screen. Another
* Filter
would examine all requested images and dynamically
* dither them to reduce the wireless bandwidth consumed by the PDA.
*
* The following configuration parameters are used to initialize this
* Handler
:
typePrefix=text/html
causes
* only html
documents to be considered for filtering.
* "typePrefix" looks at the content type of the returned document, where
* as "prefix" (and suffix...) look at the URL used to fetch it.
*
* handler
* Handler
whose output will be captured
* and then filtered. This is called the "wrapped handler".
*
* filters
* Filter
names. The filters are applied in
* the specified order to the output of the wrapped handler. For
* each filter, the following properties are used:
* class
: To find the filter implementationprefix, suffix, glob, match, ignoreCase, invert
:
* to determine which URL's are processed by this filter.
* These properties are examined at each request, so "upstream"
* filters can effect the inclusion/exclusion of downstream
* filters.
* See {@link sunlabs.brazil.util.MatchString} for more information.
* exitOnError
* initFailure
will set
* any of the filters fail to
* initialize. No handler prefix is required.
* * 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 ** 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
<img>
tags. By applying
* different filters, the developer could instead build a server Handler
and filtering the output of that
* Handler
before sending the output to the client.
*
* At several stages, the Filters
are given a chance to
* short-circuit this process:
Filter
is given a chance to examine the
* request before it is sent to the Handler
* by invoking its respond() method. The
* Filter
may decide to change the request's properties.
* A Filter
may even return some content to the client now,
* by (calling request.sendResponse()
and
* returning true),
* in which case, neither the Handler
nor any further
* Filter
s are invoked at all.
*
* handler's respond()
* method is called, and is expected to generate content.
* If no content is generated at this step,
* this handler returns false.
*
* Handler
has generated the response headers,
* but before it has generated any content, each Filter
is
* asked if it would be interested in filtering the content. If no
* Filter
is, then the subsequent content from the
* Handler
will be sent directly to the client.
*
* Filter
is interested
* in filtering the content, then the output of the Handler
* will be sent to each of the interested Filter
s in order.
* The output of each interested Filter
is sent to the
* next one; the output of the final Filter
is sent to
* the client.
* Filter
s
* can decide to reject the content completely, instead of rewriting it.
* true
if the request was handled and content
* was generated, false
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 (!isMine.match(request.url)) {
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].isMatch(request) &&
filters[i].getFilter().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];
boolean check=true; // check shouldFilter()
// do type matching first
if (typePrefix != null) {
String type = request.responseHeaders.get("content-type");
if (type !=null && !type.toLowerCase().startsWith(typePrefix)) {
check=false;
}
}
for (int i = 0; check && i < filters.length; i++) {
FilterInfo f = filters[i];
if (f.isMatch(request) && f.getFilter().shouldFilter(
request, request.responseHeaders)) {
postFilters[count++] = f.getFilter();
}
}
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_INFORMATIONAL, prefix,
"no filters activated" +
(check?"":": content type not filterable"));
restore(request);
shouldFilter = false;
old.sendHeaders(request);
} else {
shouldFilter = true;
}
}
public boolean
applyFilters(Request request)
throws IOException
{
request.out.flush();
restore(request);
byte[] content = ((ByteArrayOutputStream) out).toByteArray();
request.log(Server.LOG_INFORMATIONAL, prefix, count +
" filters activated");
for (int i = 0; i < count; i++) {
request.log(Server.LOG_DIAGNOSTIC, prefix,
"Filtering " + request.url + ": " + postFilters[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;
}
/**
* Make sure we don't get chunked encoding if we are filtering.
*/
public boolean canChunk() {
return !shouldFilter;
}
}
/**
* Keep track of a filter and its MatchString() information.
* This allows us to do url matching on individual filters.
*/
static class FilterInfo {
Filter filter; // The filter to run
MatchString isMine; // What url's to run it on
public FilterInfo(Filter filter, String name) {
this.filter = filter;
isMine = new MatchString(name + ".");
}
public Filter getFilter() {
return filter;
}
public boolean isMatch(Request request) {
return isMine.match(request.url, request.props);
}
public String toString() {
return isMine.toString();
}
}
}