summaryrefslogtreecommitdiff
path: root/neutronclient/common/serializer.py
diff options
context:
space:
mode:
Diffstat (limited to 'neutronclient/common/serializer.py')
-rw-r--r--neutronclient/common/serializer.py410
1 files changed, 410 insertions, 0 deletions
diff --git a/neutronclient/common/serializer.py b/neutronclient/common/serializer.py
new file mode 100644
index 0000000..b8f1c19
--- /dev/null
+++ b/neutronclient/common/serializer.py
@@ -0,0 +1,410 @@
+# Copyright 2013 OpenStack LLC.
+# 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.
+#
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+###
+### Codes from neutron wsgi
+###
+
+import logging
+
+from xml.etree import ElementTree as etree
+from xml.parsers import expat
+
+from neutronclient.common import constants
+from neutronclient.common import exceptions as exception
+from neutronclient.openstack.common.gettextutils import _
+from neutronclient.openstack.common import jsonutils
+
+LOG = logging.getLogger(__name__)
+
+
+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):
+ return unicode(obj)
+ return jsonutils.dumps(data, default=sanitizer)
+
+
+class XMLDictSerializer(DictSerializer):
+
+ def __init__(self, metadata=None, xmlns=None):
+ """XMLDictSerializer constructor.
+
+ :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 {}
+ if not xmlns:
+ xmlns = self.metadata.get('xmlns')
+ if not xmlns:
+ xmlns = constants.XML_NS_V20
+ self.xmlns = xmlns
+
+ def default(self, data):
+ """Default serializer of XMLDictSerializer.
+
+ :param data: expect data to contain a single key as XML root, or
+ contain another '*_links' key as atom links. Other
+ case will use 'VIRTUAL_ROOT_KEY' as XML root.
+ """
+ try:
+ links = None
+ has_atom = False
+ if data is None:
+ root_key = constants.VIRTUAL_ROOT_KEY
+ root_value = None
+ else:
+ link_keys = [k for k in data.iterkeys() or []
+ if k.endswith('_links')]
+ if link_keys:
+ links = data.pop(link_keys[0], None)
+ has_atom = True
+ root_key = (len(data) == 1 and
+ data.keys()[0] or constants.VIRTUAL_ROOT_KEY)
+ root_value = data.get(root_key, data)
+ doc = etree.Element("_temp_root")
+ used_prefixes = []
+ self._to_xml_node(doc, self.metadata, root_key,
+ root_value, used_prefixes)
+ if links:
+ self._create_link_nodes(list(doc)[0], links)
+ return self.to_xml_string(list(doc)[0], used_prefixes, has_atom)
+ except AttributeError as e:
+ LOG.exception(str(e))
+ return ''
+
+ def __call__(self, data):
+ # Provides a migration path to a cleaner WSGI layer, this
+ # "default" stuff and extreme extensibility isn't being used
+ # like originally intended
+ return self.default(data)
+
+ def to_xml_string(self, node, used_prefixes, has_atom=False):
+ self._add_xmlns(node, used_prefixes, has_atom)
+ return etree.tostring(node, 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, used_prefixes, has_atom=False):
+ node.set('xmlns', self.xmlns)
+ node.set(constants.TYPE_XMLNS, self.xmlns)
+ if has_atom:
+ node.set(constants.ATOM_XMLNS, constants.ATOM_NAMESPACE)
+ node.set(constants.XSI_NIL_ATTR, constants.XSI_NAMESPACE)
+ ext_ns = self.metadata.get(constants.EXT_NS, {})
+ for prefix in used_prefixes:
+ if prefix in ext_ns:
+ node.set('xmlns:' + prefix, ext_ns[prefix])
+
+ def _to_xml_node(self, parent, metadata, nodename, data, used_prefixes):
+ """Recursive method to convert data members to XML nodes."""
+ result = etree.SubElement(parent, nodename)
+ if ":" in nodename:
+ used_prefixes.append(nodename.split(":", 1)[0])
+ #TODO(bcwaldon): accomplish this without a type-check
+ if isinstance(data, list):
+ if not data:
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_LIST)
+ 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:
+ self._to_xml_node(result, metadata, singular, item,
+ used_prefixes)
+ #TODO(bcwaldon): accomplish this without a type-check
+ elif isinstance(data, dict):
+ if not data:
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_DICT)
+ return result
+ attrs = metadata.get('attributes', {}).get(nodename, {})
+ for k, v in data.items():
+ if k in attrs:
+ result.set(k, str(v))
+ else:
+ self._to_xml_node(result, metadata, k, v,
+ used_prefixes)
+ elif data is None:
+ result.set(constants.XSI_ATTR, 'true')
+ else:
+ if isinstance(data, bool):
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_BOOL)
+ elif isinstance(data, int):
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_INT)
+ elif isinstance(data, long):
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_LONG)
+ elif isinstance(data, float):
+ result.set(
+ constants.TYPE_ATTR,
+ constants.TYPE_FLOAT)
+ LOG.debug(_("Data %(data)s type is %(type)s"),
+ {'data': data,
+ 'type': type(data)})
+ if isinstance(data, str):
+ result.text = unicode(data, 'utf-8')
+ else:
+ result.text = unicode(data)
+ return result
+
+ def _create_link_nodes(self, xml_doc, links):
+ for link in links:
+ link_node = etree.SubElement(xml_doc, 'atom:link')
+ link_node.set('rel', link['rel'])
+ link_node.set('href', link['href'])
+
+
+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 jsonutils.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):
+ """XMLDeserializer constructor.
+
+ :param metadata: information needed to deserialize xml into
+ a dictionary.
+ """
+ super(XMLDeserializer, self).__init__()
+ self.metadata = metadata or {}
+ xmlns = self.metadata.get('xmlns')
+ if not xmlns:
+ xmlns = constants.XML_NS_V20
+ self.xmlns = xmlns
+
+ def _get_key(self, tag):
+ tags = tag.split("}", 1)
+ if len(tags) == 2:
+ ns = tags[0][1:]
+ bare_tag = tags[1]
+ ext_ns = self.metadata.get(constants.EXT_NS, {})
+ if ns == self.xmlns:
+ return bare_tag
+ for prefix, _ns in ext_ns.items():
+ if ns == _ns:
+ return prefix + ":" + bare_tag
+ else:
+ return tag
+
+ def _get_links(self, root_tag, node):
+ link_nodes = node.findall(constants.ATOM_LINK_NOTATION)
+ root_tag = self._get_key(node.tag)
+ link_key = "%s_links" % root_tag
+ link_list = []
+ for link in link_nodes:
+ link_list.append({'rel': link.get('rel'),
+ 'href': link.get('href')})
+ # Remove link node in order to avoid link node being
+ # processed as an item in _from_xml_node
+ node.remove(link)
+ return link_list and {link_key: link_list} or {}
+
+ def _from_xml(self, datastring):
+ if datastring is None:
+ return None
+ plurals = set(self.metadata.get('plurals', {}))
+ try:
+ node = etree.fromstring(datastring)
+ root_tag = self._get_key(node.tag)
+ links = self._get_links(root_tag, node)
+ result = self._from_xml_node(node, plurals)
+ # There is no case where root_tag = constants.VIRTUAL_ROOT_KEY
+ # and links is not None because of the way data are serialized
+ if root_tag == constants.VIRTUAL_ROOT_KEY:
+ return result
+ return dict({root_tag: result}, **links)
+ except Exception as e:
+ parseError = False
+ # Python2.7
+ if (hasattr(etree, 'ParseError') and
+ isinstance(e, getattr(etree, 'ParseError'))):
+ parseError = True
+ # Python2.6
+ elif isinstance(e, expat.ExpatError):
+ parseError = True
+ if parseError:
+ msg = _("Cannot understand XML")
+ raise exception.MalformedRequestBody(reason=msg)
+ else:
+ raise
+
+ 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.
+
+ """
+ attrNil = node.get(str(etree.QName(constants.XSI_NAMESPACE, "nil")))
+ attrType = node.get(str(etree.QName(
+ self.metadata.get('xmlns'), "type")))
+ if (attrNil and attrNil.lower() == 'true'):
+ return None
+ elif not len(node) and not node.text:
+ if (attrType and attrType == constants.TYPE_DICT):
+ return {}
+ elif (attrType and attrType == constants.TYPE_LIST):
+ return []
+ else:
+ return ''
+ elif (len(node) == 0 and node.text):
+ converters = {constants.TYPE_BOOL:
+ lambda x: x.lower() == 'true',
+ constants.TYPE_INT:
+ lambda x: int(x),
+ constants.TYPE_LONG:
+ lambda x: long(x),
+ constants.TYPE_FLOAT:
+ lambda x: float(x)}
+ if attrType and attrType in converters:
+ return converters[attrType](node.text)
+ else:
+ return node.text
+ elif self._get_key(node.tag) in listnames:
+ return [self._from_xml_node(n, listnames) for n in node]
+ else:
+ result = dict()
+ for attr in node.keys():
+ if (attr == 'xmlns' or
+ attr.startswith('xmlns:') or
+ attr == constants.XSI_ATTR or
+ attr == constants.TYPE_ATTR):
+ continue
+ result[self._get_key(attr)] = node.get(attr)
+ children = list(node)
+ for child in children:
+ result[self._get_key(child.tag)] = self._from_xml_node(
+ child, listnames)
+ return result
+
+ def default(self, datastring):
+ return {'body': self._from_xml(datastring)}
+
+ def __call__(self, datastring):
+ # Adding a migration path to allow us to remove unncessary classes
+ return self.default(datastring)
+
+
+# NOTE(maru): this class is duplicated from neutron.wsgi
+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': JSONDictSerializer(),
+ 'application/xml': XMLDictSerializer(self.metadata),
+ }
+
+ 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).serialize(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).deserialize(
+ datastring)
+
+ def get_deserialize_handler(self, content_type):
+ handlers = {
+ 'application/json': JSONDeserializer(),
+ 'application/xml': XMLDeserializer(self.metadata),
+ }
+
+ try:
+ return handlers[content_type]
+ except Exception:
+ raise exception.InvalidContentType(content_type=content_type)