~lazr-developers/lazr.restful/oops-fix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
# Copyright 2008 Canonical Ltd.  All rights reserved.

"""Marshallers for fields used in HTTP resources."""

__metaclass__ = type
__all__ = [
    'AbstractCollectionFieldMarshaller',
    'BoolFieldMarshaller',
    'BytesFieldMarshaller',
    'CollectionFieldMarshaller',
    'DateTimeFieldMarshaller',
    'FloatFieldMarshaller',
    'IntFieldMarshaller',
    'ObjectLookupFieldMarshaller',
    'SetFieldMarshaller',
    'SimpleFieldMarshaller',
    'SimpleVocabularyLookupFieldMarshaller',
    'TextFieldMarshaller',
    'TokenizedVocabularyFieldMarshaller',
    'URLDereferencingMixin',
    'VocabularyLookupFieldMarshaller',
    ]

from datetime import datetime
import pytz
from StringIO import StringIO
import urllib

import simplejson

from zope.datetime import DateTimeError, DateTimeParser
from zope.component import getMultiAdapter, getUtility
from zope.interface import implements
from zope.publisher.interfaces import NotFound
from zope.security.proxy import removeSecurityProxy
from zope.traversing.browser import absoluteURL

from lazr.uri import URI, InvalidURIError

from lazr.restful.interfaces import (
    IFieldMarshaller, IUnmarshallingDoesntNeedValue, IWebServiceConfiguration)
from lazr.restful.utils import safe_hasattr


class URLDereferencingMixin:
    """A mixin for any class that dereferences URLs into objects."""

    def dereference_url(self, url):
        """Look up a resource in the web service by URL.

        Representations and custom operations use URLs to refer to
        resources in the web service. When processing an incoming
        representation or custom operation it's often necessary to see
        which object a URL refers to. This method calls the URL
        traversal code to dereference a URL into a published object.

        :param url: The URL to a resource.
        :raise NotFound: If the URL does not designate a
            published object.
        """
        config = getUtility(IWebServiceConfiguration)
        if config.use_https:
            site_protocol = 'https'
            default_port = '443'
        else:
            site_protocol = 'http'
            default_port = '80'
        request_host = self.request.get('HTTP_HOST', 'localhost')
        if ':' in request_host:
            request_host, request_port = request_host.split(':', 2)
        else:
            request_port = default_port

        if not isinstance(url, basestring):
            raise ValueError("got '%s', expected string: %r" % (
                type(url).__name__, url))
        uri = URI(url)
        protocol = uri.scheme
        host = uri.host
        port = uri.port or default_port
        path = uri.path
        query = uri.query
        fragment = uri.fragment

        url_host_and_http_host_are_identical = (
            host == request_host and port == request_port)
        if (not url_host_and_http_host_are_identical
            or protocol != site_protocol or query is not None
            or fragment is not None):
            raise NotFound(self, url, self.request)

        path_parts = [urllib.unquote(part) for part in path.split('/')]
        path_parts.pop(0)
        path_parts.reverse()
        request = config.createRequest(StringIO(), {'PATH_INFO' : path})
        request.setTraversalStack(path_parts)
        root = request.publication.getApplication(self.request)
        return request.traverse(root)


