OLD | NEW |
(Empty) | |
| 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 |
| 20 Public classes: |
| 21 MessageJSONEncoder: JSON encoder for message objects. |
| 22 |
| 23 Public 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 """ |
| 27 import base64 |
| 28 import binascii |
| 29 import logging |
| 30 |
| 31 import six |
| 32 |
| 33 from apitools.base.protorpclite import message_types |
| 34 from apitools.base.protorpclite import messages |
| 35 from 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 |
| 47 def _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 |
| 81 json = _load_json_module() |
| 82 |
| 83 |
| 84 # TODO: Rename this to MessageJsonEncoder. |
| 85 class 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 else: |
| 129 return super(MessageJSONEncoder, self).default(value) |
| 130 |
| 131 |
| 132 class 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 else: |
| 276 logging.warning( |
| 277 'No variant found for unrecognized field: %s', key) |
| 278 continue |
| 279 |
| 280 # Normalize values in to a list. |
| 281 if isinstance(value, list): |
| 282 if not value: |
| 283 continue |
| 284 else: |
| 285 value = [value] |
| 286 |
| 287 valid_value = [] |
| 288 for item in value: |
| 289 valid_value.append(self.decode_field(field, item)) |
| 290 |
| 291 if field.repeated: |
| 292 _ = getattr(message, field.name) |
| 293 setattr(message, field.name, valid_value) |
| 294 else: |
| 295 setattr(message, field.name, valid_value[-1]) |
| 296 return message |
| 297 |
| 298 def decode_field(self, field, value): |
| 299 """Decode a JSON value to a python value. |
| 300 |
| 301 Args: |
| 302 field: A ProtoRPC field instance. |
| 303 value: A serialized JSON value. |
| 304 |
| 305 Return: |
| 306 A Python value compatible with field. |
| 307 """ |
| 308 if isinstance(field, messages.EnumField): |
| 309 try: |
| 310 return field.type(value) |
| 311 except TypeError: |
| 312 raise messages.DecodeError( |
| 313 'Invalid enum value "%s"' % (value or '')) |
| 314 |
| 315 elif isinstance(field, messages.BytesField): |
| 316 try: |
| 317 return base64.b64decode(value) |
| 318 except (binascii.Error, TypeError) as err: |
| 319 raise messages.DecodeError('Base64 decoding error: %s' % err) |
| 320 |
| 321 elif isinstance(field, message_types.DateTimeField): |
| 322 try: |
| 323 return util.decode_datetime(value) |
| 324 except ValueError as err: |
| 325 raise messages.DecodeError(err) |
| 326 |
| 327 elif (isinstance(field, messages.MessageField) and |
| 328 issubclass(field.type, messages.Message)): |
| 329 return self.__decode_dictionary(field.type, value) |
| 330 |
| 331 elif (isinstance(field, messages.FloatField) and |
| 332 isinstance(value, (six.integer_types, six.string_types))): |
| 333 try: |
| 334 return float(value) |
| 335 except: # pylint:disable=bare-except |
| 336 pass |
| 337 |
| 338 elif (isinstance(field, messages.IntegerField) and |
| 339 isinstance(value, six.string_types)): |
| 340 try: |
| 341 return int(value) |
| 342 except: # pylint:disable=bare-except |
| 343 pass |
| 344 |
| 345 return value |
| 346 |
| 347 @staticmethod |
| 348 def get_default(): |
| 349 """Get default instanceof ProtoJson.""" |
| 350 try: |
| 351 return ProtoJson.__default |
| 352 except AttributeError: |
| 353 ProtoJson.__default = ProtoJson() |
| 354 return ProtoJson.__default |
| 355 |
| 356 @staticmethod |
| 357 def set_default(protocol): |
| 358 """Set the default instance of ProtoJson. |
| 359 |
| 360 Args: |
| 361 protocol: A ProtoJson instance. |
| 362 """ |
| 363 if not isinstance(protocol, ProtoJson): |
| 364 raise TypeError('Expected protocol of type ProtoJson') |
| 365 ProtoJson.__default = protocol |
| 366 |
| 367 CONTENT_TYPE = ProtoJson.CONTENT_TYPE |
| 368 |
| 369 ALTERNATIVE_CONTENT_TYPES = ProtoJson.ALTERNATIVE_CONTENT_TYPES |
| 370 |
| 371 encode_message = ProtoJson.get_default().encode_message |
| 372 |
| 373 decode_message = ProtoJson.get_default().decode_message |
OLD | NEW |