blob: 1980d7a46330c1495edd04f27cc4e7fd2bfd9033 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.cocoon.generation;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.service.ServiceException;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.ResourceNotFoundException;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.commons.httpclient.HttpConnection;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpState;
import org.apache.commons.httpclient.HttpURL;
import org.apache.commons.httpclient.HostConfiguration;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.URIException;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.excalibur.xml.sax.SAXParser;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
/**
* The <code>HttpProxyGenerator</code> is a Cocoon generator using the
* <b>Jakarta Commons HTTPClient Library</b> to access an XML stream
* over HTTP.
*
* @author <a href="mailto:ivelin@apache.org">Ivelin Ivanov</a>, June 2002
* @author <a href="mailto:tony@apache.org">Tony Collen</a>, December 2002
* @author <a href="mailto:pier@apache.org">Pier Fumagalli</a>, February 2003
* @version CVS $Id$
*/
public class HttpProxyGenerator extends ServiceableGenerator implements Configurable {
/** The HTTP method to use at request time. */
private HttpMethodBase method = null;
/** The base HTTP URL for requests. */
private HttpURL url = null;
/** The list of request parameters for the request */
private List<NameValuePair> reqParams = null;
/** The list of query parameters for the request */
private List<NameValuePair> qryParams = null;
/** Wether we want a debug output or not */
private boolean debug = false;
/**
* Default (empty) constructor.
*/
public HttpProxyGenerator() {
super();
}
/**
* Set up this <code>Generator</code> instance from its sitemap <code>Configuration</code>
*
* @param configuration The base <code>Configuration</code> for this <code>Generator</code>.
* @throws ConfigurationException If this instance cannot be configured properly.
* @see #recycle()
*/
public void configure(Configuration configuration)
throws ConfigurationException {
/* Setup the HTTP method to use. */
String method = configuration.getChild("method").getValue("GET");
if ("GET".equalsIgnoreCase(method)) {
this.method = new GetMethod();
} else if ("POST".equalsIgnoreCase(method)) {
this.method = new PostMethod();
/* TODO: Is this still needed? Does it refer to a bug in bugzilla?
* At least the handling in httpclient has completely changed.
* Work around a bug from the HttpClient library */
((PostMethod) this.method).setRequestBody("");
} else {
throw new ConfigurationException("Invalid method \"" + method + "\" specified"
+ " at " + configuration.getChild("method").getLocation());
}
/* Create the base URL */
String url = configuration.getChild("url").getValue(null);
try {
if (url != null) this.url = new HttpURL(url);
} catch (URIException e) {
throw new ConfigurationException("Cannot process URL \"" + url + "\" specified"
+ " at " + configuration.getChild("url").getLocation());
}
/* Prepare the base request and query parameters */
this.reqParams = this.getParams(configuration.getChildren("param"));
this.qryParams = this.getParams(configuration.getChildren("query"));
}
/**
* Setup this <code>Generator</code> with its runtime configurations and parameters
* specified in the sitemap, and prepare it for generation.
*
* @param sourceResolver The <code>SourceResolver</code> instance resolving sources by
* system identifiers.
* @param objectModel The Cocoon "object model" <code>Map</code>
* @param parameters The runtime <code>Parameters</code> instance.
* @throws ProcessingException If this instance could not be setup.
* @throws SAXException If a SAX error occurred during setup.
* @throws IOException If an I/O error occurred during setup.
* @see #recycle()
*/
public void setup(SourceResolver sourceResolver, Map objectModel,
String source, Parameters parameters)
throws ProcessingException, SAXException, IOException {
/* Do the usual stuff */
super.setup(sourceResolver, objectModel, source, parameters);
/*
* Parameter handling: In case the method is a POST method, query
* parameters and request parameters will be two different arrays
* (one for the body, one for the query string, otherwise it's going
* to be the same one, as all parameters are passed on the query string
*/
List<NameValuePair> req = new ArrayList<NameValuePair>();
List<NameValuePair> qry = req;
if (this.method instanceof PostMethod)
qry = new ArrayList<NameValuePair>();
req.addAll(this.reqParams);
qry.addAll(this.qryParams);
/*
* Parameter handling: complete or override the configured parameters with
* those specified in the pipeline.
*/
String[] names = parameters.getNames();
for (String name : names) {
String value = parameters.getParameter(name, null);
if (value == null) continue;
if (name.startsWith("query:")) {
name = name.substring("query:".length());
qry.add(new NameValuePair(name, value));
} else if (name.startsWith("param:")) {
name = name.substring("param:".length());
req.add(new NameValuePair(name, value));
} else if (name.startsWith("query-override:")) {
name = name.substring("query-override:".length());
qry = overrideParams(qry, name, value);
} else if (name.startsWith("param-override:")) {
name = name.substring("param-override:".length());
req = overrideParams(req, name, value);
}
}
/* Process the current source URL in relation to the configured one */
HttpURL src = (super.source == null ? null : new HttpURL(super.source));
if (this.url != null) src = (src == null ? this.url : new HttpURL(this.url, src));
if (src == null) throw new ProcessingException("No URL specified");
if (src.isRelativeURI()) {
throw new ProcessingException("Invalid URL \"" + src.toString() + "\"");
}
/* Configure the method with the resolved URL */
HostConfiguration hc = new HostConfiguration();
hc.setHost(src);
this.method.setHostConfiguration(hc);
this.method.setPath(src.getPath());
this.method.setQueryString(src.getQuery());
/* And now process the query string (from the parameters above) */
if (qry.size() > 0) {
String qs = this.method.getQueryString();
NameValuePair[] nvpa = new NameValuePair[qry.size()];
this.method.setQueryString(qry.toArray(nvpa));
if (qs != null) {
this.method.setQueryString(qs + "&" + this.method.getQueryString());
}
}
/* Finally process the body parameters */
if ((this.method instanceof PostMethod) && (req.size() > 0)) {
PostMethod post = (PostMethod) this.method;
NameValuePair[] nvpa = new NameValuePair[req.size()];
post.setRequestBody(req.toArray(nvpa));
}
/* Check the debugging flag */
this.debug = parameters.getParameterAsBoolean("debug", false);
}
/**
* Recycle this instance, clearing all done during setup and generation, and reverting
* back to what was configured in the sitemap.
*
* @see #configure(Configuration)
* @see #setup(SourceResolver, Map, String, Parameters)
* @see #generate()
*/
public void recycle() {
/* Recycle the method */
this.method.recycle();
/* TODO: Is this still needed? Does it refer to a bug in bugzilla?
* At least the handling in httpclient has completely changed.
* Work around a bug from the HttpClient library */
if (this.method instanceof PostMethod) ((PostMethod) this.method).setRequestBody("");
/* Clean up our parent */
super.recycle();
}
/**
* Parse the remote <code>InputStream</code> accessed over HTTP.
*
* @throws ResourceNotFoundException If the remote HTTP resource could not be found.
* @throws ProcessingException If an error occurred processing generation.
* @throws SAXException If an error occurred parsing or processing XML in the pipeline.
* @throws IOException If an I/O error occurred accessing the HTTP server.
*/
public void generate()
throws ResourceNotFoundException, ProcessingException, SAXException, IOException {
/* Do the boring stuff in case we have to do a debug output (blablabla) */
if (this.debug) {
this.generateDebugOutput();
return;
}
/* Call up the remote HTTP server */
HttpConnection connection = new HttpConnection(this.method.getHostConfiguration());
HttpState state = new HttpState();
this.method.setFollowRedirects(true);
int status = this.method.execute(state, connection);
if (status == 404) {
throw new ResourceNotFoundException("Unable to access \"" + this.method.getURI()
+ "\" (HTTP 404 Error)");
} else if ((status < 200) || (status > 299)) {
throw new IOException("Unable to access HTTP resource at \""
+ this.method.getURI().toString() + "\" (status=" + status + ")");
}
InputStream response = this.method.getResponseBodyAsStream();
/* Let's try to set up our InputSource from the response output stream and to parse it */
SAXParser parser = null;
try {
InputSource inputSource = new InputSource(response);
parser = (SAXParser) this.manager.lookup(SAXParser.ROLE);
parser.parse(inputSource, super.xmlConsumer);
} catch (ServiceException ex) {
throw new ProcessingException("Unable to get parser", ex);
} finally {
this.manager.release(parser);
this.method.releaseConnection();
connection.close();
}
}
/**
* Generate debugging output as XML data from the current configuration.
*
* @throws SAXException If an error occurred parsing or processing XML in the pipeline.
* @throws IOException If an I/O error occurred accessing the HTTP server.
*/
private void generateDebugOutput()
throws SAXException, IOException {
super.xmlConsumer.startDocument();
AttributesImpl attributes = new AttributesImpl();
attributes.addAttribute("", "method", "method", "CDATA", this.method.getName());
attributes.addAttribute("", "url", "url", "CDATA", this.method.getURI().toString());
attributes.addAttribute("", "protocol", "protocol", "CDATA",
(this.method.isHttp11() ? "HTTP/1.1" : "HTTP/1.0"));
super.xmlConsumer.startElement("", "request", "request", attributes);
if (this.method instanceof PostMethod) {
byte[] bodyBytes = this.method.getResponseBody();
String body = new String(bodyBytes, "utf-8");
attributes.clear();
attributes.addAttribute("", "name", "name", "CDATA", "Content-Type");
attributes.addAttribute("", "value", "value", "CDATA", "application/x-www-form-urlencoded");
super.xmlConsumer.startElement("", "header", "header", attributes);
super.xmlConsumer.endElement("", "header", "header");
attributes.clear();
attributes.addAttribute("", "name", "name", "CDATA", "Content-Length");
attributes.addAttribute("", "value", "value", "CDATA", Integer.toString(body.length()));
super.xmlConsumer.startElement("", "header", "header", attributes);
super.xmlConsumer.endElement("", "header", "header");
attributes.clear();
super.xmlConsumer.startElement("", "body", "body", attributes);
super.xmlConsumer.characters(body.toCharArray(), 0, body.length());
super.xmlConsumer.endElement("", "body", "body");
}
super.xmlConsumer.endElement("", "request", "request");
super.xmlConsumer.endDocument();
}
/**
* Prepare a map of parameters from an array of <code>Configuration</code>
* items.
*
* @param configurations An array of <code>Configuration</code> elements.
* @return A <code>List</code> of <code>NameValuePair</code> elements.
* @throws ConfigurationException If a parameter doesn't specify a name.
*/
private List<NameValuePair> getParams(Configuration[] configurations)
throws ConfigurationException {
List<NameValuePair> list = new ArrayList<NameValuePair>();
if (configurations.length < 1) return (list);
for (Configuration configuration : configurations) {
String name = configuration.getAttribute("name", null);
if (name == null) {
throw new ConfigurationException("No name specified for parameter at "
+ configuration.getLocation());
}
String value = configuration.getAttribute("value", null);
if (value != null) list.add(new NameValuePair(name, value));
Configuration[] subconfigurations = configuration.getChildren("value");
for (Configuration subconfiguration : subconfigurations) {
value = subconfiguration.getValue(null);
if (value != null) list.add(new NameValuePair(name, value));
}
}
return list;
}
/**
* Override the value for a named parameter in a specfied <code>ArrayList</code>
* or add it if the parameter was not found.
*
* @param list The <code>ArrayList</code> where the parameter is stored.
* @param name The parameter name.
* @param value The new parameter value.
* @return The same <code>List</code> of <code>NameValuePair</code> elements.
*/
private List<NameValuePair> overrideParams(List<NameValuePair> list, String name, String value) {
Iterator<NameValuePair> iterator = list.iterator();
while (iterator.hasNext()) {
NameValuePair param = iterator.next();
if (param.getName().equals(name)) {
iterator.remove();
break;
}
}
list.add(new NameValuePair(name, value));
return (list);
}
}