/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.ows;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.eclipse.emf.ecore.EObject;
import org.geoserver.ows.util.CaseInsensitiveMap;
import org.geoserver.ows.util.KvpMap;
import org.geoserver.ows.util.KvpUtils;
import org.geoserver.ows.util.OwsUtils;
import org.geoserver.ows.util.RequestUtils;
import org.geoserver.platform.GeoServerExtensions;
import org.geoserver.platform.Operation;
import org.geoserver.platform.Service;
import org.geoserver.platform.ServiceException;
import org.geotools.util.Version;
import org.geotools.xml.EMFUtils;
import org.geotools.xml.transform.TransformerBase;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserFactory;
/**
* Dispatches an http request to an open web service (OWS).
* <p>
* An OWS request contains three bits of information:
* </p>
* <ol>
* <li>The service being called
* <li>The operation of the service to execute
* <li>The version of the service ( optional )
* </ol>
* <p>
* Additional, an OWS request can contain an arbitray number of additional parameters.
* </p>
* <p>
* An OWS request can be specified in two forms. The first form is known as "KVP" in which all the parameters come in the form of a set of key-value
* pairs. Commonly this type of request is made in an http "GET" request, the parameters being specified in the query string:
*
* <pre>
* <code>http://www.xyz.com/geoserver?service=someService&request=someRequest&version=X.Y.Z¶m1=...¶m2=...</code>
* </pre>
* <p>
* This type of request can also be made in a "POST" request in with a
* mime-type of "application/x-www-form-urlencoded".
* </p>
* <p>
* The second form is known as "XML" in which all the parameters come in the
* form of an xml document. This type of request is made in an http "POST"
* request.
* </p>
* <pre><code>
* <?xml version="1.0" encoding="UTF-8"?>
* <SomeRequest service="someService" version="X.Y.Z">
* <Param1>...</Param1>
* <Param2>...</Param2>
* ...
* </SomeRequest>
* </code></pre>
* <p>
* When a request is received, the <b>service</b> the <b>version</b> parameters are used to locate a service desciptor, an instance of {@link Service}
* . With the service descriptor, the <b>request</b> parameter is used to locate the operation of the service to call.
* </p>
*
* @author Justin Deoliveira, The Open Planning Project, jdeolive@openplans.org
*/
public class Dispatcher extends AbstractController {
/**
* Logging instance
*/
static Logger logger = org.geotools.util.logging.Logging.getLogger("org.geoserver.ows");
/** flag to control wether the dispatcher is cite compliant */
boolean citeCompliant = false;
/**
* buffer size for incoming XML POST requests
*/
int xmlPostRequestLogBufferSize = 1024;
/** thread local variable for the request */
public static final ThreadLocal<Request> REQUEST = new InheritableThreadLocal<Request>();
static final Charset UTF8 = Charset.forName("UTF-8");
/**
* The amount of bytes to be read to determine the proper xml reader in POST request
*/
int XML_LOOKAHEAD = 8192;
/**
* list of callbacks
*/
List<DispatcherCallback> callbacks = Collections.EMPTY_LIST;
/** SOAP namespace */
static final String SOAP_NS = "http://www.w3.org/2003/05/soap-envelope";
/** SOAP mime type */
static final String SOAP_MIME = "application/soap+xml";
/**
* document builder, used to parse SOAP requests
*/
DocumentBuilder db;
{
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
db = dbf.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
}
/**
* Sets the flag to control wether the dispatcher is cite compliante.
* <p>
* If set to <code>true</code>, the dispatcher with throw exceptions when
* it encounters something that is not 100% compliant with CITE standards.
* An example would be a request which specifies the servce in the context
* path: '.../geoserver/wfs?request=...' and not with the kvp '&service=wfs'.
* </p>
*
* @param citeCompliant <code>true</code> to set compliance,
* <code>false</code> to unset it.
*/
public void setCiteCompliant(boolean citeCompliant) {
this.citeCompliant = citeCompliant;
}
public boolean isCiteCompliant() {
return citeCompliant;
}
@Override
protected void initApplicationContext(ApplicationContext context) {
//load life cycle callbacks
callbacks = GeoServerExtensions.extensions( DispatcherCallback.class, context);
// setup the xml lookahead value
String lookahead = GeoServerExtensions.getProperty("XML_LOOKAHEAD", context);
if(lookahead != null) {
try {
int lookaheadValue = Integer.valueOf(lookahead);
if(lookaheadValue <= 0)
logger.log(Level.SEVERE, "Invalid XML_LOOKAHEAD value, " +
"will use " + XML_LOOKAHEAD + " instead");
XML_LOOKAHEAD = lookaheadValue;
} catch(Exception e) {
logger.log(Level.SEVERE, "Invalid XML_LOOKAHEAD value, " +
"will use " + XML_LOOKAHEAD + " instead");
}
}
}
protected void preprocessRequest(HttpServletRequest request)
throws Exception {
//set the charset
Charset charSet = null;
//TODO: make this server settable
charSet = UTF8;
if(request.getCharacterEncoding() != null)
try {
charSet = Charset.forName(request.getCharacterEncoding());
} catch (Exception e) {
// ok, we tried...
}
request.setCharacterEncoding(charSet.name());
}
protected ModelAndView handleRequestInternal(HttpServletRequest httpRequest,
HttpServletResponse httpResponse) throws Exception {
preprocessRequest(httpRequest);
//create a new request instance
Request request = new Request();
//set request / response
request.setHttpRequest(httpRequest);
request.setHttpResponse(httpResponse);
Service service = null;
try {
// initialize the request and allow callbacks to override it
request = init(request);
// store it in the thread local
REQUEST.set(request);
//find the service
try {
service = service(request);
} catch (Throwable t) {
exception(t, null, request);
return null;
}
//throw any outstanding errors
if (request.getError() != null) {
throw request.getError();
}
//dispatch the operation
Operation operation = dispatch(request, service);
request.setOperation(operation);
if (request.isSOAP()) {
//let the request object know that this is a SOAP request, since it effects
// often how the request will be encoded
flagAsSOAP(operation);
}
//execute it
Object result = execute(request, operation);
//write the response
if (result != null) {
response(result, request, operation);
}
} catch (Throwable t) {
// make Spring security exceptions flow so that exception transformer filter can handle them
if (isSecurityException(t))
throw (Exception) t;
exception(t, service, request);
} finally {
fireFinishedCallback(request);
REQUEST.remove();
}
return null;
}
void flagAsSOAP(Operation op) {
for (Object reqObj : op.getParameters()) {
if (OwsUtils.has(reqObj, "formatOptions")) {
OwsUtils.put(reqObj, "formatOptions", "SOAP", true);
}
if (OwsUtils.has(reqObj, "extendedProperties")) {
OwsUtils.put(reqObj, "extendedProperties", "SOAP", true);
}
if (OwsUtils.has(reqObj, "metadata")) {
OwsUtils.put(reqObj, "metadata", "SOAP", true);
}
}
}
void fireFinishedCallback(Request req) {
for ( DispatcherCallback cb : callbacks ) {
try {
cb.finished( req );
} catch (Throwable t) {
logger.log(Level.WARNING, "Error firing finished callback for "+cb.getClass(), t);
}
}
}
Request init(Request request) throws ServiceException, IOException {
HttpServletRequest httpRequest = request.getHttpRequest();
String reqContentType = httpRequest.getContentType();
//figure out method
request.setGet("GET".equalsIgnoreCase(httpRequest.getMethod()) || isForm(reqContentType));
//create the kvp map
parseKVP(request);
if ( !request.isGet() ) { // && httpRequest.getInputStream().available() > 0) {
//check for a SOAP request, if so we need to unwrap the SOAP stuff
if (httpRequest.getContentType() != null &&
httpRequest.getContentType().startsWith(SOAP_MIME)) {
request.setSOAP(true);
request.setInput(soapReader(httpRequest));
}
else if (reqContentType != null && ServletFileUpload.isMultipartContent(httpRequest)) {
// multipart form upload
ServletFileUpload up = new ServletFileUpload();
up.setFileItemFactory(new DiskFileItemFactory());
// treat regular form fields as additional kvp parameters
Map<String,FileItem> kvpFileItems = new CaseInsensitiveMap(new LinkedHashMap());
try {
for (FileItem item : (List<FileItem>) up.parseRequest(httpRequest)) {
if (item.isFormField()) {
kvpFileItems.put(item.getFieldName(), item);
}
else {
request.setInput(fileItemReader(item));
}
}
} catch (Exception e) {
throw new ServiceException("Error handling multipart/form-data content", e);
}
// if no file fields were found, look for one named "body"
if (request.getInput() == null) {
FileItem body = kvpFileItems.get("body");
if (body != null) {
request.setInput(fileItemReader(body));
kvpFileItems.remove("body");
}
}
Map<String,String> kvpItems = new LinkedHashMap();
for (Map.Entry<String,FileItem> e : kvpFileItems.entrySet()) {
kvpItems.put(e.getKey(), e.getValue().toString());
}
request.setOrAppendKvp(parseKVP(request, kvpFileItems));
}
else {
//regular XML POST
//wrap the input stream in a buffered input stream
request.setInput(reader(httpRequest));
}
char[] req = new char[xmlPostRequestLogBufferSize];
int read = request.getInput().read(req, 0, xmlPostRequestLogBufferSize);
if (logger.isLoggable(Level.FINE)) {
if (read == -1) {
request.setInput(null);
} else if (read < xmlPostRequestLogBufferSize) {
logger.fine("Raw XML request: " + new String(req));
} else if (xmlPostRequestLogBufferSize == 0) {
// logging disabled --> do nothing
} else {
logger.fine("Raw XML request starts with: " + new String(req) + "...");
}
}
if (read == -1)
request.setInput(null);
else
request.getInput().reset();
}
// parse the request path into two components. (1) the 'path' which
// is the string after the last '/', and the 'context' which is the
// string before the last '/'
String ctxPath = request.httpRequest.getContextPath();
String reqPath = request.httpRequest.getRequestURI();
reqPath = reqPath.substring(ctxPath.length());
//strip off leading and trailing slashes
if (reqPath.startsWith("/")) {
reqPath = reqPath.substring(1, reqPath.length());
}
if (reqPath.endsWith("/")) {
reqPath = reqPath.substring(0, reqPath.length() - 1);
}
String context = reqPath;
String path = null;
int index = context.lastIndexOf('/');
if ( index != -1) {
path = context.substring(index+1);
context = context.substring(0, index);
}
else {
path = reqPath;
context = null;
}
request.setContext(context);
request.setPath(path);
return fireInitCallback(request);
}
private boolean isForm(String contentType) {
if (contentType == null) {
return false;
} else {
return contentType.startsWith("application/x-www-form-urlencoded");
}
}
Request fireInitCallback(Request req) {
for ( DispatcherCallback cb : callbacks ) {
Request r = cb.init( req );
req = r != null ? r : req;
}
return req;
}
BufferedReader soapReader(HttpServletRequest httpRequest) throws IOException {
//in order to pull out the payload we have to parse the entire request and then reencode it
// not nice... but then again neither is using SOAP
Document dom = null;
try {
dom = db.parse(httpRequest.getInputStream());
} catch (SAXException e) {
throw new IOException("Error parsing SOAP request", e);
}
//find the soap:Body element
NodeList list = dom.getElementsByTagNameNS(SOAP_NS, "Body");
if (list.getLength() != 1) {
throw new IOException("SOAP requests should specify a single Body element");
}
Element body = (Element) list.item(0);
//pull out the first element child
Element payload = null;
for (int i = 0; payload == null && i < body.getChildNodes().getLength(); i++) {
Node n = body.getChildNodes().item(i);
if (n instanceof Element) {
payload = (Element) n;
}
}
if (payload == null) {
throw new IOException("Could not find payload in SOAP request");
}
//transform the payload back into an input stream so we can parse it as usual
ByteArrayOutputStream bout = new ByteArrayOutputStream();
try {
TransformerFactory.newInstance().newTransformer().transform(
new DOMSource(payload), new StreamResult(bout));
} catch (Exception e) {
throw new IOException("Error encoding payload of SOAP request", e);
}
return RequestUtils.getBufferedXMLReader(
new ByteArrayInputStream(bout.toByteArray()), XML_LOOKAHEAD);
}
BufferedReader reader(HttpServletRequest httpRequest) throws IOException {
return RequestUtils.getBufferedXMLReader(httpRequest.getInputStream(), XML_LOOKAHEAD);
}
BufferedReader fileItemReader(FileItem fileItem) throws IOException {
return RequestUtils.getBufferedXMLReader(fileItem.getInputStream(), XML_LOOKAHEAD);
}
Service service(Request req) throws Exception {
//check kvp
if (req.getKvp() != null) {
req.setService(normalize(KvpUtils.getSingleValue(req.getKvp(), "service")));
req.setVersion(normalizeVersion(normalize(KvpUtils.getSingleValue(req.getKvp(), "version"))));
req.setRequest(normalize(KvpUtils.getSingleValue(req.getKvp(), "request")));
req.setOutputFormat(normalize(KvpUtils.getSingleValue(req.getKvp(), "outputFormat")));
}
//check the body
if (req.getInput() != null) {
Map xml = readOpPost(req.getInput());
if (xml.get("service") != null) {
req.setService(normalize((String) xml.get("service")));
}
if (xml.get("version") != null) {
req.setVersion(normalizeVersion(normalize((String) xml.get("version"))));
}
if (xml.get("request") != null) {
req.setRequest(normalize((String) xml.get("request")));
}
if (xml.get("outputFormat") != null) {
req.setOutputFormat(normalize((String) xml.get("outputFormat")));
}
if ((String)xml.get("namespace") != null) {
req.setNamespace(normalize((String)xml.get("namespace")));
}
}
//try to infer from context
//JD: for cite compliance, a service *must* be specified explicitley by
// either a kvp, or an xml attribute, however in reality the context
// is often a good way to infer the service or request
String service = req.getService();
if ((service == null) || (req.getRequest() == null)) {
Map map = readOpContext(req);
if (service == null) {
service = normalize((String) map.get("service"));
if ((service != null) && !citeCompliant) {
req.setService(service);
}
}
if (req.getRequest() == null) {
req.setRequest(normalize((String) map.get("request")));
}
}
if (service == null) {
//give up
throw new ServiceException("Could not determine service", "MissingParameterValue",
"service");
}
//load from teh context
Service serviceDescriptor = findService(service, req.getVersion(), req.getNamespace());
if (serviceDescriptor == null) {
//hack for backwards compatability, try finding the service with the context instead
// of the service
if (req.getContext() != null) {
serviceDescriptor = findService(req.getContext(), req.getVersion(), req.getNamespace());
if (serviceDescriptor != null) {
//found, assume that the client is using <service>/<request>
if (req.getRequest() == null) {
req.setRequest(req.getService());
}
req.setService(req.getContext());
req.setContext(null);
}
}
if (serviceDescriptor == null) {
String msg = "No service: ( " + service + " )";
throw new ServiceException(msg, "InvalidParameterValue", "service");
}
}
req.setServiceDescriptor(serviceDescriptor);
return fireServiceDispatchedCallback(req,serviceDescriptor);
}
Service fireServiceDispatchedCallback(Request req, Service service ) {
for ( DispatcherCallback cb : callbacks ) {
Service s = cb.serviceDispatched( req, service );
service = s != null ? s : service;
}
return service;
}
/**
* Normalize a parameter, trimming whitespace
* @param value
* @return The value with whitespace trimmed, or null if this would result in an empty string.
*/
public static String normalize(String value) {
if (value == null) {
return null;
}
if ("".equals(value.trim())) {
return null;
}
return value.trim();
}
/**
* Normalize the version, handling cases like forcing "x.y" to "x.y.z".
* @param version
* @return normalized version
*/
public static String normalizeVersion(String version) {
if (version == null) {
return null;
}
Version v = new Version(version);
if (v.getMajor() == null) {
return null;
}
if (v.getMinor() == null) {
return String.format("%d.0.0", v.getMajor());
}
if (v.getRevision() == null) {
return String.format("%d.%d.0", v.getMajor(), v.getMinor());
}
//version ok
return version;
}
Operation dispatch(Request req, Service serviceDescriptor)
throws Throwable {
if (req.getRequest() == null) {
String msg = "Could not determine geoserver request from http request " + req.getHttpRequest();
throw new ServiceException(msg, "MissingParameterValue", "request");
}
// ensure the requested operation exists
boolean exists = operationExists(req, serviceDescriptor);
// did we have a mixed kvp + post request and trusted the body for the request?
if(!exists && req.getKvp().get("request") != null) {
req.setRequest(normalize(KvpUtils.getSingleValue(req.getKvp(), "request")));
exists = operationExists(req, serviceDescriptor);
}
// lookup the operation, initial lookup based on (service,request)
Object serviceBean = serviceDescriptor.getService();
Method operation = OwsUtils.method(serviceBean.getClass(), req.getRequest());
if (operation == null || !exists) {
String msg = "No such operation " + req;
throw new ServiceException(msg, "OperationNotSupported", req.getRequest());
}
//step 4: setup the paramters
Object[] parameters = new Object[operation.getParameterTypes().length];
for (int i = 0; i < parameters.length; i++) {
Class parameterType = operation.getParameterTypes()[i];
//first check for servlet request and response
if (parameterType.isAssignableFrom(HttpServletRequest.class)) {
parameters[i] = req.getHttpRequest();
} else if (parameterType.isAssignableFrom(HttpServletResponse.class)) {
parameters[i] = req.getHttpResponse();
}
//next check for input and output
else if (parameterType.isAssignableFrom(InputStream.class)) {
parameters[i] = req.getHttpRequest().getInputStream();
} else if (parameterType.isAssignableFrom(OutputStream.class)) {
parameters[i] = req.getHttpResponse().getOutputStream();
} else {
//check for a request object
Object requestBean = null;
//track an exception
Throwable t = null;
// Boolean used for evaluating if the request bean has been parsed in KVP or in XML
boolean kvpParsed = false;
boolean xmlParsed = false;
if (req.getKvp() != null && req.getKvp().size() > 0) {
//use the kvp reader mechanism
try {
requestBean = parseRequestKVP(parameterType, req);
kvpParsed = true;
}
catch (Exception e) {
//dont die now, there might be a body to parse
t = e;
}
}
if (req.getInput() != null) {
//use the xml reader mechanism
requestBean = parseRequestXML(requestBean,req.getInput(), req);
xmlParsed = true;
}
//if no reader found for the request, throw exception
//TODO: we may wish to make this configurable, as perhaps there
// might be cases when the service prefers that null be passed in?
if ( requestBean == null ) {
//unable to parse request object, throw exception if we
// caught one
if ( t != null ) {
throw t;
}
if (kvpParsed && xmlParsed || (!kvpParsed && !xmlParsed)) {
throw new ServiceException(
"Could not find request reader (either kvp or xml) for: "
+ parameterType.getName()
+ ", it might be that some request parameters are missing, "
+ "please check the documentation");
} else if (kvpParsed) {
throw new ServiceException("Could not parse the KVP for: "
+ parameterType.getName());
} else {
throw new ServiceException("Could not parse the XML for: "
+ parameterType.getName());
}
}
// GEOS-934 and GEOS-1288
Method setBaseUrl = OwsUtils.setter(requestBean.getClass(), "baseUrl", String.class);
if (setBaseUrl != null) {
setBaseUrl.invoke(requestBean, new String[] { RequestUtils.baseURL(req.getHttpRequest())});
}
// another couple of thos of those lovley cite things, version+service has to specified for
// non capabilities request, so if we dont have either thus far, check the request
// objects to try and find one
// TODO: should make this configurable
if (requestBean != null) {
//if we dont have a version thus far, check the request object
if (req.getService() == null) {
req.setService(lookupRequestBeanProperty(requestBean, "service", false));
}
if (req.getVersion() == null) {
req.setVersion(normalizeVersion(
lookupRequestBeanProperty(requestBean, "version", false)));
}
if (req.getOutputFormat() == null) {
req.setOutputFormat(lookupRequestBeanProperty(requestBean, "outputFormat",
true));
}
parameters[i] = requestBean;
}
}
}
//if we are in cite compliant mode, do some additional checks to make
// sure the "mandatory" parameters are specified, even though we
// succesfully dispatched the request.
if (citeCompliant) {
// the version is mandatory for all requests but GetCapabilities
if (!"GetCapabilities".equalsIgnoreCase(req.getRequest())) {
if (req.getVersion() == null) {
//must be a version on non-capabilities requests
throw new ServiceException("Could not determine version",
"MissingParameterValue", "version");
} else {
//version must be valid
if (!req.getVersion().matches("[0-99].[0-99].[0-99]")) {
throw new ServiceException("Invalid version: " + req.getVersion(),
"InvalidParameterValue", "version");
}
//make sure the versoin actually exists
boolean found = false;
Version version = new Version(req.getVersion());
for (Iterator s = loadServices().iterator(); s.hasNext();) {
Service service = (Service) s.next();
if (version.equals(service.getVersion())) {
found = true;
break;
}
}
if (!found) {
throw new ServiceException("Invalid version: " + req.getVersion(),
"InvalidParameterValue", "version");
}
}
}
// the service is mandatory for all requests instead
if (req.getService() == null) {
//give up
throw new ServiceException("Could not determine service",
"MissingParameterValue", "service");
}
}
Operation op = new Operation(req.getRequest(), serviceDescriptor, operation, parameters);
return fireOperationDispatchedCallback(req,op);
}
private boolean operationExists(Request req, Service serviceDescriptor) {
boolean exists = false;
for ( String op : serviceDescriptor.getOperations() ) {
if ( op.equalsIgnoreCase( req.getRequest() ) ) {
exists = true;
break;
}
}
return exists;
}
Operation fireOperationDispatchedCallback(Request req, Operation op ) {
for ( DispatcherCallback cb : callbacks ) {
Operation o = cb.operationDispatched( req, op );
op = o != null ? o : op;
}
return op;
}
String lookupRequestBeanProperty(Object requestBean, String property, boolean allowDefaultValues) {
if (requestBean instanceof EObject && EMFUtils.has((EObject) requestBean, property)) {
//special case hack for eObject, we should move
// this out into an extension ppint
EObject eObject = (EObject) requestBean;
if (allowDefaultValues || EMFUtils.isSet(eObject, property)) {
return normalize((String) EMFUtils.get(eObject, property));
}
} else {
//straight reflection
String version = (String) OwsUtils.property(requestBean, property, String.class);
if (version != null) {
return normalize(version);
}
}
return null;
}
Object execute(Request req, Operation opDescriptor)
throws Throwable {
Service serviceDescriptor = opDescriptor.getService();
Object serviceBean = serviceDescriptor.getService();
Object[] parameters = opDescriptor.getParameters();
//step 5: execute
Object result = null;
try {
if(serviceBean instanceof DirectInvocationService){
// invokeDirect expects the operation to be called as declared in the operation
// descriptor, although it used to match a method name, lets use the declared
// operation name for contract compliance.
String operationName = opDescriptor.getId();
result = ((DirectInvocationService)serviceBean).invokeDirect(operationName, parameters);
}else{
Method operation = opDescriptor.getMethod();
result = operation.invoke(serviceBean, parameters);
}
} catch (Exception e) {
if (e.getCause() != null) {
throw e.getCause();
}
throw e;
}
return fireOperationExecutedCallback(req, opDescriptor, result);
}
Object fireOperationExecutedCallback(Request req, Operation op, Object result ) {
for ( DispatcherCallback cb : callbacks ) {
Object r = cb.operationExecuted( req, op, result );
result = r != null ? r : result;
}
return result;
}
void response(Object result, Request req, Operation opDescriptor)
throws Throwable {
//step 6: write response
if (result != null) {
//look up respones
List responses = GeoServerExtensions.extensions(Response.class);
//first filter by binding, and canHandle
O: for (Iterator itr = responses.iterator(); itr.hasNext();) {
Response response = (Response) itr.next();
Class binding = response.getBinding();
if (!binding.isAssignableFrom(result.getClass())
|| !response.canHandle(opDescriptor)) {
itr.remove();
continue;
}
//filter by output format
Set outputFormats = response.getOutputFormats();
if ((req.getOutputFormat() != null) && (!outputFormats.isEmpty())
&& !outputFormats.contains(req.getOutputFormat())) {
//must do a case insensitive check
for ( Iterator of = outputFormats.iterator(); of.hasNext(); ) {
String outputFormat = (String) of.next();
if( req.getOutputFormat().equalsIgnoreCase( outputFormat ) ) {
continue O;
}
}
itr.remove();
}
}
if (responses.isEmpty()) {
if(req.getOutputFormat() != null) {
throw new ServiceException("Failed to find response for output format " + req.getOutputFormat(),
ServiceException.INVALID_PARAMETER_VALUE, "outputFormat");
} else {
String msg = "No response: ( object = " + result.getClass();
if (req.getOutputFormat() != null) {
msg += (", outputFormat = " + req.getOutputFormat());
}
msg += " )";
throw new RuntimeException(msg);
}
}
if (responses.size() > 1) {
//sort by class hierarchy
Collections.sort(responses,
new Comparator() {
public int compare(Object o1, Object o2) {
Class c1 = ((Response) o1).getBinding();
Class c2 = ((Response) o2).getBinding();
if (c1.equals(c2)) {
return 0;
}
if (c1.isAssignableFrom(c2)) {
return 1;
}
if (c2.isAssignableFrom(c1)) {
;
}
return -1;
}
});
//check first two and make sure bindings are not equal
Response r1 = (Response) responses.get(0);
Response r2 = (Response) responses.get(1);
if (r1.getBinding().equals(r2.getBinding())) {
String msg = "Multiple responses: (" + result.getClass() + "): " + r1 + ", " + r2;
throw new RuntimeException(msg);
}
}
Response response = (Response) responses.get(0);
response = fireResponseDispatchedCallback(req,opDescriptor,result,response);
//load the output strategy to be used
ServiceStrategy outputStrategy = findOutputStrategy(req.getHttpResponse());
if (outputStrategy == null) {
outputStrategy = new DefaultOutputStrategy();
}
//set the mime type
String mimeType = response.getMimeType(result, opDescriptor);
//check for SOAP request
if (req.isSOAP()) {
req.getHttpResponse().setContentType(SOAP_MIME);
}
else {
req.getHttpResponse().setContentType(mimeType);
}
//set the charset
String charset = response.getCharset(opDescriptor);
if(charset != null){
req.getHttpResponse().setCharacterEncoding(charset);
}
setHeaders(req,opDescriptor,result,response);
OutputStream output = outputStrategy.getDestination(req.getHttpResponse());
boolean abortResponse = true;
try {
if (req.isSOAP()) {
//SOAP request, start the SOAP wrapper
startSOAPEnvelope(output, response);
}
//special check for transformer
if (req.isSOAP() && result instanceof TransformerBase) {
((TransformerBase)result).setOmitXMLDeclaration(true);
}
// actually write out the response
response.write(result, output, opDescriptor);
if (req.isSOAP()) {
//SOAP request, start the SOAP wrapper
endSOAPEnvelope(output);
}
// flush the output with detection of client shutting the door in our face
try {
outputStrategy.flush(req.getHttpResponse());
} catch(IOException e) {
throw new ClientStreamAbortedException(e);
}
abortResponse = true;
} finally {
if(abortResponse) {
outputStrategy.abort();
}
}
// flush the underlying out stream for good measure
req.getHttpResponse().getOutputStream().flush();
}
}
void setHeaders(Request req, Operation opDescriptor, Object result, Response response) {
// get the basics using the new api
Map rawKvp = req.getRawKvp();
String disposition = response.getPreferredDisposition(result, opDescriptor);
String filename = response.getAttachmentFileName(result, opDescriptor);
// get user overrides, if any
if (rawKvp != null) {
// check if the filename and content disposition were provided
if(rawKvp.get("FILENAME") != null) {
filename = (String) rawKvp.get("FILENAME");
}
if(rawKvp.get("CONTENT-DISPOSITION") != null) {
disposition = (String) rawKvp.get("CONTENT-DISPOSITION");
}
}
// make sure the disposition obtained so far is valid
// check and prevent invalid header injection
if(disposition != null && !Response.DISPOSITION_ATTACH.equals(disposition)
&& !Response.DISPOSITION_INLINE.equals(disposition)) {
disposition = null;
}
// set any extra headers, other than the mime-type
String[][] headers = response.getHeaders(result, opDescriptor);
boolean contentDispositionProvided = false;
if (headers != null) {
for (int i = 0; i < headers.length; i++) {
if (headers[i][0].equalsIgnoreCase("Content-Disposition")) {
contentDispositionProvided = true;
if(disposition == null) {
req.getHttpResponse().addHeader(headers[i][0], headers[i][1]);
}
} else {
req.getHttpResponse().addHeader(headers[i][0], headers[i][1]);
}
}
}
// default disposition value and set if not forced by the user and not set
// directly by the response
if(!contentDispositionProvided) {
if (disposition == null) {
disposition = Response.DISPOSITION_INLINE;
}
// override any existing header
String disp = disposition + "; filename=" + filename;
req.getHttpResponse().setHeader("Content-Disposition", disp);
}
}
void startSOAPEnvelope(OutputStream output, Response response) throws IOException {
output.write(("<soap:Envelope xmlns:soap='" + SOAP_NS + "'><soap:Header/>").getBytes());
output.write("<soap:Body".getBytes());
if (response != null && response instanceof SOAPAwareResponse) {
String type = ((SOAPAwareResponse)response).getBodyType();
if (type != null) {
output.write((" type='" + type + "'").getBytes());
}
}
output.write(">".getBytes());
}
void endSOAPEnvelope(OutputStream output) throws IOException {
output.write(("</soap:Body></soap:Envelope>").getBytes());
}
Response fireResponseDispatchedCallback(Request req, Operation op, Object result, Response response ) {
for ( DispatcherCallback cb : callbacks ) {
Response r = cb.responseDispatched(req, op, result, response);
response = r != null ? r : response;
}
return response;
}
Collection loadServices() {
Collection services = GeoServerExtensions.extensions(Service.class);
if (!(new HashSet(services).size() == services.size())) {
String msg = "Two identical service descriptors found";
throw new IllegalStateException(msg);
}
return services;
}
Service findService(String id, String ver, String namespace) throws ServiceException {
Version version = (ver != null) ? new Version(ver) : null;
Collection services = loadServices();
// the id is actually the pathinfo, in case workspace specific services
// are active we want to skip the workspace part in the path and go directly to the
// servlet, which normally, if we ended up here, is a reflector (wms/kml)
if(id.contains("/")) {
id = id.substring(id.indexOf("/") + 1);
}
//first just match on service,request
List matches = new ArrayList();
for (Iterator itr = services.iterator(); itr.hasNext();) {
Service sBean = (Service) itr.next();
if (sBean.getId().equalsIgnoreCase(id)) {
matches.add(sBean);
}
}
if (matches.isEmpty()) {
return null;
}
Service sBean = null;
//if multiple, use version to filter match
if (matches.size() > 1) {
List vmatches = new ArrayList(matches);
//match up the version
if (version != null) {
//version specified, look for a match
for (Iterator itr = vmatches.iterator(); itr.hasNext();) {
Service s = (Service) itr.next();
if (version.equals(s.getVersion())) {
continue;
}
itr.remove();
}
if (vmatches.isEmpty()) {
//no matching version found, drop out and next step
// will sort to return highest version
vmatches = new ArrayList(matches);
}
}
//if still multiple matches use namespace, if available, to filter
if (namespace != null && vmatches.size() > 1) {
List nmatches = new ArrayList(vmatches);
for (Iterator itr = nmatches.iterator(); itr.hasNext();) {
Service s = (Service) itr.next();
if (s.getNamespace() != null && !s.getNamespace().equals(namespace)) {
//service declares namespace, kick it out if there is no match
itr.remove();
}
else {
//service does not declare namespace, leave it along
}
}
if (!nmatches.isEmpty()) {
vmatches = nmatches;
}
}
//multiple services found, sort by version
if (vmatches.size() > 1) {
//use highest version
Comparator comparator = new Comparator() {
public int compare(Object o1, Object o2) {
Service s1 = (Service) o1;
Service s2 = (Service) o2;
return s1.getVersion().compareTo(s2.getVersion());
}
};
Collections.sort(vmatches, comparator);
}
sBean = (Service) vmatches.get(vmatches.size() - 1);
} else {
//only a single match, that was easy
sBean = (Service) matches.get(0);
}
return sBean;
}
public static Collection loadKvpRequestReaders() {
Collection kvpReaders = GeoServerExtensions.extensions(KvpRequestReader.class);
if (!(new HashSet(kvpReaders).size() == kvpReaders.size())) {
String msg = "Two identical kvp readers found";
throw new IllegalStateException(msg);
}
return kvpReaders;
}
public static KvpRequestReader findKvpRequestReader(Class type) {
Collection kvpReaders = loadKvpRequestReaders();
List matches = new ArrayList();
for (Iterator itr = kvpReaders.iterator(); itr.hasNext();) {
KvpRequestReader kvpReader = (KvpRequestReader) itr.next();
if (kvpReader.getRequestBean().isAssignableFrom(type)) {
matches.add(kvpReader);
}
}
if (matches.isEmpty()) {
return null;
}
if (matches.size() > 1) {
//sort by class hierarchy
Comparator comparator = new Comparator() {
public int compare(Object o1, Object o2) {
KvpRequestReader kvp1 = (KvpRequestReader) o1;
KvpRequestReader kvp2 = (KvpRequestReader) o2;
if (kvp2.getRequestBean().isAssignableFrom(kvp1.getRequestBean())) {
return -1;
}
return 1;
}
};
Collections.sort(matches, comparator);
}
return (KvpRequestReader) matches.get(0);
}
static Collection loadXmlReaders() {
List<XmlRequestReader> xmlReaders = GeoServerExtensions.extensions(XmlRequestReader.class);
if (!(new HashSet<XmlRequestReader>(xmlReaders).size() == xmlReaders.size())) {
String msg = "Two identical xml readers found";
for (int i = 0; i < xmlReaders.size(); i++) {
XmlRequestReader r1 = xmlReaders.get(i);
for (int j = i + 1; j < xmlReaders.size(); j++) {
XmlRequestReader r2 = xmlReaders.get(j);
if(r1.equals(r2)) {
msg += ": " + r1 + " and " + r2;
break;
}
}
}
throw new IllegalStateException(msg);
}
return xmlReaders;
}
/**
* Finds a registered {@link XmlRequestReader} bean able to read a request, given the request details
*
* @param namespace The XML namespace of the request body
* @param element The OWS request, e.g. "GetMap"
* @param serviceId The OWS service, e.g. "WMS"
* @param ver The OWS service version, e.g "1.1.1"
* @return An {@link XmlRequestReader} capable of reading the request body
*/
public static XmlRequestReader findXmlReader(String namespace, String element, String serviceId, String ver) {
Collection xmlReaders = loadXmlReaders();
//first just match on namespace, element
List matches = new ArrayList();
for (Iterator itr = xmlReaders.iterator(); itr.hasNext();) {
XmlRequestReader xmlReader = (XmlRequestReader) itr.next();
QName xmlElement = xmlReader.getElement();
if (xmlElement.getLocalPart().equalsIgnoreCase(element)) {
if (xmlElement.getNamespaceURI().equalsIgnoreCase(namespace)) {
matches.add(xmlReader);
}
}
}
if (matches.isEmpty()) {
//do a more lax serach, search only on the element name if the
// namespace was unspecified
if ( namespace == null || namespace.equals( "" ) ) {
String msg = "No namespace specified in request, searching for "
+ " xml reader by element name only";
logger.info( msg );
for ( Iterator itr = xmlReaders.iterator(); itr.hasNext(); ) {
XmlRequestReader xmlReader = (XmlRequestReader) itr.next();
if ( xmlReader.getElement().getLocalPart().equals( element ) ) {
matches.add( xmlReader );
}
}
if ( !matches.isEmpty() ) {
//we found some matches, make sure they are all in the
// same service
Iterator itr = matches.iterator();
XmlRequestReader first = (XmlRequestReader) itr.next();
while( itr.hasNext() ) {
XmlRequestReader xmlReader = (XmlRequestReader ) itr.next();
if (!first.getServiceId().equals(xmlReader.getServiceId())) {
//abort
matches.clear();
break;
}
}
}
}
}
if ( matches.isEmpty() ) {
String msg = "No xml reader: (" + namespace + "," + element + ")";
logger.info(msg);
return null;
}
XmlRequestReader xmlReader = null;
//if multiple, use version to filter match
if (matches.size() > 1) {
List vmatches = new ArrayList(matches);
// match up the service
if(serviceId != null) {
for (Iterator itr = vmatches.iterator(); itr.hasNext();) {
XmlRequestReader r = (XmlRequestReader) itr.next();
if (r.getServiceId() == null || serviceId.equalsIgnoreCase(r.getServiceId())) {
continue;
}
itr.remove();
}
// if no reader matching the service is found, we should
// not return a reader, as service is key to identify the reader
// we cannot just assume a meaningful default
}
// match up the version
if (ver != null) {
Version version = new Version(ver);
// version specified, look for a match (and allow version
// generic ones to live by)
for (Iterator itr = vmatches.iterator(); itr.hasNext();) {
XmlRequestReader r = (XmlRequestReader) itr.next();
if (r.getVersion() == null || version.equals(r.getVersion())) {
continue;
}
itr.remove();
}
if (vmatches.isEmpty()) {
// no matching version found, drop out and next step
// will sort to return highest version
vmatches = new ArrayList(matches);
}
}
//multiple readers found, sort by version and by service match
if (vmatches.size() > 1) {
//use highest version
Comparator comparator = new Comparator() {
public int compare(Object o1, Object o2) {
XmlRequestReader r1 = (XmlRequestReader) o1;
XmlRequestReader r2 = (XmlRequestReader) o2;
Version v1 = r1.getVersion();
Version v2 = r2.getVersion();
if ((v1 == null) && (v2 == null)) {
return 0;
}
if ((v1 != null) && (v2 == null)) {
return 1;
}
if ((v1 == null) && (v2 != null)) {
return -1;
}
int versionCompare = v1.compareTo(v2);
if (versionCompare != 0) {
return versionCompare;
}
String sid1 = r1.getServiceId();
String sid2 = r2.getServiceId();
if ((sid1 == null) && (sid2 == null)) {
return 0;
}
if ((sid1 != null) && (sid2 == null)) {
return 1;
}
if ((sid1 == null) && (sid2 != null)) {
return -1;
}
return sid1.compareTo(sid2);
}
};
Collections.sort(vmatches, comparator);
}
if(vmatches.size() > 0 )
xmlReader = (XmlRequestReader) vmatches.get(vmatches.size() - 1);
} else {
//only a single match, that was easy
xmlReader = (XmlRequestReader) matches.get(0);
}
return xmlReader;
}
ServiceStrategy findOutputStrategy(HttpServletResponse response) {
OutputStrategyFactory factory = null;
try {
factory = (OutputStrategyFactory) GeoServerExtensions.bean("serviceStrategyFactory");
} catch(NoSuchBeanDefinitionException e) {
return null;
}
return factory.createOutputStrategy(response);
}
BufferedInputStream input(File cache) throws IOException {
return (cache == null) ? null : new BufferedInputStream(new FileInputStream(cache));
}
void preParseKVP(Request req) throws ServiceException {
HttpServletRequest request = req.getHttpRequest();
//unparsed kvp set
Map kvp = request.getParameterMap();
if (kvp == null || kvp.isEmpty()) {
req.setKvp(new HashMap());
//req.kvp = null;
return;
}
//track parsed kvp and unparsd
Map parsedKvp = KvpUtils.normalize(kvp);
Map rawKvp = new KvpMap( parsedKvp );
req.setKvp(parsedKvp);
req.setRawKvp(rawKvp);
}
void parseKVP(Request req) throws ServiceException {
preParseKVP(req);
parseKVP(req, req.getKvp());
}
Map parseKVP(Request req, Map kvp) {
List<Throwable> errors = KvpUtils.parse(kvp);
if ( !errors.isEmpty() ) {
req.setError(errors.get(0));
}
return kvp;
}
Object parseRequestKVP(Class type, Request request)
throws Exception {
KvpRequestReader kvpReader = findKvpRequestReader(type);
if (kvpReader != null) {
//check for http request awareness
if (kvpReader instanceof HttpServletRequestAware) {
((HttpServletRequestAware) kvpReader).setHttpRequest(request.getHttpRequest());
}
Object requestBean = kvpReader.createRequest();
if (requestBean != null) {
requestBean = kvpReader.read(requestBean, request.getKvp(), request.getRawKvp());
}
return requestBean;
}
return null;
}
Object parseRequestXML(Object requestBean, BufferedReader input, Request request)
throws Exception {
//check for an empty input stream
//if (input.available() == 0) {
if (!input.ready()) {
return null;
}
//create stream parser
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
//parse root element
XmlPullParser parser = factory.newPullParser();
//parser.setInput(input, "UTF-8");
parser.setInput(input);
parser.nextTag();
String namespace = (parser.getNamespace() != null) ? parser.getNamespace() : "";
String element = parser.getName();
String version = null;
String service = null;
for (int i = 0; i < parser.getAttributeCount(); i++) {
if ("version".equals(parser.getAttributeName(i))) {
version = parser.getAttributeValue(i);
}
if ("service".equals(parser.getAttributeName(i))) {
service = parser.getAttributeValue(i);
}
}
parser.setInput(null);
//reset input stream
input.reset();
XmlRequestReader xmlReader = findXmlReader(namespace, element, service, version);
if (xmlReader == null ) {
//no xml reader, just return object passed in
return requestBean;
}
if (xmlReader instanceof HttpServletRequestAware) {
((HttpServletRequestAware) xmlReader).setHttpRequest(request.getHttpRequest());
}
//return xmlReader.read(input);
return xmlReader.read( requestBean, input, request.getKvp() );
}
/**
* Reads the following parameters from an OWS XML request body:
* * service
*
* @param request {@link Request} object
* @return a {@link Map} containing the parsed parameters.
*/
public static Map readOpContext(Request request) {
Map map = new HashMap();
if (request.getPath() != null) {
map.put("service", request.getPath());
}
return map;
}
/**
* Reads the following parameters from an OWS XML request body:
* * request
* * namespace
* * service
* * version
* * outputFormat
* Resets the input reader after reading
*
* @param input {@link BufferedReader} containing a valid OWS XML request body
* @return a {@link Map} containing the parsed parameters.
* @throws Exception if there was an error reading the input.
*/
public static Map readOpPost(BufferedReader input) throws Exception {
//create stream parser
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setValidating(false);
//parse root element
XmlPullParser parser = factory.newPullParser();
parser.setInput(input);
parser.nextTag();
Map map = new HashMap();
map.put("request", parser.getName());
map.put("namespace", parser.getNamespace());
for (int i = 0; i < parser.getAttributeCount(); i++) {
String attName = parser.getAttributeName(i);
if ("service".equals(attName)) {
map.put("service", parser.getAttributeValue(i));
}
if ("version".equals(parser.getAttributeName(i))) {
map.put("version", parser.getAttributeValue(i));
}
if ("outputFormat".equals(attName)) {
map.put("outputFormat", parser.getAttributeValue(i));
}
}
//close parser + release resources
parser.setInput(null);
//reset the input stream
input.reset();
return map;
}
void exception(Throwable t, Service service, Request request) {
Throwable current = t;
while (current != null && !(current instanceof ClientStreamAbortedException)
&& !isSecurityException(current)
&& !(current instanceof HttpErrorCodeException)) {
if(current instanceof SAXException)
current = ((SAXException) current).getException();
else
current = current.getCause();
}
if (current instanceof ClientStreamAbortedException) {
logger.log(Level.FINER, "Client has closed stream", t);
return;
}
if ( isSecurityException(current)) {
throw (RuntimeException) current;
}
if (current instanceof HttpErrorCodeException) {
HttpErrorCodeException ece = (HttpErrorCodeException) current;
int errorCode = ece.getErrorCode();
if (errorCode < 199 || errorCode > 299) {
logger.log(Level.FINE, "", t);
} else {
logger.log(Level.FINER, "", t);
}
boolean isError = ece.getErrorCode() >= 400;
HttpServletResponse rsp = request.getHttpResponse();
if (ece.getContentType() != null) {
rsp.setContentType(ece.getContentType());
}
try {
if (isError) {
if (ece.getMessage() != null) {
rsp.sendError(ece.getErrorCode(), ece.getMessage());
}
else {
rsp.sendError(ece.getErrorCode());
}
}
else {
rsp.setStatus(ece.getErrorCode());
if (ece.getMessage() != null) {
rsp.getOutputStream().print(ece.getMessage());
}
}
if (!isError) {
// gwc returns an HttpErrorCodeException for 304s
// we don't want to flag these as errors for upstream filters, ie the monitoring extension
t = null;
}
} catch (IOException e) {
// means the resposne was already commited something
logger.log(Level.FINER, "", t);
}
} else {
logger.log(Level.SEVERE, "", t);
//unwind the exception stack until we find one we know about
Throwable cause = t;
while( cause != null ) {
if ( cause instanceof ServiceException ) {
break;
}
cause = cause.getCause();
}
if ( cause == null ) {
// did not fine a "special" exception, create a service exception by default
cause = new ServiceException(t);
}
// at this point we're sure it'a service exception
ServiceException se = (ServiceException) cause;
if ( cause != t ) {
//copy the message, code + locator, but set cause equal to root
se = new ServiceException( se.getMessage(), t, se.getCode(), se.getLocator() );
}
handleServiceException(se,service,request);
}
request.error = t;
}
void handleServiceException( ServiceException se, Service service, Request request ) {
//find an exception handler
ServiceExceptionHandler handler = null;
if (service != null) {
//look up the service exception handler
Collection handlers = GeoServerExtensions.extensions(ServiceExceptionHandler.class);
for (Iterator h = handlers.iterator(); h.hasNext();) {
ServiceExceptionHandler seh = (ServiceExceptionHandler) h.next();
if (seh.getServices().contains(service)) {
//found one,
handler = seh;
break;
}
}
}
if (handler == null) {
//none found, fall back on default
handler = new DefaultServiceExceptionHandler();
}
//if SOAP request use special SOAP exception handler, but only for OWS requests because
// there could be other service exception handlers (like WMS for instance) that do not
// output XML
if (request.isSOAP() && (handler instanceof OWS10ServiceExceptionHandler ||
handler instanceof OWS11ServiceExceptionHandler)) {
handler = new SOAPServiceExceptionHandler(handler);
}
handler.handleServiceException(se, request);
}
/**
* Examines a {@link Throwable} object and returns true if it represents a security exception.
* @param t Throwable
* @return true if t is a security exception
*/
protected static boolean isSecurityException(Throwable t) {
return t != null &&
t.getClass().getPackage().getName().startsWith("org.springframework.security");
}
public int getXMLPostRequestLogBufferSize() {
return xmlPostRequestLogBufferSize;
}
public void setXMLPostRequestLogBufferSize(int bufferSize) {
this.xmlPostRequestLogBufferSize = bufferSize;
}
}