• 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 six
28
29__author__ = 'rafek@google.com (Rafe Kaplan)'
30
31import base64
32import binascii
33import logging
34
35from . import message_types
36from . import messages
37from . import util
38
39__all__ = [
40    'ALTERNATIVE_CONTENT_TYPES',
41    'CONTENT_TYPE',
42    'MessageJSONEncoder',
43    'encode_message',
44    'decode_message',
45    'ProtoJson',
46]
47
48
49def _load_json_module():
50  """Try to load a valid json module.
51
52  There are more than one json modules that might be installed.  They are
53  mostly compatible with one another but some versions may be different.
54  This function attempts to load various json modules in a preferred order.
55  It does a basic check to guess if a loaded version of json is compatible.
56
57  Returns:
58    Compatible json module.
59
60  Raises:
61    ImportError if there are no json modules or the loaded json module is
62      not compatible with ProtoRPC.
63  """
64  first_import_error = None
65  for module_name in ['json',
66                      'simplejson']:
67    try:
68      module = __import__(module_name, {}, {}, 'json')
69      if not hasattr(module, 'JSONEncoder'):
70        message = ('json library "%s" is not compatible with ProtoRPC' %
71                   module_name)
72        logging.warning(message)
73        raise ImportError(message)
74      else:
75        return module
76    except ImportError as err:
77      if not first_import_error:
78        first_import_error = err
79
80  logging.error('Must use valid json library (Python 2.6 json or simplejson)')
81  raise first_import_error
82json = _load_json_module()
83
84
85# TODO: Rename this to MessageJsonEncoder.
86class MessageJSONEncoder(json.JSONEncoder):
87  """Message JSON encoder class.
88
89  Extension of JSONEncoder that can build JSON from a message object.
90  """
91
92  def __init__(self, protojson_protocol=None, **kwargs):
93    """Constructor.
94
95    Args:
96      protojson_protocol: ProtoJson instance.
97    """
98    super(MessageJSONEncoder, self).__init__(**kwargs)
99    self.__protojson_protocol = 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] = self.__protojson_protocol.encode_field(
120             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(unknown_key)
125        result[unknown_key] = unrecognized_field
126      return result
127    else:
128      return super(MessageJSONEncoder, self).default(value)
129
130
131class ProtoJson(object):
132  """ProtoRPC JSON implementation class.
133
134  Implementation of JSON based protocol used for serializing and deserializing
135  message objects.  Instances of remote.ProtocolConfig constructor or used with
136  remote.Protocols.add_protocol.  See the remote.py module for more details.
137  """
138
139  CONTENT_TYPE = 'application/json'
140  ALTERNATIVE_CONTENT_TYPES = [
141      'application/x-javascript',
142      'text/javascript',
143      'text/x-javascript',
144      'text/x-json',
145      'text/json',
146  ]
147
148  def encode_field(self, field, value):
149    """Encode a python field value to a JSON value.
150
151    Args:
152      field: A ProtoRPC field instance.
153      value: A python value supported by field.
154
155    Returns:
156      A JSON serializable value appropriate for field.
157    """
158    if isinstance(field, messages.BytesField):
159      if field.repeated:
160        value = [base64.b64encode(byte) for byte in value]
161      else:
162        value = base64.b64encode(value)
163    elif isinstance(field, message_types.DateTimeField):
164      # DateTimeField stores its data as a RFC 3339 compliant string.
165      if field.repeated:
166        value = [i.isoformat() for i in value]
167      else:
168        value = value.isoformat()
169    return value
170
171  def encode_message(self, message):
172    """Encode Message instance to JSON string.
173
174    Args:
175      Message instance to encode in to JSON string.
176
177    Returns:
178      String encoding of Message instance in protocol JSON format.
179
180    Raises:
181      messages.ValidationError if message is not initialized.
182    """
183    message.check_initialized()
184
185    return json.dumps(message, cls=MessageJSONEncoder, protojson_protocol=self)
186
187  def decode_message(self, message_type, encoded_message):
188    """Merge JSON structure to Message instance.
189
190    Args:
191      message_type: Message to decode data to.
192      encoded_message: JSON encoded version of message.
193
194    Returns:
195      Decoded instance of message_type.
196
197    Raises:
198      ValueError: If encoded_message is not valid JSON.
199      messages.ValidationError if merged message is not initialized.
200    """
201    if not encoded_message.strip():
202      return message_type()
203
204    dictionary = json.loads(encoded_message)
205    message = self.__decode_dictionary(message_type, dictionary)
206    message.check_initialized()
207    return message
208
209  def __find_variant(self, value):
210    """Find the messages.Variant type that describes this value.
211
212    Args:
213      value: The value whose variant type is being determined.
214
215    Returns:
216      The messages.Variant value that best describes value's type, or None if
217      it's a type we don't know how to handle.
218    """
219    if isinstance(value, bool):
220      return messages.Variant.BOOL
221    elif isinstance(value, six.integer_types):
222      return messages.Variant.INT64
223    elif isinstance(value, float):
224      return messages.Variant.DOUBLE
225    elif isinstance(value, six.string_types):
226      return messages.Variant.STRING
227    elif isinstance(value, (list, tuple)):
228      # Find the most specific variant that covers all elements.
229      variant_priority = [None, messages.Variant.INT64, messages.Variant.DOUBLE,
230                          messages.Variant.STRING]
231      chosen_priority = 0
232      for v in value:
233        variant = self.__find_variant(v)
234        try:
235          priority = variant_priority.index(variant)
236        except IndexError:
237          priority = -1
238        if priority > chosen_priority:
239          chosen_priority = priority
240      return variant_priority[chosen_priority]
241    # Unrecognized type.
242    return None
243
244  def __decode_dictionary(self, message_type, dictionary):
245    """Merge dictionary in to message.
246
247    Args:
248      message: Message to merge dictionary in to.
249      dictionary: Dictionary to extract information from.  Dictionary
250        is as parsed from JSON.  Nested objects will also be dictionaries.
251    """
252    message = message_type()
253    for key, value in six.iteritems(dictionary):
254      if value is None:
255        try:
256          message.reset(key)
257        except AttributeError:
258          pass  # This is an unrecognized field, skip it.
259        continue
260
261      try:
262        field = message.field_by_name(key)
263      except KeyError:
264        # Save unknown values.
265        variant = self.__find_variant(value)
266        if variant:
267          if key.isdigit():
268            key = int(key)
269          message.set_unrecognized_field(key, value, variant)
270        else:
271          logging.warning('No variant found for unrecognized field: %s', key)
272        continue
273
274      # Normalize values in to a list.
275      if isinstance(value, list):
276        if not value:
277          continue
278      else:
279        value = [value]
280
281      valid_value = []
282      for item in value:
283        valid_value.append(self.decode_field(field, item))
284
285      if field.repeated:
286        existing_value = getattr(message, field.name)
287        setattr(message, field.name, valid_value)
288      else:
289        setattr(message, field.name, valid_value[-1])
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('Invalid enum value "%s"' % (value or ''))
307
308    elif isinstance(field, messages.BytesField):
309      try:
310        return base64.b64decode(value)
311      except (binascii.Error, TypeError) as err:
312        raise messages.DecodeError('Base64 decoding error: %s' % err)
313
314    elif isinstance(field, message_types.DateTimeField):
315      try:
316        return util.decode_datetime(value)
317      except ValueError as err:
318        raise messages.DecodeError(err)
319
320    elif (isinstance(field, messages.MessageField) and
321          issubclass(field.type, messages.Message)):
322      return self.__decode_dictionary(field.type, value)
323
324    elif (isinstance(field, messages.FloatField) and
325          isinstance(value, (six.integer_types, six.string_types))):
326      try:
327        return float(value)
328      except:
329        pass
330
331    elif (isinstance(field, messages.IntegerField) and
332          isinstance(value, six.string_types)):
333      try:
334        return int(value)
335      except:
336        pass
337
338    return value
339
340  @staticmethod
341  def get_default():
342    """Get default instanceof ProtoJson."""
343    try:
344      return ProtoJson.__default
345    except AttributeError:
346      ProtoJson.__default = ProtoJson()
347      return ProtoJson.__default
348
349  @staticmethod
350  def set_default(protocol):
351    """Set the default instance of ProtoJson.
352
353    Args:
354      protocol: A ProtoJson instance.
355    """
356    if not isinstance(protocol, ProtoJson):
357      raise TypeError('Expected protocol of type ProtoJson')
358    ProtoJson.__default = protocol
359
360CONTENT_TYPE = ProtoJson.CONTENT_TYPE
361
362ALTERNATIVE_CONTENT_TYPES = ProtoJson.ALTERNATIVE_CONTENT_TYPES
363
364encode_message = ProtoJson.get_default().encode_message
365
366decode_message = ProtoJson.get_default().decode_message
367