summaryrefslogtreecommitdiffstats
path: root/openstack
diff options
context:
space:
mode:
Diffstat (limited to 'openstack')
-rw-r--r--openstack/common/__init__.py2
-rw-r--r--openstack/common/exception.py4
-rw-r--r--openstack/common/extensions.py530
-rw-r--r--openstack/common/wsgi.py502
4 files changed, 968 insertions, 70 deletions
diff --git a/openstack/common/__init__.py b/openstack/common/__init__.py
index b22496f..64da808 100644
--- a/openstack/common/__init__.py
+++ b/openstack/common/__init__.py
@@ -15,5 +15,5 @@
# License for the specific language governing permissions and limitations
# under the License.
-# TODO(jaypipes) Code in this module is intended to be ported to the eventual
+# TODO(jaypipes) Code in this module is intended to be ported to the eventual
# openstack-common library
diff --git a/openstack/common/exception.py b/openstack/common/exception.py
index a81355e..ba32da5 100644
--- a/openstack/common/exception.py
+++ b/openstack/common/exception.py
@@ -139,5 +139,9 @@ class OpenstackException(Exception):
return self._error_string
+class MalformedRequestBody(OpenstackException):
+ message = "Malformed message body: %(reason)s"
+
+
class InvalidContentType(OpenstackException):
message = "Invalid content type %(content_type)s"
diff --git a/openstack/common/extensions.py b/openstack/common/extensions.py
new file mode 100644
index 0000000..d2fab36
--- /dev/null
+++ b/openstack/common/extensions.py
@@ -0,0 +1,530 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Justin Santa Barbara
+# All Rights Reserved.
+#
+# Licensed 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.
+
+import imp
+import os
+import routes
+import webob.dec
+import webob.exc
+import logging
+from lxml import etree
+
+from openstack.common import exception
+from openstack.common import wsgi
+
+LOG = logging.getLogger('extensions')
+DEFAULT_XMLNS = "http://docs.openstack.org/"
+XMLNS_ATOM = "http://www.w3.org/2005/Atom"
+
+
+class ExtensionDescriptor(object):
+ """Base class that defines the contract for extensions.
+
+ Note that you don't have to derive from this class to have a valid
+ extension; it is purely a convenience.
+
+ """
+
+ def get_name(self):
+ """The name of the extension.
+
+ e.g. 'Fox In Socks'
+
+ """
+ raise NotImplementedError()
+
+ def get_alias(self):
+ """The alias for the extension.
+
+ e.g. 'FOXNSOX'
+
+ """
+ raise NotImplementedError()
+
+ def get_description(self):
+ """Friendly description for the extension.
+
+ e.g. 'The Fox In Socks Extension'
+
+ """
+ raise NotImplementedError()
+
+ def get_namespace(self):
+ """The XML namespace for the extension.
+
+ e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
+
+ """
+ raise NotImplementedError()
+
+ def get_updated(self):
+ """The timestamp when the extension was last updated.
+
+ e.g. '2011-01-22T13:25:27-06:00'
+
+ """
+ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
+ raise NotImplementedError()
+
+ def get_resources(self):
+ """List of extensions.ResourceExtension extension objects.
+
+ Resources define new nouns, and are accessible through URLs.
+
+ """
+ resources = []
+ return resources
+
+ def get_actions(self):
+ """List of extensions.ActionExtension extension objects.
+
+ Actions are verbs callable from the API.
+
+ """
+ actions = []
+ return actions
+
+ def get_request_extensions(self):
+ """List of extensions.RequestException extension objects.
+
+ Request extensions are used to handle custom request data.
+
+ """
+ request_exts = []
+ return request_exts
+
+
+class ActionExtensionController(object):
+ def __init__(self, application):
+ self.application = application
+ self.action_handlers = {}
+
+ def add_action(self, action_name, handler):
+ self.action_handlers[action_name] = handler
+
+ def action(self, req, id, body):
+ for action_name, handler in self.action_handlers.iteritems():
+ if action_name in body:
+ return handler(body, req, id)
+ # no action handler found (bump to downstream application)
+ res = self.application
+ return res
+
+
+class ActionExtensionResource(wsgi.Resource):
+
+ def __init__(self, application):
+ controller = ActionExtensionController(application)
+ wsgi.Resource.__init__(self, controller)
+
+ def add_action(self, action_name, handler):
+ self.controller.add_action(action_name, handler)
+
+
+class RequestExtensionController(object):
+
+ def __init__(self, application):
+ self.application = application
+ self.handlers = []
+
+ def add_handler(self, handler):
+ self.handlers.append(handler)
+
+ def process(self, req, *args, **kwargs):
+ res = req.get_response(self.application)
+ # currently request handlers are un-ordered
+ for handler in self.handlers:
+ res = handler(req, res)
+ return res
+
+
+class RequestExtensionResource(wsgi.Resource):
+
+ def __init__(self, application):
+ controller = RequestExtensionController(application)
+ wsgi.Resource.__init__(self, controller)
+
+ def add_handler(self, handler):
+ self.controller.add_handler(handler)
+
+
+class ExtensionsResource(wsgi.Resource):
+
+ def __init__(self, extension_manager):
+ self.extension_manager = extension_manager
+ body_serializers = {'application/xml': ExtensionsXMLSerializer()}
+ serializer = wsgi.ResponseSerializer(body_serializers=body_serializers)
+ super(ExtensionsResource, self).__init__(self, None, serializer)
+
+ def _translate(self, ext):
+ ext_data = {}
+ ext_data['name'] = ext.get_name()
+ ext_data['alias'] = ext.get_alias()
+ ext_data['description'] = ext.get_description()
+ ext_data['namespace'] = ext.get_namespace()
+ ext_data['updated'] = ext.get_updated()
+ ext_data['links'] = [] # TODO(dprince): implement extension links
+ return ext_data
+
+ def index(self, req):
+ extensions = []
+ for _alias, ext in self.extension_manager.extensions.iteritems():
+ extensions.append(self._translate(ext))
+ return dict(extensions=extensions)
+
+ def show(self, req, id):
+ # NOTE(dprince): the extensions alias is used as the 'id' for show
+ ext = self.extension_manager.extensions.get(id, None)
+ if not ext:
+ raise webob.exc.HTTPNotFound(
+ _("Extension with alias %s does not exist") % id)
+
+ return dict(extension=self._translate(ext))
+
+ def delete(self, req, id):
+ raise webob.exc.HTTPNotFound()
+
+ def create(self, req):
+ raise webob.exc.HTTPNotFound()
+
+
+class ExtensionMiddleware(wsgi.Middleware):
+ """Extensions middleware for WSGI."""
+
+ def _action_ext_resources(self, application, ext_mgr, mapper):
+ """Return a dict of ActionExtensionResource-s by collection."""
+ action_resources = {}
+ for action in ext_mgr.get_actions():
+ if not action.collection in action_resources.keys():
+ resource = ActionExtensionResource(application)
+ mapper.connect("/%s/:(id)/action.:(format)" %
+ action.collection,
+ action='action',
+ controller=resource,
+ conditions=dict(method=['POST']))
+ mapper.connect("/%s/:(id)/action" %
+ action.collection,
+ action='action',
+ controller=resource,
+ conditions=dict(method=['POST']))
+ action_resources[action.collection] = resource
+
+ return action_resources
+
+ def _request_ext_resources(self, application, ext_mgr, mapper):
+ """Returns a dict of RequestExtensionResource-s by collection."""
+ request_ext_resources = {}
+ for req_ext in ext_mgr.get_request_extensions():
+ if not req_ext.key in request_ext_resources.keys():
+ resource = RequestExtensionResource(application)
+ mapper.connect(req_ext.url_route + '.:(format)',
+ action='process',
+ controller=resource,
+ conditions=req_ext.conditions)
+
+ mapper.connect(req_ext.url_route,
+ action='process',
+ controller=resource,
+ conditions=req_ext.conditions)
+ request_ext_resources[req_ext.key] = resource
+
+ return request_ext_resources
+
+ def __init__(self, application, ext_mgr):
+ self.ext_mgr = ext_mgr
+
+ mapper = routes.Mapper()
+
+ # extended resources
+ for resource_ext in ext_mgr.get_resources():
+ LOG.debug(_('Extended resource: %s'), resource_ext.collection)
+ controller_resource = wsgi.Resource(resource_ext.controller,
+ resource_ext.deserializer,
+ resource_ext.serializer)
+ self._map_custom_collection_actions(resource_ext, mapper,
+ controller_resource)
+ kargs = dict(controller=controller_resource,
+ collection=resource_ext.collection_actions,
+ member=resource_ext.member_actions)
+ if resource_ext.parent:
+ kargs['parent_resource'] = resource_ext.parent
+ mapper.resource(resource_ext.collection,
+ resource_ext.collection, **kargs)
+
+ # extended actions
+ action_resources = self._action_ext_resources(application, ext_mgr,
+ mapper)
+ for action in ext_mgr.get_actions():
+ LOG.debug(_('Extended action: %s'), action.action_name)
+ resource = action_resources[action.collection]
+ resource.add_action(action.action_name, action.handler)
+
+ # extended requests
+ req_controllers = self._request_ext_resources(application, ext_mgr,
+ mapper)
+ for request_ext in ext_mgr.get_request_extensions():
+ LOG.debug(_('Extended request: %s'), request_ext.key)
+ controller = req_controllers[request_ext.key]
+ controller.add_handler(request_ext.handler)
+
+ self._router = routes.middleware.RoutesMiddleware(self._dispatch,
+ mapper)
+
+ super(ExtensionMiddleware, self).__init__(application)
+
+ def _map_custom_collection_actions(self, resource_ext, mapper,
+ controller_resource):
+ for action, method in resource_ext.collection_actions.iteritems():
+ parent = resource_ext.parent
+ conditions = dict(method=[method])
+ path = "/%s/%s" % (resource_ext.collection, action)
+
+ path_prefix = ""
+ if parent:
+ path_prefix = "/%s/{%s_id}" % (parent["collection_name"],
+ parent["member_name"])
+
+ with mapper.submapper(controller=controller_resource,
+ action=action,
+ path_prefix=path_prefix,
+ conditions=conditions) as submap:
+ submap.connect(path)
+ submap.connect("%s.:(format)" % path)
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """Route the incoming request with router."""
+ req.environ['extended.app'] = self.application
+ return self._router
+
+ @staticmethod
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def _dispatch(req):
+ """Dispatch the request.
+
+ Returns the routed WSGI app's response or defers to the extended
+ application.
+
+ """
+ match = req.environ['wsgiorg.routing_args'][1]
+ if not match:
+ return req.environ['extended.app']
+ app = match['controller']
+ return app
+
+
+class ExtensionManager(object):
+ """Load extensions from the configured extension path.
+
+ See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
+ example extension implementation.
+
+ """
+
+ def __init__(self, path):
+ LOG.debug(_('Initializing extension manager.'))
+
+ self.path = path
+ self.extensions = {}
+ self._load_all_extensions()
+
+ def get_resources(self):
+ """Returns a list of ResourceExtension objects."""
+ resources = []
+ extension_resource = ExtensionsResource(self)
+ res_ext = ResourceExtension('extensions',
+ extension_resource,
+ serializer=extension_resource.serializer)
+ resources.append(res_ext)
+ for alias, ext in self.extensions.iteritems():
+ try:
+ resources.extend(ext.get_resources())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have resource
+ # extensions
+ pass
+ return resources
+
+ def get_actions(self):
+ """Returns a list of ActionExtension objects."""
+ actions = []
+ for alias, ext in self.extensions.iteritems():
+ try:
+ actions.extend(ext.get_actions())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have action
+ # extensions
+ pass
+ return actions
+
+ def get_request_extensions(self):
+ """Returns a list of RequestExtension objects."""
+ request_exts = []
+ for alias, ext in self.extensions.iteritems():
+ try:
+ request_exts.extend(ext.get_request_extensions())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have request
+ # extensions
+ pass
+ return request_exts
+
+ def _check_extension(self, extension):
+ """Checks for required methods in extension objects."""
+ try:
+ LOG.debug(_('Ext name: %s'), extension.get_name())
+ LOG.debug(_('Ext alias: %s'), extension.get_alias())
+ LOG.debug(_('Ext description: %s'), extension.get_description())
+ LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
+ LOG.debug(_('Ext updated: %s'), extension.get_updated())
+ except AttributeError as ex:
+ LOG.exception(_("Exception loading extension: %s"), unicode(ex))
+ return False
+ return True
+
+ def _load_all_extensions(self):
+ """Load extensions from the configured path.
+
+ Load extensions from the configured path. The extension name is
+ constructed from the module_name. If your extension module was named
+ widgets.py the extension class within that module should be
+ 'Widgets'.
+
+ In addition, extensions are loaded from the 'contrib' directory.
+
+ See nova/tests/api/openstack/extensions/foxinsocks.py for an example
+ extension implementation.
+
+ """
+ if os.path.exists(self.path):
+ self._load_all_extensions_from_path(self.path)
+
+ contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
+ if os.path.exists(contrib_path):
+ self._load_all_extensions_from_path(contrib_path)
+
+ def _load_all_extensions_from_path(self, path):
+ for f in os.listdir(path):
+ LOG.debug(_('Loading extension file: %s'), f)
+ mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
+ ext_path = os.path.join(path, f)
+ if file_ext.lower() == '.py' and not mod_name.startswith('_'):
+ mod = imp.load_source(mod_name, ext_path)
+ ext_name = mod_name[0].upper() + mod_name[1:]
+ new_ext_class = getattr(mod, ext_name, None)
+ if not new_ext_class:
+ LOG.warn(_('Did not find expected name '
+ '"%(ext_name)s" in %(file)s'),
+ {'ext_name': ext_name,
+ 'file': ext_path})
+ continue
+ new_ext = new_ext_class()
+ self.add_extension(new_ext)
+
+ def add_extension(self, ext):
+ # Do nothing if the extension doesn't check out
+ if not self._check_extension(ext):
+ return
+
+ alias = ext.get_alias()
+ LOG.debug(_('Loaded extension: %s'), alias)
+
+ if alias in self.extensions:
+ raise exception.Error("Found duplicate extension: %s" % alias)
+ self.extensions[alias] = ext
+
+
+class RequestExtension(object):
+ """Extend requests and responses of core nova OpenStack API resources.
+
+ Provide a way to add data to responses and handle custom request data
+ that is sent to core nova OpenStack API controllers.
+
+ """
+ def __init__(self, method, url_route, handler):
+ self.url_route = url_route
+ self.handler = handler
+ self.conditions = dict(method=[method])
+ self.key = "%s-%s" % (method, url_route)
+
+
+class ActionExtension(object):
+ """Add custom actions to core nova OpenStack API resources."""
+
+ def __init__(self, collection, action_name, handler):
+ self.collection = collection
+ self.action_name = action_name
+ self.handler = handler
+
+
+class ResourceExtension(object):
+ """Add top level resources to the OpenStack API in nova."""
+
+ def __init__(self, collection, controller, parent=None,
+ collection_actions=None, member_actions=None,
+ deserializer=None, serializer=None):
+ if not collection_actions:
+ collection_actions = {}
+ if not member_actions:
+ member_actions = {}
+ self.collection = collection
+ self.controller = controller
+ self.parent = parent
+ self.collection_actions = collection_actions
+ self.member_actions = member_actions
+ self.deserializer = deserializer
+ self.serializer = serializer
+
+
+class ExtensionsXMLSerializer(wsgi.XMLDictSerializer):
+
+ NSMAP = {None: DEFAULT_XMLNS, 'atom': XMLNS_ATOM}
+
+ def show(self, ext_dict):
+ ext = etree.Element('extension', nsmap=self.NSMAP)
+ self._populate_ext(ext, ext_dict['extension'])
+ return self._to_xml(ext)
+
+ def index(self, exts_dict):
+ exts = etree.Element('extensions', nsmap=self.NSMAP)
+ for ext_dict in exts_dict['extensions']:
+ ext = etree.SubElement(exts, 'extension')
+ self._populate_ext(ext, ext_dict)
+ return self._to_xml(exts)
+
+ def _populate_ext(self, ext_elem, ext_dict):
+ """Populate an extension xml element from a dict."""
+
+ ext_elem.set('name', ext_dict['name'])
+ ext_elem.set('namespace', ext_dict['namespace'])
+ ext_elem.set('alias', ext_dict['alias'])
+ ext_elem.set('updated', ext_dict['updated'])
+ desc = etree.Element('description')
+ desc.text = ext_dict['description']
+ ext_elem.append(desc)
+ for link in ext_dict.get('links', []):
+ elem = etree.SubElement(ext_elem, '{%s}link' % XMLNS_ATOM)
+ elem.set('rel', link['rel'])
+ elem.set('href', link['href'])
+ elem.set('type', link['type'])
+ return ext_elem
+
+ def _to_xml(self, root):
+ """Convert the xml object to an xml string."""
+
+ return etree.tostring(root, encoding='UTF-8')
diff --git a/openstack/common/wsgi.py b/openstack/common/wsgi.py
index 8faa6dc..128ae8c 100644
--- a/openstack/common/wsgi.py
+++ b/openstack/common/wsgi.py
@@ -15,26 +15,30 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""
-Utility methods for working with WSGI servers
-"""
+"""Utility methods for working with WSGI servers."""
-import json
-import logging
-import sys
import datetime
-
import eventlet
import eventlet.wsgi
+
eventlet.patcher.monkey_patch(all=False, socket=True)
+
+import json
+import logging
+import sys
import routes
import routes.middleware
import webob.dec
import webob.exc
+from xml.dom import minidom
+from xml.parsers import expat
from openstack.common import exception
+LOG = logging.getLogger('wsgi')
+
+
class WritableLogger(object):
"""A thin wrapper that responds to `write` and logs."""
@@ -206,65 +210,47 @@ class Router(object):
class Request(webob.Request):
-
"""Add some Openstack API-specific logic to the base webob.Request."""
- def best_match_content_type(self):
- """Determine the requested response content-type."""
- supported = ('application/json',)
- bm = self.accept.best_match(supported)
- return bm or 'application/json'
-
- def get_content_type(self, allowed_content_types):
- """Determine content type of the request body."""
- if not "Content-Type" in self.headers:
- raise exception.InvalidContentType(content_type=None)
-
- content_type = self.content_type
-
- if content_type not in allowed_content_types:
- raise exception.InvalidContentType(content_type=content_type)
- else:
- return content_type
+ default_request_content_types = ('application/json', 'application/xml')
+ default_accept_types = ('application/json', 'application/xml')
+ default_accept_type = 'application/json'
+ def best_match_content_type(self, supported_content_types=None):
+ """Determine the requested response content-type.
-class JSONRequestDeserializer(object):
- def has_body(self, request):
- """
- Returns whether a Webob.Request object will possess an entity body.
+ Based on the query extension then the Accept header.
+ Defaults to default_accept_type if we don't find a preference
- :param request: Webob.Request object
"""
- if 'transfer-encoding' in request.headers:
- return True
- elif request.content_length > 0:
- return True
-
- return False
+ supported_content_types = (supported_content_types or
+ self.default_accept_types)
- def from_json(self, datastring):
- return json.loads(datastring)
+ parts = self.path.rsplit('.', 1)
+ if len(parts) > 1:
+ ctype = 'application/{0}'.format(parts[1])
+ if ctype in supported_content_types:
+ return ctype
- def default(self, request):
- if self.has_body(request):
- return {'body': self.from_json(request.body)}
- else:
- return {}
+ bm = self.accept.best_match(supported_content_types)
+ return bm or self.default_accept_type
+ def get_content_type(self, allowed_content_types=None):
+ """Determine content type of the request body.
-class JSONResponseSerializer(object):
+ Does not do any body introspection, only checks header
- def to_json(self, data):
- def sanitizer(obj):
- if isinstance(obj, datetime.datetime):
- return obj.isoformat()
- return obj
+ """
+ if not "Content-Type" in self.headers:
+ return None
- return json.dumps(data, default=sanitizer)
+ content_type = self.content_type
+ allowed_content_types = (allowed_content_types or
+ self.default_request_content_types)
- def default(self, response, result):
- response.headers.add('Content-Type', 'application/json')
- response.body = self.to_json(result)
+ if content_type not in allowed_content_types:
+ raise exception.InvalidContentType(content_type=content_type)
+ return content_type
class Resource(object):
@@ -284,7 +270,7 @@ class Resource(object):
may raise a webob.exc exception or return a dict, which will be
serialized by requested content type.
"""
- def __init__(self, controller, deserializer, serializer):
+ def __init__(self, controller, deserializer=None, serializer=None):
"""
:param controller: object that implement methods created by routes lib
:param deserializer: object that supports webob request deserialization
@@ -293,33 +279,34 @@ class Resource(object):
through controller-like actions
"""
self.controller = controller
- self.serializer = serializer
- self.deserializer = deserializer
+ self.serializer = serializer or ResponseSerializer()
+ self.deserializer = deserializer or RequestDeserializer()
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, request):
"""WSGI method that controls (de)serialization and method dispatch."""
- action_args = self.get_action_args(request.environ)
- action = action_args.pop('action', None)
- deserialized_params = self.deserialize_request(action, request)
- action_args.update(deserialized_params)
- action_result = self.execute_action(action, request, **action_args)
try:
- return self.serialize_response(action, action_result, request)
+ action, action_args, accept = self.deserialize_request(request)
+ except exception.InvalidContentType:
+ msg = _("Unsupported Content-Type")
+ return webob.exc.HTTPUnsupportedMediaType(explanation=msg)
+ except exception.MalformedRequestBody:
+ msg = _("Malformed request body")
+ return webob.exc.HTTPBadRequest(explanation=msg)
+ action_result = self.execute_action(action, request, **action_args)
+ try:
+ return self.serialize_response(action, action_result, accept)
# return unserializable result (typically a webob exc)
except Exception:
return action_result
- def deserialize_request(self, action, request):
- return self.dispatch(self.deserializer, action, request)
+ def deserialize_request(self, request):
+ return self.deserializer.deserialize(request)
- def serialize_response(self, action, action_result, request):
- response = webob.Response()
- self.dispatch(self.serializer, action, response,
- action_result, request)
- return response
+ def serialize_response(self, action, action_result, accept):
+ return self.serializer.serialize(action_result, accept, action)
def execute_action(self, action, request, **action_args):
return self.dispatch(self.controller, action, request, **action_args)
@@ -351,3 +338,380 @@ class Resource(object):
pass
return args
+
+
+class ActionDispatcher(object):
+ """Maps method name to local methods through action name."""
+
+ def dispatch(self, *args, **kwargs):
+ """Find and call local method."""
+ action = kwargs.pop('action', 'default')
+ action_method = getattr(self, str(action), self.default)
+ return action_method(*args, **kwargs)
+
+ def default(self, data):
+ raise NotImplementedError()
+
+
+class DictSerializer(ActionDispatcher):
+ """Default request body serialization"""
+
+ def serialize(self, data, action='default'):
+ return self.dispatch(data, action=action)
+
+ def default(self, data):
+ return ""
+
+
+class JSONDictSerializer(DictSerializer):
+ """Default JSON request body serialization"""
+
+ def default(self, data):
+ def sanitizer(obj):
+ if isinstance(obj, datetime.datetime):
+ _dtime = obj - datetime.timedelta(microseconds=obj.microsecond)
+ return _dtime.isoformat()
+ return obj
+ return json.dumps(data, default=sanitizer)
+
+
+class XMLDictSerializer(DictSerializer):
+
+ def __init__(self, metadata=None, xmlns=None):
+ """
+ :param metadata: information needed to deserialize xml into
+ a dictionary.
+ :param xmlns: XML namespace to include with serialized xml
+ """
+ super(XMLDictSerializer, self).__init__()
+ self.metadata = metadata or {}
+ self.xmlns = xmlns
+
+ def default(self, data):
+ # We expect data to contain a single key which is the XML root.
+ root_key = data.keys()[0]
+ doc = minidom.Document()
+ node = self._to_xml_node(doc, self.metadata, root_key, data[root_key])
+
+ return self.to_xml_string(node)
+
+ def to_xml_string(self, node, has_atom=False):
+ self._add_xmlns(node, has_atom)
+ return node.toprettyxml(indent=' ', encoding='UTF-8')
+
+ #NOTE (ameade): the has_atom should be removed after all of the
+ # xml serializers and view builders have been updated to the current
+ # spec that required all responses include the xmlns:atom, the has_atom
+ # flag is to prevent current tests from breaking
+ def _add_xmlns(self, node, has_atom=False):
+ if self.xmlns is not None:
+ node.setAttribute('xmlns', self.xmlns)
+ if has_atom:
+ node.setAttribute('xmlns:atom', "http://www.w3.org/2005/Atom")
+
+ def _to_xml_node(self, doc, metadata, nodename, data):
+ """Recursive method to convert data members to XML nodes."""
+ result = doc.createElement(nodename)
+
+ # Set the xml namespace if one is specified
+ # TODO(justinsb): We could also use prefixes on the keys
+ xmlns = metadata.get('xmlns', None)
+ if xmlns:
+ result.setAttribute('xmlns', xmlns)
+
+ #TODO(bcwaldon): accomplish this without a type-check
+ if type(data) is list:
+ collections = metadata.get('list_collections', {})
+ if nodename in collections:
+ metadata = collections[nodename]
+ for item in data:
+ node = doc.createElement(metadata['item_name'])
+ node.setAttribute(metadata['item_key'], str(item))
+ result.appendChild(node)
+ return result
+ singular = metadata.get('plurals', {}).get(nodename, None)
+ if singular is None:
+ if nodename.endswith('s'):
+ singular = nodename[:-1]
+ else:
+ singular = 'item'
+ for item in data:
+ node = self._to_xml_node(doc, metadata, singular, item)
+ result.appendChild(node)
+ #TODO(bcwaldon): accomplish this without a type-check
+ elif type(data) is dict:
+ collections = metadata.get('dict_collections', {})
+ if nodename in collections:
+ metadata = collections[nodename]
+ for k, v in data.items():
+ node = doc.createElement(metadata['item_name'])
+ node.setAttribute(metadata['item_key'], str(k))
+ text = doc.createTextNode(str(v))
+ node.appendChild(text)
+ result.appendChild(node)
+ return result
+ attrs = metadata.get('attributes', {}).get(nodename, {})
+ for k, v in data.items():
+ if k in attrs:
+ result.setAttribute(k, str(v))
+ else:
+ node = self._to_xml_node(doc, metadata, k, v)
+ result.appendChild(node)
+ else:
+ # Type is atom
+ node = doc.createTextNode(str(data))
+ result.appendChild(node)
+ return result
+
+ def _create_link_nodes(self, xml_doc, links):
+ link_nodes = []
+ for link in links:
+ link_node = xml_doc.createElement('atom:link')
+ link_node.setAttribute('rel', link['rel'])
+ link_node.setAttribute('href', link['href'])
+ if 'type' in link:
+ link_node.setAttribute('type', link['type'])
+ link_nodes.append(link_node)
+ return link_nodes
+
+
+class ResponseHeadersSerializer(ActionDispatcher):
+ """Default response headers serialization"""
+
+ def serialize(self, response, data, action):
+ self.dispatch(response, data, action=action)
+
+ def default(self, response, data):
+ response.status_int = 200
+
+
+class ResponseSerializer(object):
+ """Encode the necessary pieces into a response object"""
+
+ def __init__(self, body_serializers=None, headers_serializer=None):
+ self.body_serializers = {
+ 'application/xml': XMLDictSerializer(),
+ 'application/json': JSONDictSerializer(),
+ }
+ self.body_serializers.update(body_serializers or {})
+
+ self.headers_serializer = headers_serializer or \
+ ResponseHeadersSerializer()
+
+ def serialize(self, response_data, content_type, action='default'):
+ """Serialize a dict into a string and wrap in a wsgi.Request object.
+
+ :param response_data: dict produced by the Controller
+ :param content_type: expected mimetype of serialized response body
+
+ """
+ response = webob.Response()
+ self.serialize_headers(response, response_data, action)
+ self.serialize_body(response, response_data, content_type, action)
+ return response
+
+ def serialize_headers(self, response, data, action):
+ self.headers_serializer.serialize(response, data, action)
+
+ def serialize_body(self, response, data, content_type, action):
+ response.headers['Content-Type'] = content_type
+ if data is not None:
+ serializer = self.get_body_serializer(content_type)
+ response.body = serializer.serialize(data, action)
+
+ def get_body_serializer(self, content_type):
+ try:
+ return self.body_serializers[content_type]
+ except (KeyError, TypeError):
+ raise exception.InvalidContentType(content_type=content_type)
+
+
+class RequestHeadersDeserializer(ActionDispatcher):
+ """Default request headers deserializer"""
+
+ def deserialize(self, request, action):
+ return self.dispatch(request, action=action)
+
+ def default(self, request):
+ return {}
+
+
+class RequestDeserializer(object):
+ """Break up a Request object into more useful pieces."""
+
+ def __init__(self, body_deserializers=None, headers_deserializer=None,
+ supported_content_types=None):
+
+ self.supported_content_types = supported_content_types
+
+ self.body_deserializers = {
+ 'application/xml': XMLDeserializer(),
+ 'application/json': JSONDeserializer(),
+ }
+ self.body_deserializers.update(body_deserializers or {})
+
+ self.headers_deserializer = headers_deserializer or \
+ RequestHeadersDeserializer()
+
+ def deserialize(self, request):
+ """Extract necessary pieces of the request.
+
+ :param request: Request object
+ :returns tuple of expected controller action name, dictionary of
+ keyword arguments to pass to the controller, the expected
+ content type of the response
+
+ """
+ action_args = self.get_action_args(request.environ)
+ action = action_args.pop('action', None)
+
+ action_args.update(self.deserialize_headers(request, action))
+ action_args.update(self.deserialize_body(request, action))
+
+ accept = self.get_expected_content_type(request)
+
+ return (action, action_args, accept)
+
+ def deserialize_headers(self, request, action):
+ return self.headers_deserializer.deserialize(request, action)
+
+ def deserialize_body(self, request, action):
+ if not len(request.body) > 0:
+ LOG.debug(_("Empty body provided in request"))
+ return {}
+
+ try:
+ content_type = request.get_content_type()
+ except exception.InvalidContentType:
+ LOG.debug(_("Unrecognized Content-Type provided in request"))
+ raise
+
+ if content_type is None:
+ LOG.debug(_("No Content-Type provided in request"))
+ return {}
+
+ try:
+ deserializer = self.get_body_deserializer(content_type)
+ except exception.InvalidContentType:
+ LOG.debug(_("Unable to deserialize body as provided Content-Type"))
+ raise
+
+ return deserializer.deserialize(request.body, action)
+
+ def get_body_deserializer(self, content_type):
+ try:
+ return self.body_deserializers[content_type]
+ except (KeyError, TypeError):
+ raise exception.InvalidContentType(content_type=content_type)
+
+ def get_expected_content_type(self, request):
+ return request.best_match_content_type(self.supported_content_types)
+
+ def get_action_args(self, request_environment):
+ """Parse dictionary created by routes library."""
+ try:
+ args = request_environment['wsgiorg.routing_args'][1].copy()
+ except Exception:
+ return {}
+
+ try:
+ del args['controller']
+ except KeyError:
+ pass
+
+ try:
+ del args['format']
+ except KeyError:
+ pass
+
+ return args
+
+
+class TextDeserializer(ActionDispatcher):
+ """Default request body deserialization"""
+
+ def deserialize(self, datastring, action='default'):
+ return self.dispatch(datastring, action=action)
+
+ def default(self, datastring):
+ return {}
+
+
+class JSONDeserializer(TextDeserializer):
+
+ def _from_json(self, datastring):
+ try:
+ return json.loads(datastring)
+ except ValueError:
+ msg = _("cannot understand JSON")
+ raise exception.MalformedRequestBody(reason=msg)
+
+ def default(self, datastring):
+ return {'body': self._from_json(datastring)}
+
+
+class XMLDeserializer(TextDeserializer):
+
+ def __init__(self, metadata=None):
+ """
+ :param metadata: information needed to deserialize xml into
+ a dictionary.
+ """
+ super(XMLDeserializer, self).__init__()
+ self.metadata = metadata or {}
+
+ def _from_xml(self, datastring):
+ plurals = set(self.metadata.get('plurals', {}))
+
+ try:
+ node = minidom.parseString(datastring).childNodes[0]
+ return {node.nodeName: self._from_xml_node(node, plurals)}
+ except expat.ExpatError:
+ msg = _("cannot understand XML")
+ raise exception.MalformedRequestBody(reason=msg)
+
+ def _from_xml_node(self, node, listnames):
+ """Convert a minidom node to a simple Python type.
+
+ :param listnames: list of XML node names whose subnodes should
+ be considered list items.
+
+ """
+
+ if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3:
+ return node.childNodes[0].nodeValue
+ elif node.nodeName in listnames:
+ return [self._from_xml_node(n, listnames) for n in node.childNodes]
+ else:
+ result = dict()
+ for attr in node.attributes.keys():
+ result[attr] = node.attributes[attr].nodeValue
+ for child in node.childNodes:
+ if child.nodeType != node.TEXT_NODE:
+ result[child.nodeName] = self._from_xml_node(child,
+ listnames)
+ return result
+
+ def find_first_child_named(self, parent, name):
+ """Search a nodes children for the first child with a given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ return node
+ return None
+
+ def find_children_named(self, parent, name):
+ """Return all of a nodes children who have the given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ yield node
+
+ def extract_text(self, node):
+ """Get the text field contained by the given node"""
+ if len(node.childNodes) == 1:
+ child = node.childNodes[0]
+ if child.nodeType == child.TEXT_NODE:
+ return child.nodeValue
+ return ""
+
+ def default(self, datastring):
+ return {'body': self._from_xml(datastring)}