removing controller/serializer code from wsgi.py; updating other code to use new modules

This commit is contained in:
Brian Waldon 2011-05-18 20:33:25 -04:00
parent 5e722ea7b9
commit cfd58f5d58
15 changed files with 147 additions and 427 deletions

View File

@ -42,6 +42,7 @@ from nova import exception
from nova import flags
from nova import utils
from nova import wsgi
import nova.api.openstack.wsgi
# Global storage for registering modules.
@ -251,7 +252,7 @@ class Reflection(object):
return self._methods[method]
class ServiceWrapper(wsgi.Controller):
class ServiceWrapper(object):
"""Wrapper to dynamically povide a WSGI controller for arbitrary objects.
With lightweight introspection allows public methods on the object to
@ -265,7 +266,7 @@ class ServiceWrapper(wsgi.Controller):
def __init__(self, service_handle):
self.service_handle = service_handle
@webob.dec.wsgify(RequestClass=wsgi.Request)
@webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request)
def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
@ -289,8 +290,11 @@ class ServiceWrapper(wsgi.Controller):
try:
content_type = req.best_match_content_type()
default_xmlns = self.get_default_xmlns(req)
return self._serialize(result, content_type, default_xmlns)
serializer = {
'application/xml': nova.api.openstack.wsgi.XMLSerializer(),
'application/json': nova.api.openstack.wsgi.JSONSerializer(),
}[content_type]
return serializer.serialize(result)
except:
raise exception.Error("returned non-serializable type: %s"
% result)

View File

@ -23,7 +23,6 @@ import webob
from nova import exception
from nova import flags
from nova import log as logging
from nova import wsgi
LOG = logging.getLogger('nova.api.openstack.common')
@ -146,9 +145,3 @@ def get_id_from_href(href):
except:
LOG.debug(_("Error extracting id from href: %s") % href)
raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
class OpenstackController(wsgi.Controller):
def get_default_xmlns(self, req):
# Use V10 by default
return XML_NS_V10

View File

@ -44,11 +44,10 @@ def _translate_detail_keys(cons):
class Controller(object):
"""The Consoles Controller for the Openstack API"""
"""The Consoles controller for the Openstack API"""
def __init__(self):
self.console_api = console.API()
super(Controller, self).__init__()
def index(self, req, server_id):
"""Returns a list of consoles for this instance"""

View File

@ -22,7 +22,6 @@ from nova import exception
from nova import flags
from nova import log as logging
from nova import volume
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import extensions
from nova.api.openstack import faults
@ -64,7 +63,7 @@ def _translate_volume_summary_view(context, vol):
return d
class VolumeController(wsgi.Controller):
class VolumeController(object):
"""The Volumes API controller for the OpenStack API."""
_serialization_metadata = {
@ -124,15 +123,14 @@ class VolumeController(wsgi.Controller):
res = [entity_maker(context, vol) for vol in limited_list]
return {'volumes': res}
def create(self, req):
def create(self, req, body):
"""Creates a new volume."""
context = req.environ['nova.context']
env = self._deserialize(req.body, req.get_content_type())
if not env:
if not body:
return faults.Fault(exc.HTTPUnprocessableEntity())
vol = env['volume']
vol = body['volume']
size = vol['size']
LOG.audit(_("Create volume of %s GB"), size, context=context)
new_volume = self.volume_api.create(context, size,
@ -175,7 +173,7 @@ def _translate_attachment_summary_view(_context, vol):
return d
class VolumeAttachmentController(wsgi.Controller):
class VolumeAttachmentController(object):
"""The volume attachment API controller for the Openstack API.
A child resource of the server. Note that we use the volume id
@ -219,17 +217,16 @@ class VolumeAttachmentController(wsgi.Controller):
return {'volumeAttachment': _translate_attachment_detail_view(context,
vol)}
def create(self, req, server_id):
def create(self, req, server_id, body):
"""Attach a volume to an instance."""
context = req.environ['nova.context']
env = self._deserialize(req.body, req.get_content_type())
if not env:
if not body:
return faults.Fault(exc.HTTPUnprocessableEntity())
instance_id = server_id
volume_id = env['volumeAttachment']['volumeId']
device = env['volumeAttachment']['device']
volume_id = body['volumeAttachment']['volumeId']
device = body['volumeAttachment']['device']
msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
" at %(device)s") % locals()
@ -259,7 +256,7 @@ class VolumeAttachmentController(wsgi.Controller):
# TODO(justinsb): How do I return "accepted" here?
return {'volumeAttachment': attachment}
def update(self, _req, _server_id, _id):
def update(self, req, server_id, id, body):
"""Update a volume attachment. We don't currently support this."""
return faults.Fault(exc.HTTPBadRequest())