class SimpleFieldMarshaller:
    """A marshaller that returns the same value it's served.

    This implementation is meant to be subclassed.
    """
    implements(IFieldMarshaller)


    # Set this to type or tuple of types that the JSON value must be of.
    _type = None

    def __init__(self, field, request):
        self.field = field
        self.request = request

    def marshall_from_json_data(self, value):
        """See `IFieldMarshaller`.

        When value is None, return None, otherwise call
        _marshall_from_json_data().
        """
        if value is None:
            return None
        return self._marshall_from_json_data(value)

    def marshall_from_request(self, value):
        """See `IFieldMarshaller`.

        Try to decode value as a JSON-encoded string and pass it on to
        _marshall_from_request() if it's not None. If value isn't a
        JSON-encoded string, interpret it as string literal.
        """
        if value != '':
            try:
                v = value
                if isinstance (v, str):
                    v = v.decode('utf8') # assume utf8
                # XXX gary 2009-03-28
                # The use of the enclosing brackets is a hack to work around
                # simplejson bug 43:
                # http://code.google.com/p/simplejson/issues/detail?id=43
                v = simplejson.loads(u'[%s]' % (v,))
            except (ValueError, TypeError):
                # Pass the value as is. This saves client from having to encode
                # strings.
                pass
            else:
                # see comment about simplejson bug above
                value = v[0]
        if value is None:
            return None
        return self._marshall_from_request(value)

    def _marshall_from_request(self, value):
        """Hook method to marshall a non-null JSON value.

        Default is to just call _marshall_from_json_data() with the value.
        """
        return self._marshall_from_json_data(value)

    def _marshall_from_json_data(self, value):
        """Hook method to marshall a no-null value.

        Default is to return the value unchanged.
        """
        if self._type is not None:
            if not isinstance(value, self._type):
                if isinstance(self._type, (tuple, list)):
                    expected_name = ", ".join(
                        a_type.__name__ for a_type in self._type)
                else:
                    expected_name = self._type.__name__
                raise ValueError(
                    "got '%s', expected %s: %r" % (
                        type(value).__name__, expected_name, value))
        return value

    @property
    def representation_name(self):
        """See `IFieldMarshaller`.

        Return the field name as is.
        """
        return self.field.__name__

    def unmarshall(self, entry, value):
        """See `IFieldMarshaller`.

        Return the value as is.
        """
        return value

    def unmarshall_to_closeup(self, entry, value):
        """See `IFieldMarshaller`.

        Return the value as is.
        """
        return self.unmarshall(entry, value)


class BoolFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller that transforms its value into an integer."""

    _type = bool


class IntFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller that transforms its value into an integer."""

    _type = int


class FloatFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller that transforms its value into an integer."""

    _type = (float, int)

    def _marshall_from_json_data(self, value):
        """See `SimpleFieldMarshaller`.

        Converts the value to a float.
        """
        return float(
            super(FloatFieldMarshaller, self)._marshall_from_json_data(value))


class BytesFieldMarshaller(SimpleFieldMarshaller):
    """FieldMarshaller for IBytes field."""

    _type = str
    _type_error_message = 'not a string: %r'

    @property
    def representation_name(self):
        """See `IFieldMarshaller`.

        Represent as a link to another resource.
        """
        return "%s_link" % self.field.__name__

    def unmarshall(self, entry, bytestorage):
        """See `IFieldMarshaller`.

        Marshall as a link to the byte storage resource.
        """
        return "%s/%s" % (absoluteURL(entry.context, self.request),
                          self.field.__name__)

    def _marshall_from_request(self, value):
        """See `SimpleFieldMarshaller`.

        Reads the data from file-like object, and converts non-strings into
        one.
        """
        if safe_hasattr(value, 'seek'):
            value.seek(0)
            value = value.read()
        elif not isinstance(value, basestring):
            value = str(value)
        else:
            # Leave string conversion to _marshall_from_json_data.
            pass
        return super(BytesFieldMarshaller, self)._marshall_from_request(value)

    def _marshall_from_json_data(self, value):
        """See `SimpleFieldMarshaller`.

        Convert all strings to byte strings.
        """
        if isinstance(value, unicode):
            value = value.encode('utf-8')
        return super(
            BytesFieldMarshaller, self)._marshall_from_json_data(value)


class TextFieldMarshaller(SimpleFieldMarshaller):
    """FieldMarshaller for IText fields."""

    _type = unicode
    _type_error_message = 'not a unicode string: %r'

    def _marshall_from_request(self, value):
        """See `SimpleFieldMarshaller`.

        Converts the value to unicode.
        """
        value = unicode(value)
        return super(TextFieldMarshaller, self)._marshall_from_request(value)


class FixedVocabularyFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller for vocabulary fields whose vocabularies are fixed."""

    def __init__(self, field, request, vocabulary):
        """Initialize with respect to a field.

        :vocabulary: This argument is ignored; field.vocabulary is the
            same object. This argument is passed because the
            VocabularyLookupFieldMarshaller uses the vocabulary as
            part of a multiadapter lookup of the appropriate
            marshaller.
        """
        super(FixedVocabularyFieldMarshaller, self).__init__(
            field, request)

    def unmarshall_to_closeup(self, entry, value):
        """Describe all values, not just the selected value."""
        unmarshalled = []
        for item in self.field.vocabulary:
            item_dict = {'token' : item.token, 'title' : item.title}
            if value.title == item.title:
                item_dict['selected'] = True
            unmarshalled.append(item_dict)
        return unmarshalled


