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