View File

@ -27,9 +27,10 @@ import webob.exc
from nova import exception
from nova import flags
from nova import log as logging
from nova import wsgi
from nova import wsgi as base_wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack import wsgi
LOG = logging.getLogger('extensions')
@ -116,28 +117,34 @@ class ExtensionDescriptor(object):
return response_exts
class ActionExtensionController(common.OpenstackController):
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):
input_dict = self._deserialize(req.body, req.get_content_type())
def action(self, req, id, body):
for action_name, handler in self.action_handlers.iteritems():
if action_name in input_dict:
return handler(input_dict, req, id)
if action_name in body:
return handler(body, req, id)
# no action handler found (bump to downstream application)
res = self.application
return res
class ResponseExtensionController(common.OpenstackController):
class ActionExtensionResource(wsgi.Resource):
def __init__(self, application):
controller = ActionExtensionController(application)
super(ActionExtensionResource, self).__init__(controller)
def add_action(self, action_name, handler):
self.controller.add_action(action_name, handler)
class ResponseExtensionController(object):
def __init__(self, application):
self.application = application
@ -157,7 +164,11 @@ class ResponseExtensionController(common.OpenstackController):
headers = res.headers
except AttributeError:
default_xmlns = None
body = self._serialize(res, content_type, default_xmlns)
serializer = {
'application/xml': wsgi.XMLSerializer(),
'application/json': wsgi.JSONSerializer(),
}[content_type]
body = serializer.serialize(res)
headers = {"Content-Type": content_type}
res = webob.Response()
res.body = body
@ -165,7 +176,17 @@ class ResponseExtensionController(common.OpenstackController):
return res
class ExtensionController(common.OpenstackController):
class ResponseExtensionResource(wsgi.Resource):
def __init__(self, application):
controller = ResponseExtensionController(application)
super(ResponseExtensionResource, self).__init__(controller)
def add_handler(self, handler):
self.controller.add_handler(handler)
class ExtensionController(object):
def __init__(self, extension_manager):
self.extension_manager = extension_manager
@ -198,7 +219,7 @@ class ExtensionController(common.OpenstackController):
raise faults.Fault(webob.exc.HTTPNotFound())
class ExtensionMiddleware(wsgi.Middleware):
class ExtensionMiddleware(base_wsgi.Middleware):
"""Extensions middleware for WSGI."""
@classmethod
def factory(cls, global_config, **local_config):
@ -207,43 +228,43 @@ class ExtensionMiddleware(wsgi.Middleware):
return cls(app, **local_config)
return _factory
def _action_ext_controllers(self, application, ext_mgr, mapper):
"""Return a dict of ActionExtensionController-s by collection."""
action_controllers = {}
def _action_ext_resources(self, application, ext_mgr, mapper):
"""Return a dict of ActionExtensionResource objects by collection."""
action_resources = {}
for action in ext_mgr.get_actions():
if not action.collection in action_controllers.keys():
controller = ActionExtensionController(application)
if not action.collection in action_resources.keys():
resource = ActionExtensionResource(application)
mapper.connect("/%s/:(id)/action.:(format)" %
action.collection,
action='action',
controller=controller,
controller=resource,
conditions=dict(method=['POST']))
mapper.connect("/%s/:(id)/action" % action.collection,
action='action',
controller=controller,
controller=resource,
conditions=dict(method=['POST']))
action_controllers[action.collection] = controller
action_resources[action.collection] = resource
return action_controllers
return action_resources
def _response_ext_controllers(self, application, ext_mgr, mapper):
"""Returns a dict of ResponseExtensionController-s by collection."""
response_ext_controllers = {}
def _response_ext_resources(self, application, ext_mgr, mapper):
"""Returns a dict of ResponseExtensionResource objects by collection."""
response_ext_resources = {}
for resp_ext in ext_mgr.get_response_extensions():
if not resp_ext.key in response_ext_controllers.keys():
controller = ResponseExtensionController(application)
if not resp_ext.key in response_ext_resources.keys():
resource = ResponseExtensionResource(application)
mapper.connect(resp_ext.url_route + '.:(format)',
action='process',
controller=controller,
controller=resource,
conditions=resp_ext.conditions)
mapper.connect(resp_ext.url_route,
action='process',
controller=controller,
controller=resource,
conditions=resp_ext.conditions)
response_ext_controllers[resp_ext.key] = controller
response_ext_resources[resp_ext.key] = resource
return response_ext_controllers
return response_ext_resources
def __init__(self, application, ext_mgr=None):
@ -258,21 +279,21 @@ class ExtensionMiddleware(wsgi.Middleware):
LOG.debug(_('Extended resource: %s'),
resource.collection)
mapper.resource(resource.collection, resource.collection,
controller=resource.controller,
controller=wsgi.Resource(resource.controller),
collection=resource.collection_actions,
member=resource.member_actions,
parent_resource=resource.parent)
# extended actions
action_controllers = self._action_ext_controllers(application, ext_mgr,
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)
controller = action_controllers[action.collection]
controller.add_action(action.action_name, action.handler)
resource = action_resources[action.collection]
resource.add_action(action.action_name, action.handler)
# extended responses
resp_controllers = self._response_ext_controllers(application, ext_mgr,
resp_controllers = self._response_ext_resources(application, ext_mgr,
mapper)
for response_ext in ext_mgr.get_response_extensions():
LOG.debug(_('Extended response: %s'), response_ext.key)
@ -422,7 +443,7 @@ class ExtensionManager(object):
class ResponseExtension(object):
"""Add data to responses from core nova OpenStack API controllers."""
"""Add data to responses from core nova OpenStack API resources."""
def __init__(self, method, url_route, handler):
self.url_route = url_route
@ -432,7 +453,7 @@ class ResponseExtension(object):
class ActionExtension(object):
"""Add custom actions to core nova OpenStack API controllers."""
"""Add custom actions to core nova OpenStack API resources."""
def __init__(self, collection, action_name, handler):
self.collection = collection

View File

@ -19,8 +19,7 @@
import webob.dec
import webob.exc
from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import wsgi
class Fault(webob.exc.HTTPException):
@ -55,13 +54,21 @@ class Fault(webob.exc.HTTPException):
if code == 413:
retry = self.wrapped_exc.headers['Retry-After']
fault_data[fault_name]['retryAfter'] = retry
# 'code' is an attribute on the fault tag itself
metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
default_xmlns = common.XML_NS_V10
serializer = wsgi.Serializer(metadata, default_xmlns)
metadata = {'attributes': {fault_name: 'code'}}
content_type = req.best_match_content_type()
self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
serializer = {
'application/xml': wsgi.XMLSerializer(metadata=metadata,
xmlns=wsgi.XMLNS_V10),
'application/json': wsgi.JSONSerializer(),
}[content_type]
self.wrapped_exc.body = serializer.serialize(fault_data)
self.wrapped_exc.content_type = content_type
return self.wrapped_exc
@ -70,14 +77,6 @@ class OverLimitFault(webob.exc.HTTPException):
Rate-limited request response.
"""
_serialization_metadata = {
"application/xml": {
"attributes": {
"overLimitFault": "code",
},
},
}
def __init__(self, message, details, retry_time):
"""
Initialize new `OverLimitFault` with relevant information.
@ -97,8 +96,16 @@ class OverLimitFault(webob.exc.HTTPException):
Return the wrapped exception with a serialized body conforming to our
error format.
"""
serializer = wsgi.Serializer(self._serialization_metadata)
content_type = request.best_match_content_type()
content = serializer.serialize(self.content, content_type)
metadata = {"attributes": {"overLimitFault": "code"}}
serializer = {
'application/xml': wsgi.XMLSerializer(metadata=metadata,
xmlns=wsgi.XMLNS_V10),
'application/json': wsgi.JSONSerializer(),
}[content_type]
content = serializer.serialize(self.content)
self.wrapped_exc.body = content
return self.wrapped_exc

View File

@ -20,7 +20,6 @@ from webob import exc
from nova import flags
from nova import quota
from nova import utils
from nova import wsgi
from nova.api.openstack import faults
from nova.api.openstack import wsgi

View File

@ -31,7 +31,7 @@ FLAGS = flags.FLAGS
class Controller(object):
"""Base `wsgi.Controller` for retrieving/displaying images."""
"""Base controller for retrieving/displaying images."""
def __init__(self, image_service=None, compute_service=None):
"""Initialize new `ImageController`.
@ -99,21 +99,20 @@ class Controller(object):
self._image_service.delete(context, image_id)
return webob.exc.HTTPNoContent()
def create(self, req):
def create(self, req, body):
"""Snapshot a server instance and save the image.
:param req: `wsgi.Request` object
"""
context = req.environ['nova.context']
content_type = req.get_content_type()
image = self._deserialize(req.body, content_type)
if not image:
if not body:
raise webob.exc.HTTPBadRequest()
try:
server_id = image["image"]["serverId"]
image_name = image["image"]["name"]
server_id = body["image"]["serverId"]
image_name = body["image"]["name"]
except KeyError:
raise webob.exc.HTTPBadRequest()

View File

@ -18,13 +18,27 @@
import webob
import webob.dec
from nova import wsgi
from nova import wsgi as base_wsgi
import nova.api.openstack.views.versions
from nova.api.openstack import wsgi
class Versions(wsgi.Application):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
class Versions(wsgi.Resource, base_wsgi.Application):
def __init__(self):
metadata = {
"attributes": {
"version": ["status", "id"],
"link": ["rel", "href"],
}
}
serializers = {
'application/xml': wsgi.XMLSerializer(metadata=metadata),
}
super(Versions, self).__init__(None, serializers=serializers)
def dispatch(self, request, *args):
"""Respond to a request for all OpenStack API versions."""
version_objs = [
{
@ -37,24 +51,6 @@ class Versions(wsgi.Application):
},
]
builder = nova.api.openstack.views.versions.get_view_builder(req)
builder = nova.api.openstack.views.versions.get_view_builder(request)
versions = [builder.build(version) for version in version_objs]
response = dict(versions=versions)
metadata = {
"application/xml": {
"attributes": {
"version": ["status", "id"],
"link": ["rel", "href"],
}
}
}
content_type = req.best_match_content_type()
body = wsgi.Serializer(metadata).serialize(response, content_type)
response = webob.Response()
response.content_type = content_type
response.body = body
return response
return dict(versions=versions)

View File

@ -206,8 +206,7 @@ class Resource(object):
except exception.InvalidContentType:
return webob.exc.HTTPBadRequest(_("Unsupported Content-Type"))
controller_method = getattr(self.controller, action)
result = controller_method(req=request, **action_args)
result = self.dispatch(request, action, action_args)
response = self.serialize_response(accept, result)
@ -222,6 +221,10 @@ class Resource(object):
return response
def dispatch(self, request, action, action_args):
controller_method = getattr(self.controller, action)
return controller_method(req=request, **action_args)
def serialize_response(self, content_type, response_body):
"""Serialize a dict into a string and wrap in a wsgi.Request object.
@ -253,7 +256,7 @@ class Resource(object):
"""
action_args = self.get_action_args(request.environ)
action = action_args.pop('action')
action = action_args.pop('action', None)
if request.method.lower() in ('post', 'put'):
if len(request.body) == 0:
@ -275,14 +278,18 @@ class Resource(object):
return request.best_match_content_type()
def get_action_args(self, request_environment):
args = request_environment['wsgiorg.routing_args'][1].copy()
try:
args = request_environment['wsgiorg.routing_args'][1].copy()
del args['controller']
del args['controller']
if 'format' in args:
del args['format']
if 'format' in args:
del args['format']
return args
return args
except KeyError:
return {}
def get_deserializer(self, content_type):
try:

View File

@ -81,7 +81,7 @@ class S3Application(wsgi.Router):
super(S3Application, self).__init__(mapper)
class BaseRequestHandler(wsgi.Controller):
class BaseRequestHandler(object):
"""Base class emulating Tornado's web framework pattern in WSGI.
This is a direct port of Tornado's implementation, so some key decisions

View File

@ -17,12 +17,10 @@
import json
from nova import wsgi
from nova.api.openstack import extensions
class FoxInSocksController(wsgi.Controller):
class FoxInSocksController(object):
def index(self, req):
return "Try to say this Mr. Knox, sir..."

View File

@ -26,15 +26,15 @@ from nova import flags
from nova.api import openstack
from nova.api.openstack import extensions
from nova.api.openstack import flavors
from nova.api.openstack import wsgi
from nova.tests.api.openstack import fakes
import nova.wsgi
FLAGS = flags.FLAGS
response_body = "Try to say this Mr. Knox, sir..."
class StubController(nova.wsgi.Controller):
class StubController(object):
def __init__(self, body):
self.body = body

View File

@ -67,57 +67,3 @@ class Test(test.TestCase):
self.assertEqual(result.body, "Router result")
result = webob.Request.blank('/bad').get_response(Router())
self.assertNotEqual(result.body, "Router result")
class ControllerTest(test.TestCase):
class TestRouter(wsgi.Router):
class TestController(wsgi.Controller):
_serialization_metadata = {
'application/xml': {
"attributes": {
"test": ["id"]}}}
def show(self, req, id): # pylint: disable=W0622,C0103
return {"test": {"id": id}}
def __init__(self):
mapper = routes.Mapper()
mapper.resource("test", "tests", controller=self.TestController())
wsgi.Router.__init__(self, mapper)
def test_show(self):
request = wsgi.Request.blank('/tests/123')
result = request.get_response(self.TestRouter())
self.assertEqual(json.loads(result.body), {"test": {"id": "123"}})
def test_response_content_type_from_accept_xml(self):
request = webob.Request.blank('/tests/123')
request.headers["Accept"] = "application/xml"
result = request.get_response(self.TestRouter())
self.assertEqual(result.headers["Content-Type"], "application/xml")
def test_response_content_type_from_accept_json(self):
request = wsgi.Request.blank('/tests/123')
request.headers["Accept"] = "application/json"
result = request.get_response(self.TestRouter())
self.assertEqual(result.headers["Content-Type"], "application/json")
def test_response_content_type_from_query_extension_xml(self):
request = wsgi.Request.blank('/tests/123.xml')
result = request.get_response(self.TestRouter())
self.assertEqual(result.headers["Content-Type"], "application/xml")
def test_response_content_type_from_query_extension_json(self):
request = wsgi.Request.blank('/tests/123.json')
result = request.get_response(self.TestRouter())
self.assertEqual(result.headers["Content-Type"], "application/json")
def test_response_content_type_default_when_unsupported(self):
request = wsgi.Request.blank('/tests/123.unsupported')
request.headers["Accept"] = "application/unsupported1"
result = request.get_response(self.TestRouter())
self.assertEqual(result.status_int, 200)
self.assertEqual(result.headers["Content-Type"], "application/json")

View File

@ -82,36 +82,7 @@ class Server(object):
class Request(webob.Request):
def best_match_content_type(self):
"""Determine the most acceptable content-type.
Based on the query extension then the Accept header.
"""
parts = self.path.rsplit('.', 1)
if len(parts) > 1:
format = parts[1]
if format in ['json', 'xml']:
return 'application/{0}'.format(parts[1])
ctypes = ['application/json', 'application/xml']
bm = self.accept.best_match(ctypes)
return bm or 'application/json'
def get_content_type(self):
allowed_types = ("application/xml", "application/json")
if not "Content-Type" in self.headers:
msg = _("Missing Content-Type")
LOG.debug(msg)
raise webob.exc.HTTPBadRequest(msg)
type = self.content_type
if type in allowed_types:
return type
LOG.debug(_("Wrong Content-Type: %s") % type)
raise webob.exc.HTTPBadRequest("Invalid content type")
pass
class Application(object):
@ -286,7 +257,7 @@ class Router(object):
Each route in `mapper` must specify a 'controller', which is a
WSGI app to call. You'll probably want to specify an 'action' as
well and have your controller be a wsgi.Controller, who will route
well and have your controller be a controller, who will route
the request to the action method.
Examples:
@ -335,223 +306,6 @@ class Router(object):
return app
class Controller(object):
"""WSGI app that dispatched to methods.
WSGI app that reads routing information supplied by RoutesMiddleware
and calls the requested action method upon itself. All action methods
must, in addition to their normal parameters, accept a 'req' argument
which is the incoming wsgi.Request. They raise a webob.exc exception,
or return a dict which will be serialized by requested content type.
"""
@webob.dec.wsgify(RequestClass=Request)
def __call__(self, req):
"""Call the method specified in req.environ by RoutesMiddleware."""
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
method = getattr(self, action)
LOG.debug("%s %s" % (req.method, req.url))
del arg_dict['controller']
del arg_dict['action']
if 'format' in arg_dict:
del arg_dict['format']
arg_dict['req'] = req
result = method(**arg_dict)
if type(result) is dict:
content_type = req.best_match_content_type()
default_xmlns = self.get_default_xmlns(req)
body = self._serialize(result, content_type, default_xmlns)
response = webob.Response()
response.headers['Content-Type'] = content_type
response.body = body
msg_dict = dict(url=req.url, status=response.status_int)
msg = _("%(url)s returned with HTTP %(status)d") % msg_dict
LOG.debug(msg)
return response
else:
return result
def _serialize(self, data, content_type, default_xmlns):
"""Serialize the given dict to the provided content_type.
Uses self._serialization_metadata if it exists, which is a dict mapping
MIME types to information needed to serialize to that type.
"""
_metadata = getattr(type(self), '_serialization_metadata', {})
serializer = Serializer(_metadata, default_xmlns)
try:
return serializer.serialize(data, content_type)
except exception.InvalidContentType:
raise webob.exc.HTTPNotAcceptable()
def _deserialize(self, data, content_type):
"""Deserialize the request body to the specefied content type.
Uses self._serialization_metadata if it exists, which is a dict mapping
MIME types to information needed to serialize to that type.
"""
_metadata = getattr(type(self), '_serialization_metadata', {})
serializer = Serializer(_metadata)
return serializer.deserialize(data, content_type)
def get_default_xmlns(self, req):
"""Provide the XML namespace to use if none is otherwise specified."""
return None
class Serializer(object):
"""Serializes and deserializes dictionaries to certain MIME types."""
def __init__(self, metadata=None, default_xmlns=None):
"""Create a serializer based on the given WSGI environment.
'metadata' is an optional dict mapping MIME types to information
needed to serialize a dictionary to that type.
"""
self.metadata = metadata or {}
self.default_xmlns = default_xmlns
def _get_serialize_handler(self, content_type):
handlers = {
'application/json': self._to_json,
'application/xml': self._to_xml,
}
try:
return handlers[content_type]
except Exception:
raise exception.InvalidContentType(content_type=content_type)
def serialize(self, data, content_type):
"""Serialize a dictionary into the specified content type."""
return self._get_serialize_handler(content_type)(data)
def deserialize(self, datastring, content_type):
"""Deserialize a string to a dictionary.
The string must be in the format of a supported MIME type.
"""
return self.get_deserialize_handler(content_type)(datastring)
def get_deserialize_handler(self, content_type):
handlers = {
'application/json': self._from_json,
'application/xml': self._from_xml,
}
try:
return handlers[content_type]
except Exception:
raise exception.InvalidContentType(content_type=content_type)
def _from_json(self, datastring):
return utils.loads(datastring)
def _from_xml(self, datastring):
xmldata = self.metadata.get('application/xml', {})
plurals = set(xmldata.get('plurals', {}))
node = minidom.parseString(datastring).childNodes[0]
return {node.nodeName: self._from_xml_node(node, plurals)}
def _from_xml_node(self, node, listnames):
"""Convert a minidom node to a simple Python type.
listnames is a collection of names of XML nodes 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 _to_json(self, data):
return utils.dumps(data)
def _to_xml(self, data):
metadata = self.metadata.get('application/xml', {})
# 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, metadata, root_key, data[root_key])
xmlns = node.getAttribute('xmlns')
if not xmlns and self.default_xmlns:
node.setAttribute('xmlns', self.default_xmlns)
return node.toprettyxml(indent=' ')
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)
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)
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 paste_config_file(basename):
"""Find the best location in the system for a paste config file.