#!/usr/bin/python
# 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.
# Composite deployment and integration with WSGI
from wsgiref.simple_server import make_server
from wsgiref.handlers import CGIHandler
from wsgiref.util import request_uri
from wsgiref.util import FileWrapper
from os import environ
import os.path
import hashlib
from sys import stderr, argv
from util import *
from scdl import *
from atomutil import *
from jsonutil import *
# Cache the deployed components between requests
comps = None
# Return the path of an HTTP request
def requestPath(e):
return e.get("PATH_INFO", "")
# Return the method of an HTTP request
def requestMethod(e):
return e.get("REQUEST_METHOD", "")
# Return the method of an HTTP request
def requestContentType(e):
return e.get("CONTENT_TYPE", "")
# Return the request body input stream
def requestBody(e):
i = e.get("wsgi.input", None)
if i == None:
return ()
l = int(e.get("CONTENT_LENGTH", "0"))
return (i.read(l),)
def requestIfNoneMatch(e):
return e.get("HTTP_IF_NONE_MATCH", "");
# Hash a list of strings into an MD5 signature
def md5update(md, s):
if isNil(s):
return md.hexdigest()
md.update(car(s))
return md5update(md, cdr(s))
def md5(s):
return md5update(hashlib.md5(), s)
# Return an HTTP success result
def result(e, r, st, h = (), b = None):
if st == 201:
r("201 Created", list(h))
return ()
if st == 200:
if b == None:
r("200 OK", list(h))
return ()
# Handle etags to minimize bandwidth usage
md = md5(b)
if (md == requestIfNoneMatch(e)):
r("304 Not Modified", list((("Etag", md), ("Expires", "Tue, 01 Jan 1980 00:00:00 GMT"))))
return ()
r("200 OK", list(h + (("Etag", md), ("Expires", "Tue, 01 Jan 1980 00:00:00 GMT"))))
return b
if st == 301:
r("301 Moved Permanently", list(h))
return failure(e, r, 500)
# Return an HTTP failure result
def failure(e, r, st):
s = "404 Not Found" if st == 404 else str(st) + " " + "Internal Server Error"
r(s, list((("Content-type", "text/html"),)))
return ("
"+ s + "" + s[4:] + "
",)
# Return a static file
def fileresult(e, r, ct, f):
# Read the file, return a 404 if not found
p = "htdocs" + f
if not os.path.exists(p):
return failure(e, r, 404)
c = tuple(FileWrapper(open("htdocs" + f)))
# Handle etags to minimize bandwidth usage
md = md5(c)
r("200 OK", list((("Content-type", ct),("Etag", md))))
return c
# Converts the args received in a POST to a list of key value pairs
def postArgs(a):
if isNil(a):
return ((),)
l = car(a);
return cons(l, postArgs(cdr(a)))
# Return the URL used to sign out
def signout(ruri):
try:
from google.appengine.api import users
return users.create_logout_url(ruri)
except:
return None
# Return the URL used to sign in
def signin(ruri):
try:
from google.appengine.api import users
return users.create_login_url(ruri)
except:
return None
# WSGI application function
def application(e, r):
m = requestMethod(e)
fpath = requestPath(e)
# Serve static files
if m == "GET":
if fpath.endswith(".html"):
return fileresult(e, r, "text/html", fpath)
if fpath.endswith(".css"):
return fileresult(e, r, "text/css", fpath)
if fpath.endswith(".js"):
return fileresult(e, r, "application/x-javascript", fpath)
if fpath.endswith(".png"):
return fileresult(e, r, "image/png", fpath)
if fpath == "/":
return result(e, r, 301, (("Location", "/index.html"),))
# Debug hook
if fpath == "/debug":
return result(e, r, 200, (("Content-type", "text/plain"),), ("Debug",))
# Sign in and out
if fpath == "/login":
redir = signin("/")
if redir:
return result(e, r, 301, (("Location", redir),))
if fpath == "/logout":
redir = signout(signin("/"))
if redir:
return result(e, r, 301, (("Location", redir),))
# Find the requested component
path = tokens(fpath)
uc = uriToComponent(path, comps)
uri = car(uc)
if uri == None:
return failure(e, r, 404)
comp = cadr(uc)
# Call the requested component function
id = path[len(uri):]
if m == "GET":
v = comp("get", id)
# Write a simple value as a JSON value
if not isList(v):
return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(valuesToElements((("'value", v),))))
# Write an empty list as a JSON empty value
if not isList(v):
return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(()))
# Write content-type / content-list pair
if isString(car(v)) and not isNil(cdr(v)) and isList(cadr(v)):
return result(e, r, 200, (("Content-type", car(v)),), cadr(v))
# Convert list of values to element values
ve = valuesToElements(v)
# Write an assoc result as a JSON value
if isList(car(ve)) and not isNil(car(ve)):
el = car(ve)
if isSymbol(car(el)) and car(el) == element and not isNil(cdr(el)) and isSymbol(cadr(el)):
if cadr(el) == "'feed":
return result(e, r, 200, (("Content-type", "application/atom+xml"),), writeATOMFeed(ve))
if cadr(el) == "'entry":
return result(e, r, 200, (("Content-type", "application/atom+xml"),), writeATOMEntry(ve))
# Write a JSON value
return result(e, r, 200, (("Content-type", "application/json"),), writeJSON(ve))
if m == "POST":
ct = requestContentType(e)
# Handle a JSON-RPC function call
if contains(ct, "application/json-rpc") or contains(ct, "text/plain") or contains(ct, "application/x-www-form-urlencoded"):
print >> stderr, "Handling JSON-RPC request"
json = elementsToValues(readJSON(requestBody(e)))
args = postArgs(json)
jid = cadr(assoc("'id", args))
func = funcName(cadr(assoc("'method", args)))
params = cadr(assoc("'params", args))
v = comp(func, *params)
return result(e, r, 200, (("Content-type", "application/json-rpc"),), jsonResult(jid, v))
# Handle an ATOM entry POST
if contains(ct, "application/atom+xml"):
ae = elementsToValues(readATOMEntry(requestBody(e)))
v = comp("post", id, ae)
if isNil(v):
return failure(e, r, 500)
return result(e, r, 201, (("Location", request_uri(e) + "/" + "/".join(v)),))
return failure(e, r, 500)
if m == "PUT":
# Handle an ATOM entry PUT
ae = elementsToValues(readATOMEntry(requestBody(e)))
v = comp("put", id, ae)
if v == False:
return failure(e, r, 404)
return result(e, r, 200)
if m == "DELETE":
v = comp("delete", id)
if v == False:
return failure(e, r, 404)
return result(e, r, 200)
return failure(e, r, 500)
# Return the WSGI server type
def serverType(e):
return e.get("SERVER_SOFTWARE", "")
def main():
# Read the deployed composite and evaluate the configured components
global comps
if comps == None:
domain = "domain.composite" if os.path.exists("domain.composite") else "domain-test.composite"
comps = evalComponents(components(parse(domain)))
# Handle the WSGI request with the WSGI runtime
st = serverType(environ)
if contains(st, "App Engine") or contains(st, "Development"):
from google.appengine.ext.webapp.util import run_wsgi_app
run_wsgi_app(application)
elif st != "":
CGIHandler().run(application)
else:
make_server("", int(argv[1]), application).serve_forever()
# Run the WSGI application
if __name__ == "__main__":
main()