class TokenizedVocabularyFieldMarshaller(FixedVocabularyFieldMarshaller):
    """A marshaller that looks up value using a token in a vocabulary."""

    def __init__(self, field, request, vocabulary):
        super(TokenizedVocabularyFieldMarshaller, self).__init__(
            field, request, vocabulary)

    def _marshall_from_json_data(self, value):
        """See `SimpleFieldMarshaller`.

        Looks up the value as a token in the vocabulary.
        """
        try:
            return self.field.vocabulary.getTermByToken(str(value)).value
        except LookupError:
            raise ValueError("%r isn't a valid token" % value)


class DateTimeFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller that transforms its value into a datetime object."""

    def _marshall_from_json_data(self, value):
        """Parse the value as a datetime object."""
        try:
            value = DateTimeParser().parse(value)
            (year, month, day, hours, minutes, secondsAndMicroseconds,
             timezone) = value
            seconds = int(secondsAndMicroseconds)
            microseconds = int(
                round((secondsAndMicroseconds - seconds) * 1000000))
            if timezone not in ['Z', '+0000', '-0000']:
                raise ValueError("Time not in UTC.")
            return datetime(year, month, day, hours, minutes,
                            seconds, microseconds, pytz.utc)
        except DateTimeError:
            raise ValueError("Value doesn't look like a date.")
        except TypeError:
            # JSON will serialize '20090131' as a number
            raise ValueError("Value doesn't look like a date.")


class DateFieldMarshaller(DateTimeFieldMarshaller):
    """A marshaller that transforms its value into a date object."""

    def _marshall_from_json_data(self, value):
        """Parse the value as a datetime.date object."""
        super_class = super(DateFieldMarshaller, self)
        date_time = super_class._marshall_from_json_data(value)
        return date_time.date()


class AbstractCollectionFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller for AbstractCollections.

    It looks up the marshaller for its value-type, to handle its contained
    elements.
    """
    # The only valid JSON representation is a list.
    _type = list
    _type_error_message = 'not a list: %r'

    def __init__(self, field, request):
        """See `SimpleFieldMarshaller`.

        This also looks for the appropriate marshaller for value_type.
        """
        super(AbstractCollectionFieldMarshaller, self).__init__(
            field, request)
        self.value_marshaller = getMultiAdapter(
            (field.value_type, request), IFieldMarshaller)

    def _marshall_from_json_data(self, value):
        """See `SimpleFieldMarshaller`.

        Marshall every elements of the list using the appropriate
        marshaller.
        """
        value = super(
            AbstractCollectionFieldMarshaller,
            self)._marshall_from_json_data(value)

        # In AbstractCollection subclasses, _type contains the type object,
        # which can be used as a factory.
        return self._python_collection_factory(
            self.value_marshaller.marshall_from_json_data(item)
            for item in value)

    def _marshall_from_request(self, value):
        """See `SimpleFieldMarshaller`.

        If the value isn't a list, transform it into a one-element list. That
        allows web client to submit one-element list of strings
        without having to JSON-encode it.

        Additionally, all items in the list are marshalled using the
        appropriate `IFieldMarshaller` for the value_type.
        """
        if not isinstance(value, list):
            value = [value]
        return self._python_collection_factory(
            self.value_marshaller.marshall_from_request(item)
            for item in value)

    @property
    def _python_collection_factory(self):
        """Create the appropriate python collection from a list."""
        # In AbstractCollection subclasses, _type contains the type object,
        # which can be used as a factory.
        return self.field._type

    def unmarshall(self, entry, value):
        """See `SimpleFieldMarshaller`.

        The collection is unmarshalled into a list and all its items are
        unmarshalled using the appropriate FieldMarshaller.
        """
        return [self.value_marshaller.unmarshall(entry, item)
               for item in value]


