• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python
2#
3# Copyright 2010 Google Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17
18"""JSON support for message types.
19
20Public classes:
21  MessageJSONEncoder: JSON encoder for message objects.
22
23Public functions:
24  encode_message: Encodes a message in to a JSON string.
25  decode_message: Merge from a JSON string in to a message.
26"""
27import base64
28import binascii
29import logging
30
31import six
32
33from apitools.base.protorpclite import message_types
34from apitools.base.protorpclite import messages
35from apitools.base.protorpclite import util
36
37__all__ = [
38    'ALTERNATIVE_CONTENT_TYPES',
39    'CONTENT_TYPE',
40    'MessageJSONEncoder',
41    'encode_message',
42    'decode_message',
43    'ProtoJson',
44]
45
46
47def _load_json_module():
48    """Try to load a valid json module.
49
50    There are more than one json modules that might be installed.  They are
51    mostly compatible with one another but some versions may be different.
52    This function attempts to load various json modules in a preferred order.
53    It does a basic check to guess if a loaded version of json is compatible.
54
55    Returns:
56      Compatible json module.
57
58    Raises:
59      ImportError if there are no json modules or the loaded json module is
60        not compatible with ProtoRPC.
61    """
62    first_import_error = None
63    for module_name in ['json',
64                        'simplejson']:
65        try:
66            module = __import__(module_name, {}, {}, 'json')
67            if not hasattr(module, 'JSONEncoder'):
68                message = (
69                    'json library "%s" is not compatible with ProtoRPC' %
70                    module_name)
71                logging.warning(message)
72                raise ImportError(message)
73            else:
74                return module
75        except ImportError as err:
76            if not first_import_error:
77                first_import_error = err
78
79    logging.error('Must use valid json library (json or simplejson)')
80    raise first_import_error  # pylint:disable=raising-bad-type
81json = _load_json_module()
82
83
84# TODO: Rename this to MessageJsonEncoder.
85class MessageJSONEncoder(json.JSONEncoder):
86    """Message JSON encoder class.
87
88    Extension of JSONEncoder that can build JSON from a message object.
89    """
90
91    def __init__(self, protojson_protocol=None, **kwargs):
92        """Constructor.
93
94        Args:
95          protojson_protocol: ProtoJson instance.
96        """
97        super(MessageJSONEncoder, self).__init__(**kwargs)
98        self.__protojson_protocol = (
99            protojson_protocol or ProtoJson.get_default())
100
101    def default(self, value):
102        """Return dictionary instance from a message object.
103
104        Args:
105        value: Value to get dictionary for.  If not encodable, will
106          call superclasses default method.
107        """
108        if isinstance(value, messages.Enum):
109            return str(value)
110
111        if six.PY3 and isinstance(value, bytes):
112            return value.decode('utf8')
113
114        if isinstance(value, messages.Message):
115            result = {}
116            for field in value.all_fields():
117                item = value.get_assigned_value(field.name)
118                if item not in (None, [], ()):
119                    result[field.name] = (
120                        self.__protojson_protocol.encode_field(field, item))
121            # Handle unrecognized fields, so they're included when a message is
122            # decoded then encoded.
123            for unknown_key in value.all_unrecognized_fields():
124                unrecognized_field, _ = value.get_unrecognized_field_info(
125                    unknown_key)
126                result[unknown_key] = unrecognized_field
127            return result
128
129        return super(MessageJSONEncoder, self).default(value)
130
131
132class ProtoJson(object):
133    """ProtoRPC JSON implementation class.
134
135    Implementation of JSON based protocol used for serializing and
136    deserializing message objects. Instances of remote.ProtocolConfig
137    constructor or used with remote.Protocols.add_protocol. See the
138    remote.py module for more details.
139
140    """
141
142    CONTENT_TYPE = 'application/json'
143    ALTERNATIVE_CONTENT_TYPES = [
144        'application/x-javascript',
145        'text/javascript',
146        'text/x-javascript',
147        'text/x-json',
148        'text/json',
149    ]
150
151    def encode_field(self, field, value):
152        """Encode a python field value to a JSON value.
153
154        Args:
155          field: A ProtoRPC field instance.
156          value: A python value supported by field.
157
158        Returns:
159          A JSON serializable value appropriate for field.
160        """
161        if isinstance(field, messages.BytesField):
162            if field.repeated:
163                value = [base64.b64encode(byte) for byte in value]
164            else:
165                value = base64.b64encode(value)
166        elif isinstance(field, message_types.DateTimeField):
167            # DateTimeField stores its data as a RFC 3339 compliant string.
168            if field.repeated:
169                value = [i.isoformat() for i in value]
170            else:
171                value = value.isoformat()
172        return value
173
174    def encode_message(self, message):
175        """Encode Message instance to JSON string.
176
177        Args:
178          Message instance to encode in to JSON string.
179
180        Returns:
181          String encoding of Message instance in protocol JSON format.
182
183        Raises:
184          messages.ValidationError if message is not initialized.
185        """
186        message.check_initialized()
187
188        return json.dumps(message, cls=MessageJSONEncoder,
189                          protojson_protocol=self)
190
191    def decode_message(self, message_type, encoded_message):
192        """Merge JSON structure to Message instance.
193
194        Args:
195          message_type: Message to decode data to.
196          encoded_message: JSON encoded version of message.
197
198        Returns:
199          Decoded instance of message_type.
200
201        Raises:
202          ValueError: If encoded_message is not valid JSON.
203          messages.ValidationError if merged message is not initialized.
204        """
205        if not encoded_message.strip():
206            return message_type()
207
208        dictionary = json.loads(encoded_message)
209        message = self.__decode_dictionary(message_type, dictionary)
210        message.check_initialized()
211        return message
212
213    def __find_variant(self, value):
214        """Find the messages.Variant type that describes this value.
215
216        Args:
217          value: The value whose variant type is being determined.
218
219        Returns:
220          The messages.Variant value that best describes value's type,
221          or None if it's a type we don't know how to handle.
222
223        """
224        if isinstance(value, bool):
225            return messages.Variant.BOOL
226        elif isinstance(value, six.integer_types):
227            return messages.Variant.INT64
228        elif isinstance(value, float):
229            return messages.Variant.DOUBLE
230        elif isinstance(value, six.string_types):
231            return messages.Variant.STRING
232        elif isinstance(value, (list, tuple)):
233            # Find the most specific variant that covers all elements.
234            variant_priority = [None,
235                                messages.Variant.INT64,
236                                messages.Variant.DOUBLE,
237                                messages.Variant.STRING]
238            chosen_priority = 0
239            for v in value:
240                variant = self.__find_variant(v)
241                try:
242                    priority = variant_priority.index(variant)
243                except IndexError:
244                    priority = -1
245                if priority > chosen_priority:
246                    chosen_priority = priority
247            return variant_priority[chosen_priority]
248        # Unrecognized type.
249        return None
250
251    def __decode_dictionary(self, message_type, dictionary):
252        """Merge dictionary in to message.
253
254        Args:
255          message: Message to merge dictionary in to.
256          dictionary: Dictionary to extract information from.  Dictionary
257            is as parsed from JSON.  Nested objects will also be dictionaries.
258        """
259        message = message_type()
260        for key, value in six.iteritems(dictionary):
261            if value is None:
262                try:
263                    message.reset(key)
264                except AttributeError:
265                    pass  # This is an unrecognized field, skip it.
266                continue
267
268            try:
269                field = message.field_by_name(key)
270            except KeyError:
271                # Save unknown values.
272                variant = self.__find_variant(value)
273                if variant:
274                    message.set_unrecognized_field(key, value, variant)
275                continue
276
277            if field.repeated:
278                # This should be unnecessary? Or in fact become an error.
279                if not isinstance(value, list):
280                    value = [value]
281                valid_value = [self.decode_field(field, item)
282                               for item in value]
283                setattr(message, field.name, valid_value)
284            else:
285                # This is just for consistency with the old behavior.
286                if value == []:
287                    continue
288                setattr(message, field.name, self.decode_field(field, value))
289
290        return message
291
292    def decode_field(self, field, value):
293        """Decode a JSON value to a python value.
294
295        Args:
296          field: A ProtoRPC field instance.
297          value: A serialized JSON value.
298
299        Return:
300          A Python value compatible with field.
301        """
302        if isinstance(field, messages.EnumField):
303            try:
304                return field.type(value)
305            except TypeError:
306                raise messages.DecodeError(
307                    'Invalid enum value "%s"' % (value or ''))
308
309        elif isinstance(field, messages.BytesField):
310            try:
311                return base64.b64decode(value)
312            except (binascii.Error, TypeError) as err:
313                raise messages.DecodeError('Base64 decoding error: %s' % err)
314
315        elif isinstance(field, message_types.DateTimeField):
316            try:
317                return util.decode_datetime(value)
318            except ValueError as err:
319                raise messages.DecodeError(err)
320
321        elif (isinstance(field, messages.MessageField) and
322              issubclass(field.type, messages.Message)):
323            return self.__decode_dictionary(field.type, value)
324
325        elif (isinstance(field, messages.FloatField) and
326              isinstance(value, (six.integer_types, six.string_types))):
327            try:
328                return float(value)
329            except:  # pylint:disable=bare-except
330                pass
331
332        elif (isinstance(field, messages.IntegerField) and
333              isinstance(value, six.string_types)):
334            try:
335                return int(value)
336            except:  # pylint:disable=bare-except
337                pass
338
339        return value
340
341    @staticmethod
342    def get_default():
343        """Get default instanceof ProtoJson."""
344        try:
345            return ProtoJson.__default
346        except AttributeError:
347            ProtoJson.__default = ProtoJson()
348            return ProtoJson.__default
349
350    @staticmethod
351    def set_default(protocol):
352        """Set the default instance of ProtoJson.
353
354        Args:
355          protocol: A ProtoJson instance.
356        """
357        if not isinstance(protocol, ProtoJson):
358            raise TypeError('Expected protocol of type ProtoJson')
359        ProtoJson.__default = protocol
360
361CONTENT_TYPE = ProtoJson.CONTENT_TYPE
362
363ALTERNATIVE_CONTENT_TYPES = ProtoJson.ALTERNATIVE_CONTENT_TYPES
364
365encode_message = ProtoJson.get_default().encode_message
366
367decode_message = ProtoJson.get_default().decode_message
368