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