class SetFieldMarshaller(AbstractCollectionFieldMarshaller):
    """Marshaller for sets."""

    @property
    def _python_collection_factory(self):
        return set


class CollectionFieldMarshaller(SimpleFieldMarshaller):
    """A marshaller for collection fields."""
    implements(IUnmarshallingDoesntNeedValue)

    @property
    def representation_name(self):
        """See `IFieldMarshaller`.

        Make it clear that the value is a link to a collection.
        """
        return "%s_collection_link" % self.field.__name__

    def unmarshall(self, entry, value):
        """See `IFieldMarshaller`.

        This returns a link to the scoped collection.
        """
        return "%s/%s" % (absoluteURL(entry.context, self.request),
                          self.field.__name__)


def VocabularyLookupFieldMarshaller(field, request):
    """A marshaller that uses the underlying vocabulary.

    This is just a factory function that does another adapter lookup
    for a marshaller, one that can take into account the vocabulary
    in addition to the field type (presumably Choice) and the request.
    """
    return getMultiAdapter((field, request, field.vocabulary),
                           IFieldMarshaller)


class SimpleVocabularyLookupFieldMarshaller(FixedVocabularyFieldMarshaller):
    """A marshaller for vocabulary lookup by title."""

    def __init__(self, field, request, vocabulary):
        """Initialize the marshaller with the vocabulary it'll use."""
        super(SimpleVocabularyLookupFieldMarshaller, self).__init__(
            field, request, vocabulary)
        self.vocabulary = vocabulary

    def _marshall_from_json_data(self, value):
        """Find an item in the vocabulary by title."""
        valid_titles = []
        for item in self.field.vocabulary.items:
            if item.title == value:
                return item
            valid_titles.append(item.title)
        raise ValueError(
            ('Invalid value "%s". Acceptable values are: %s' %
             (value, ', '.join(valid_titles))).encode("utf-8"))

    def unmarshall(self, entry, value):
        if value is None:
            return None
        return value.title


class ObjectLookupFieldMarshaller(SimpleFieldMarshaller,
                                  URLDereferencingMixin):
    """A marshaller that turns URLs into data model objects.

    This marshaller can be used with a IChoice field (initialized
    with a vocabulary) or with an IObject field (no vocabulary).
    """

    def __init__(self, field, request, vocabulary=None):
        super(ObjectLookupFieldMarshaller, self).__init__(field, request)
        self.vocabulary = vocabulary

    @property
    def representation_name(self):
        """See `IFieldMarshaller`.

        Make it clear that the value is a link to an object, not an object.
        """
        return "%s_link" % self.field.__name__

    def unmarshall(self, entry, value):
        """See `IFieldMarshaller`.

        Represent an object as the URL to that object
        """
        repr_value = None
        if value is not None:
            repr_value = absoluteURL(value, self.request)
        return repr_value

    def _marshall_from_json_data(self, value):
        """See `IFieldMarshaller`.

        Look up the data model object by URL.
        """
        try:
            resource = self.dereference_url(value)
        except NotFound:
            # The URL doesn't correspond to any real object.
            raise ValueError('No such object "%s".' % value)
        except InvalidURIError:
            raise ValueError('"%s" is not a valid URI.' % value)
        # We looked up the URL and got the thing at the other end of
        # the URL: a resource. But internally, a resource isn't a
        # valid value for any schema field. Instead we want the object
        # that serves as a resource's context. Any time we want to get
        # to the object underlying a resource, we need to strip its
        # security proxy.
        return removeSecurityProxy(resource).context