/* (c) 2014 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.script.py;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
import org.apache.commons.lang.StringUtils;
import org.geoserver.ows.util.CaseInsensitiveMap;
import org.geoserver.script.app.AppHook;
import org.geotools.util.logging.Logging;
import org.python.core.Py;
import org.python.core.PyDictionary;
import org.python.core.PyFile;
import org.python.core.PyFunction;
import org.python.core.PyInteger;
import org.python.core.PyIterator;
import org.python.core.PyList;
import org.python.core.PyObject;
import org.python.core.PyObjectDerived;
import org.python.core.PyString;
import org.python.core.PyStringMap;
import org.python.core.PyTuple;
import org.restlet.data.MediaType;
import org.restlet.data.Reference;
import org.restlet.data.Request;
import org.restlet.data.Response;
import org.restlet.resource.OutputRepresentation;
/**
* Python app hook.
*
* <p>
* This app hook adapts the incoming request into a WSGI request requiring the app script to
* implement a WSGI interface via a function named "app". See
* <a href="http://en.wikipedia.org/wiki/Web_Server_Gateway_Interface">web service gateway interface</a> for more details about
* WSGI.
* </p>
*
* @author Justin Deoliveira, OpenGeo
*
*/
public class PyAppHook extends AppHook {
static Logger LOGGER = Logging.getLogger(PyAppHook.class);
static ThreadLocal<WSGIResponse> RESPONSE = new ThreadLocal<WSGIResponse>();
public PyAppHook(PythonPlugin plugin) {
super(plugin);
}
@Override
public void run(Request request, Response response, ScriptEngine engine)
throws ScriptException, IOException {
Object obj = engine.get("app");
if (obj == null) {
throw new RuntimeException("No 'app' function defined");
}
if (!(obj instanceof PyObject)) {
throw new RuntimeException("'app not callable, found a " + obj.toString());
}
PyObject app = (PyObject) obj;
WSGIResponse wr = new WSGIResponse();
RESPONSE.set(wr);
try {
Object ret = app.__call__(new PyObject[]{createEnviron(request), createStartResponse()});
if (ret != null) {
String contentType = wr.headers.get("content-type");
if (contentType == null) {
contentType = "text/plain";
}
MediaType mediaType = new MediaType(contentType);
if (ret instanceof PyString) {
response.setEntity(ret.toString(), mediaType);
}
else if (ret instanceof PyList) {
final PyList list = (PyList) ret;
response.setEntity(new OutputRepresentation(mediaType) {
@Override
public void write(OutputStream outputStream) throws IOException {
for (Iterator i = list.iterator(); i.hasNext();) {
outputStream.write(i.next().toString().getBytes());
if (i.hasNext()) {
outputStream.write('\n');
}
}
}
});
}
else if (ret instanceof PyIterator) {
final PyIterator iter = (PyIterator) ret;
response.setEntity(new OutputRepresentation(mediaType) {
@Override
public void write(OutputStream outputStream) throws IOException {
for (Iterator i = iter.iterator(); i.hasNext();) {
outputStream.write(i.next().toString().getBytes());
outputStream.write('\n');
}
}
});
}
else if (ret instanceof PyObjectDerived) {
final PyObjectDerived iter = (PyObjectDerived)ret;
response.setEntity(new OutputRepresentation(mediaType) {
@Override
public void write(OutputStream outputStream) throws IOException {
PyObject next = null;
while ((next = iter.__iternext__()) != null) {
outputStream.write(next.toString().getBytes());
outputStream.write('\n');
}
}
});
}
else {
LOGGER.warning( "Unsure how to handle " + ret + ". Resorting to outputing string " +
"representation.");
response.setEntity(ret.toString(), mediaType);
}
}
}
finally {
RESPONSE.remove();
}
}
/**
* Creates the environ object which is a dictionary with the following entries:
* <pre>
* REQUEST_METHOD
* The HTTP request method, such as "GET" or "POST". This cannot ever be
* an empty string, and so is always required.
* SCRIPT_NAME
* The initial portion of the request URL's "path" that corresponds to the
* application object, so that the application knows its virtual "location"
* . This may be an empty string, if the application corresponds to the
* "root" of the server.
* PATH_INFO
* The remainder of the request URL's "path", designating the virtual
* "location" of the request's target within the application. This may be
* an empty string, if the request URL targets the application root and
* does not have a trailing slash.
* QUERY_STRING
* The portion of the request URL that follows the "?", if any. May be
* empty or absent.
* CONTENT_TYPE
* The contents of any Content-Type fields in the HTTP request. May be
* empty or absent.
* CONTENT_LENGTH
* The contents of any Content-Length fields in the HTTP request. May be
* empty or absent.
* SERVER_NAME, SERVER_PORT
* When combined with SCRIPT_NAME and PATH_INFO, these variables can be
* used to complete the URL. Note, however, that HTTP_HOST, if present,
* should be used in preference to SERVER_NAME for reconstructing the
* request URL. See the URL Reconstruction section below for more detail.
* SERVER_NAME and SERVER_PORT can never be empty strings, and so are
* always required.
* SERVER_PROTOCOL
* The version of the protocol the client used to send the request.
* Typically this will be something like "HTTP/1.0" or "HTTP/1.1" and may
* be used by the application to determine how to treat any HTTP request
* headers. (This variable should probably be called REQUEST_PROTOCOL,
* since it denotes the protocol used in the request, and is not
* necessarily the protocol that will be used in the server's response.
* However, for compatibility with CGI we have to keep the existing name.)
* HTTP_ Variables
* Variables corresponding to the client-supplied HTTP request headers
* (i.e., variables whose names begin with "HTTP_"). The presence or
* absence of these variables should correspond with the presence or
* absence of the appropriate HTTP header in the request.
* wsgi.version
* The tuple (1, 0), representing WSGI version 1.0.
*
* wsgi.url_scheme
* A string representing the "scheme" portion of the URL at which the application
* is being invoked. Normally, this will have the value "http" or "https", as
* appropriate.
*
* wsgi.input
* An input stream (file-like object) from which the HTTP request body can be
* read. (The server or gateway may perform reads on-demand as requested by the
* application, or it may pre- read the client's request body and buffer it
* in-memory or on disk, or use any other technique for providing such an input
* stream, according to its preference.)
*
* wsgi.errors
* An output stream (file-like object) to which error output can be written, for
* the purpose of recording program or other errors in a standardized and
* possibly centralized location. This should be a "text mode" stream; i.e.,
* applications should use "\n" as a line ending, and assume that it will be
* converted to the correct line ending by the server/gateway.
*
* For many servers, wsgi.errors will be the server's main error log.
* Alternatively, this may be sys.stderr, or a log file of some sort. The
* server's documentation should include an explanation of how to configure this
* or where to find the recorded output. A server or gateway may supply different
* error streams to different applications, if this is desired.
*
* wsgi.multithread
* This value should evaluate true if the application object may be
* simultaneously invoked by another thread in the same process, and should
* evaluate false otherwise.
*
* wsgi.multiprocess
* This value should evaluate true if an equivalent application object may be
* simultaneously invoked by another process, and should evaluate false
* otherwise.
*
* wsgi.run_once
* This value should evaluate true if the server or gateway expects (but does not
* guarantee!) that the application will only be invoked this one time during the
* life of its containing process. Normally, this will only be true for a gateway
* based on CGI (or something similar).
* </pre>
* @param request
*
* @throws IOException
*/
PyObject createEnviron(Request request) throws IOException {
PyDictionary environ = new PyDictionary();
environ.put("REQUEST_METHOD", request.getMethod().toString());
Reference ref = request.getResourceRef();
environ.put("SCRIPT_NAME", ref.getLastSegment());
//force to pystring so that frameworks don't try to encode as idna
environ.put("SERVER_NAME", new PyString(ref.getHostDomain()));
environ.put("SERVER_PORT", String.valueOf(ref.getHostPort()));
List<String> seg = new ArrayList(ref.getSegments().subList(4, ref.getSegments().size()));
seg.add(0, "");
environ.put("PATH_INFO", StringUtils.join(seg.toArray(), "/"));
//environ.put("PATH_INFO", );
environ.put("QUERY_STRING", request.getResourceRef().getQuery());
environ.put("wsgi.version", new PyTuple(new PyInteger(0), new PyInteger(1)));
environ.put("wsgi.url_scheme", ref.getScheme());
environ.put("wsgi.input", new PyFile(request.getEntity().getStream()));
environ.put("wsgi.errors", new PyFile(System.err));
environ.put("wsgi.multithread", true);
environ.put("wsgi.multitprocess", false);
environ.put("wsgi.run_once", false);
return environ;
}
/**
* Creates the start_response object.
*/
PyFunction createStartResponse() {
return new PyFunction(new PyStringMap(), new PyObject[]{}, Py.newJavaCode(getClass(), "start_response"));
}
public static Object start_response(PyObject[] objs, String[] values) {
PyString status = (PyString) objs[0];
int space = status.toString().indexOf(' ');
WSGIResponse r = RESPONSE.get();
if (space != -1) {
r.code = status.toString().substring(0, space);
r.message = status.toString().substring(space+1);
}
else {
r.code = status.toString();
}
if (objs.length > 1) {
PyList headers = (PyList) objs[1];
for (Iterator i = headers.iterator(); i.hasNext();) {
PyTuple tup = (PyTuple) i.next();
r.headers.put(tup.get(0).toString(), tup.get(1).toString());
}
}
return null;
}
static class WSGIResponse {
String code;
String message;
Map<String,String> headers = new CaseInsensitiveMap(new TreeMap<String, String>());
}
}