1 """I define class Content and a mixin class WithContentMixin for all types of
2 elements that can have a content.
3
4 Note that Content instances are just place-holders for content-related
5 attributes and methods. They do not store data about the content, the data is
6 stored in the element owning the content, thanks to WithContentMixin. This
7 makes their on-demand-generation relatively cheap (no data retrieving).
8
9 Note also attributes/methods of the form e.content.X are also
10 accessible under the form e.content_X, which might be slightly more
11 efficient (less lookup). Maybe the former should be eventually
12 deprecated...
13 """
14
15 from cStringIO import StringIO
16 from os import path, tmpfile, unlink
17 from tempfile import mkdtemp
18 from urllib2 import urlopen, url2pathname
19 from urlparse import urljoin, urlparse
20 from weakref import ref
21
22 from advene.model.consts import _RAISE, PACKAGED_ROOT
23 from advene.model.content.register import iter_content_handlers, \
24 iter_textual_mimetypes
25 from advene.model.core.element import RELATION, RESOURCE
26 from advene.model.exceptions import ModelError
27 from advene.util.autoproperty import autoproperty
28 from advene.util.files import recursive_mkdir
29 from advene.util.session import tempdir_list
32 """I provide functionality for elements with a content.
33
34 I assume that I will be mixed in subclasses of PackageElement.
35
36 Note that there are 4 kinds of contents:
37
38 backend-stored contents:
39 those contents have no URL, and are stored directly in the backend;
40 their data can be modified through this class.
41 external contents:
42 they have a URL at which their data is stored; their data can not be
43 modified through this class.
44 packaged contents:
45 they have a URL in the special ``packaged:`` scheme, meaning that their
46 data is stored in the local filesystem (usually extracted from a zipped
47 package); their data can be modified through this class.
48 empty content:
49 only authorized for relations; they are marked by the special mimetype
50 "x-advene/none"; neither their model, URL nor data can be modified.
51
52 It follows that content-related properties are not independant from one
53 another. Property `content_mimetype` has higher priority, as it is used to
54 decide whether the content is empty or not (hence whether the other
55 properties can be set or not). Then `content_url` is used to decide between
56 the three other kinds of content, in order to decide whether `content_data`
57 can be set or not. See the documentation of each property for more detail.
58
59 Content initialization
60 ======================
61
62 According to CODING_STYLE, a mixin class should not require any
63 initialization. However, whenever an element is instantiated from backend
64 data, its content information is available, so it would be a waste of
65 CPU time (and possibly network traffic) to reload those info from the
66 backend from the sake of purity.
67
68 Hence, this class provides a method _instantiate_content, to be used as an
69 optimizationin the instantiate class method of element classes.
70 However, the implementation does not rely on the fact that the mixin will
71 be initialized with this method.
72
73 Content handler
74 ===============
75
76 Some element types, like views and queries, require a content handler which
77 depends on their content's mimetype. This mixin provides a hook method,
78 named `_update_content_handler`, which is invoked after the mimetype is
79 modified.
80 """
81
82 __mimetype = None
83 __model_id = None
84 __model_wref = staticmethod(lambda: None)
85 __url = None
86 __data = None
87 __as_synced_file = None
88 __handler = None
89
90 __cached_content = staticmethod(lambda: None)
91
92 - def _instantiate_content(self, mimetype, model, url):
93 """
94 This is method is for optimization only: it is not strictly required
95 (though recommended) to call it at instantiate time (see class
96 docstring).
97
98 No integrity constraint is checked: the backend is assumed to be sane.
99 """
100 self.__mimetype = mimetype
101 self.__model_id = model
102 self.__url = url
103 self._update_content_handler()
104
105 - def _check_content(self, mimetype=None, model_id=None, url=None):
106 """
107 Check that the provided values (assumed to be the future values of
108 the corresponding attributes) are valid and consistent (with each other
109 *and* with unmodified attributes).
110
111 NB: we do *not* check that model_id identifies a resource. Use
112 _check_reference for that.
113 """
114 if mimetype is not None:
115 if len(mimetype.split("/")) != 2:
116 raise ModelError("%r does not look like a mimetype" % mimetype)
117 if mimetype == "x-advene/none":
118 if self.ADVENE_TYPE != RELATION:
119 raise ModelError("Only relations may have an empty content")
120 if model_id is None and self.__model_id != "":
121 raise ModelError("Empty content must have no model")
122 if url is None and self.__url != "":
123 raise ModelError("Empty content must have no URL")
124 if model_id is not None:
125 if model_id != "":
126 if mimetype == "x-advene/none" \
127 or mimetype is None and self.__mimetype == "x-advene/none":
128 raise ModelError("Can not set model of empty content")
129 if url is not None:
130 if url != "":
131 if mimetype == "x-advene/none" \
132 or mimetype is None and self.__mimetype == "x-advene/none":
133 raise ModelError("Can not set URL of empty content")
134
135 - def _update_caches(self, old_idref, new_idref, element, relation):
136 """
137 :see-also: `advene.model.core.element.PackageElement._update_caches`
138 """
139 if relation == ("content_model"):
140 self.__model_id = new_idref
141 else:
142 super(WithContentMixin, self) \
143 ._update_caches(old_idref, new_idref, element, relation)
144
145 @classmethod
146 - def _check_content_cls(cls, mimetype, model, url, _func=_check_content):
147 """
148 This is a classmethod variant of _check_content, to be use in
149 _instantiate (note that all parameters must be provided in this
150 variant).
151 """
152
153
154 _func(cls, mimetype, model, url)
155
157 """See :class:`WithContentMixin` documentation."""
158
159 m = self.__mimetype
160 cmax = 0; hmax = None
161 for h in iter_content_handlers():
162 c = h.claims_for_handle(m)
163 if c > cmax:
164 cmax, hmax = c, h
165 if cmax > 0:
166 self.__handler = hmax
167 else:
168 self.__handler = None
169
170
172 """Load the content info (mimetype, model, url)."""
173
174 o = self._owner
175 self.__mimetype, self.__model_id, self.__url = \
176 o._backend.get_content_info(o._id, self._id, self.ADVENE_TYPE)
177
178 - def __store_info(self):
179 "store info in backend"
180 o = self._owner
181 o._backend.update_content_info(o._id, self._id, self.ADVENE_TYPE,
182 self.__mimetype or "",
183 self.__model_id or "",
184 self.__url or "")
185
186 - def __store_data(self):
187 "store data in backend"
188 o = self._owner
189 o._backend.update_content_data(o._id, self._id, self.ADVENE_TYPE,
190 self.__data or "")
191
192 - def get_content_model(self, default=None):
193 """Return the resource used as the model of the content of this element.
194
195 Return None if that content has no model.
196 If the model can not be retrieved, the default value is returned.
197
198 See also `content_model` and `content_model_id`.
199 """
200
201
202
203 mid = self.__model_id
204 if mid is None:
205 self._load_content_info()
206 mid = self.__model_id
207 if mid:
208 m = self.__model_wref()
209 if m is None:
210 m = self._owner.get_element(self.__model_id, default)
211 if m is not default:
212 self._media_wref = ref(m)
213 return m
214
216 """Return a file-like object giving access to the content data.
217
218 The file-like object is updatable unless the content is external.
219
220 It is an error to try to modify the data while such a file-like object
221 is opened. An exception will thus be raised whenever this method is
222 invoked or `content_data` is set before a previously returned file-like
223 object is closed.
224
225 See also `content_data`.
226 """
227 url = self.__url
228 if url is None:
229 self._load_content_info()
230 url = self.__url
231
232 if url:
233 if url.startswith("packaged:"):
234
235 if self.__as_synced_file:
236 raise IOError("content already opened as a file")
237 o = self._owner
238 prefix = o.get_meta(PACKAGED_ROOT, None)
239 assert prefix is not None, "No root is specified for packaged: paths"
240 base = url2pathname(urlparse(prefix).path)
241 filename = path.join(base, url2pathname(url[10:]))
242 f = self.__as_synced_file = PackagedDataFile(filename, self)
243 else:
244 abs = urljoin(self._owner._url, url)
245 f = urlopen(abs)
246 else:
247 if self.__as_synced_file:
248 raise IOError("content already opened as a file")
249 f = self.__as_synced_file = ContentDataFile(self)
250 return f
251
252 @autoproperty
254 """The mimetype of this element's content.
255
256 If set to "x-advene/none", the other properties are erased and become
257 unsettable. Note that it is only possible for relations.
258 """
259 r = self.__mimetype
260 if r is None:
261 self._load_content_info()
262 r = self.__mimetype
263 return r
264
265 @autoproperty
266 - def _set_content_mimetype(self, mimetype):
267 if self.__mimetype is None:
268 self._load_content_info()
269 if mimetype == "x-advene/none":
270 self._check_content(mimetype, "", "")
271 self._set_content_model(None)
272 self._set_content_url("")
273 self._set_content_data("")
274 else:
275 self._check_content(mimetype)
276 self.emit("pre-modified::content_mimetype", "content_mimetype", mimetype)
277 self.__mimetype = mimetype
278 self.__store_info()
279 self.emit("modified::content_mimetype", "content_mimetype", mimetype)
280 self._update_content_handler()
281
282 @autoproperty
284 """
285 This property indicates if this element's content data can be handled
286 as text.
287
288 It uses the mimetypes registered with
289 `advene.model.content.register.register_textual_mimetype`.
290 """
291 t1,t2 = self._get_content_mimetype().split("/")
292 if t1 == "text":
293 return True
294 for m1,m2 in iter_textual_mimetypes():
295 if m1 == "*" or m1 == t1 and m2 == "*" or m2 == t2:
296 return True
297 return False
298
299 @autoproperty
301 """The resource used as the model of the content of this element.
302
303 None if that content has no model.
304 If the model can not be retrieved, an exception is raised.
305
306 See also `get_content_model` and `content_model_id`.
307 """
308 return self.get_content_model(_RAISE)
309
310 @autoproperty
311 - def _set_content_model(self, resource):
312 """FIXME: missing docstring.
313 """
314 if self.__model_id is None:
315 self._load_content_info()
316 resource_id = self._check_reference(self._owner, resource, RESOURCE)
317 self._check_content(model_id=resource_id)
318 op = self._owner
319 self.emit("pre-modified::content_model", "content_model", resource)
320 if resource_id is not resource:
321 self.__model_id = resource_id
322 if resource is not None:
323 self.__model_wref = ref(resource)
324 elif self.__model_wref() is not None:
325 del self.__model_wref
326 else:
327 if self.__model_wref():
328 del self.__model_wref
329 if resource is None:
330 self.__model_id = ""
331 else:
332 self.__model_id = unicode(resource_id)
333 self.__store_info()
334 self.emit("modified::content_model", "content_model", resource)
335
336 @autoproperty
338 """The id-ref of the content model, or an empty string.
339
340 This is a read-only property giving the id-ref of the resource held
341 by `content_model`, or an empty string if there is no model.
342
343 Note that this property is accessible even if the corresponding
344 model is unreachable.
345
346 See also `get_content_model` and `content_model`.
347 """
348 return self.__model_id or ""
349
350 @autoproperty
352 """This property holds the URL of the content, or an empty string.
353
354 Its value determines whether the content is backend-stored, external
355 or packaged.
356
357 Note that setting a standard URL (i.e. not in the ``packaged:`` scheme)
358 to a backend-stored or packaged URL will discard its data. On the
359 other hand, changing from backend-store to packaged and vice-versa
360 keeps the data.
361
362 Finally, note that setting the URL to one in the ``packaged:`` model
363 will automatically create a temporary directory and set the
364 PACKAGED_ROOT metadata of the package to that directory.
365 """
366 r = self.__url
367 if r is None:
368 self._load_content_info()
369 r = self.__url
370 return r
371
372 @autoproperty
373 - def _set_content_url(self, url):
374 """See `_get_content_url`."""
375 if self.__url is None:
376 self._load_content_info()
377 if url == self.__url:
378 return
379 self._check_content(url=url)
380 oldurl = self.__url
381 if oldurl.startswith("packaged:"):
382
383 f = self.__as_synced_file
384 if f:
385 f.close()
386 del self.__as_synced_file
387 rootdir = self._owner.get_meta(PACKAGED_ROOT)
388 pname = url2pathname(oldurl[10:])
389 fname = path.join(rootdir, pname)
390 if not url or url.startswith("packaged:"):
391 f = open(fname)
392 self.__data = f.read()
393 f.close()
394 unlink(fname)
395
396 if url.startswith("packaged:"):
397 rootdir = self._owner.get_meta(PACKAGED_ROOT, None)
398 if rootdir is None:
399 rootdir = create_temporary_packaged_root(self._owner)
400 if self.__data:
401 seq = url.split("/")[1:]
402 dir = recursive_mkdir(rootdir, seq[:-1])
403 fname = path.join(dir, seq[-1])
404 f = open(fname, "w")
405 f.write(self.__data)
406 f.close()
407 if url:
408 if self.__data:
409 del self.__data
410
411
412
413 self.emit("pre-modified::content_url", "content_url", url)
414 self.__url = url
415 self.__store_info()
416 if not url:
417 self.__store_data()
418 self.emit("modified::content_url", "content_url", url)
419
420 @autoproperty
422 """This property holds the data of the content.
423
424 It can be read whatever the kind of content (backend-stored, external
425 or packaged). However, only backend-stored and packaged can have their
426 data safely modified. Trying to set the data of an external content
427 will raise a `ValueError`. Its `content_url` must first be set to the
428 empty string or a ``packaged:`` URL.
429
430 See also `get_content_as_synced_file`.
431 """
432 url = self.__url
433 if url is None:
434 self._load_content_info()
435 url = self.__url
436 f = self.__as_synced_file
437 if f:
438
439 pos = f.tell()
440 f.seek(0)
441 r = f.read()
442 f.seek(pos)
443 elif url:
444 f = self.get_content_as_synced_file()
445 r = f.read()
446 f.close()
447 else:
448 r = self.__data
449 if r is None:
450 op = self._owner
451 r = self.__data = op._backend. \
452 get_content_data(op._id, self._id, self.ADVENE_TYPE)
453 return r
454
455 @autoproperty
456 - def _set_content_data(self, data):
457 """ See `_get_content_data`."""
458 url = self.__url
459 if url is None:
460 self._load_content_info()
461 url = self.__url
462 if self.__mimetype == "x-advene/none":
463 raise ModelError("Can not set data of empty content")
464 if url.startswith("packaged:"):
465 f = self.get_content_as_synced_file()
466 diff = None
467 f.truncate()
468 f.write(data)
469 f.close()
470 else:
471 if url:
472 raise AttributeError("content has a url, can not set data")
473 elif self.__as_synced_file:
474 raise IOError("content already opened as a file")
475 diff = None
476 self.__data = data
477 self.__store_data()
478 self.emit("modified-content-data", diff)
479
480 @autoproperty
482 h = self.__handler
483 if h is not None:
484 return h.parse_content(self)
485 else:
486 return self._get_content_data()
487
488 @autoproperty
489 - def _set_content_parsed(self, parsed):
490 h = self.__handler
491 if h is not None:
492 return self._set_content_data(h.unparse_content(parsed))
493 else:
494 return self._set_content_data(self, parsed)
495
496 @autoproperty
498 """
499 This property returns a *copy* of this element's content data wrapped in
500 a file-like object.
501
502 Note that the returned file-like object may be writable, but the
503 written data will *not* be reflected back to the content. Also, if the
504 content data is modified between the moment where this method is called
505 and the moment the file-like object is actually read, thoes changes
506 will not be included in the read data.
507
508 For a synchronized file-like object, see `get_content_as_synced_file`.
509 """
510 return StringIO(self.content_data)
511
512 @autoproperty
513 - def _get_content(self):
514 """Return a `Content` instance representing the content."""
515 c = self.__cached_content()
516 if c is None:
517 c = Content(self)
518 self.__cached_content = ref(c)
519 return c
520
521
522 -class Content(object):
523 """A class for content objects.
524
525 This class may be deprecated in the future. All attributes and methods have
526 equivalent ones in WithContentMixin, with prefix "content_".
527 """
528
529 - def __init__(self, owner_element):
530 self._owner_elt = owner_element
531
532 - def get_model(self, default=None):
533 return self._owner_elt.get_content_model(default)
534
536 return self._owner_elt.get_content_as_synced_file()
537
538 @autoproperty
539 - def _get_mimetype(self):
540 return self._owner_elt._get_content_mimetype()
541
542 @autoproperty
543 - def _set_mimetype(self, mimetype):
544 return self._owner_elt._set_content_mimetype(mimetype)
545
546 @autoproperty
547 - def _get_is_textual(self):
548 """
549 This property indicates if this content's data can be handled as text.
550
551 It uses the mimetypes registered with
552 `advene.model.content.register.register_textual_mimetype`.
553 """
554 return self._owner_elt._get_content_is_textual()
555
556 @autoproperty
557 - def _get_model(self):
558 return self._owner_elt._get_content_model()
559
560 @autoproperty
561 - def _set_model(self, model):
562 return self._owner_elt._set_content_model(model)
563
564 @autoproperty
565 - def _get_model_id(self):
566 """The id-ref of the model, or None.
567
568 This is a read-only property giving the id-ref of the resource held
569 by `model`, or None if there is no model.
570
571 Note that this property is accessible even if the corresponding
572 model is unreachable.
573 """
574 return self._owner_elt._get_content_model_id()
575
576 @autoproperty
577 - def _get_url(self):
578 return self._owner_elt._get_content_url()
579
580 @autoproperty
581 - def _set_url(self, url):
582 return self._owner_elt._set_content_url(url)
583
584 @autoproperty
585 - def _get_data(self):
586 return self._owner_elt._get_content_data()
587
588 @autoproperty
589 - def _set_data(self, data):
590 return self._owner_elt._set_content_data(data)
591
592 @autoproperty
593 - def _get_parsed(self):
594 return self._owner_elt._get_content_parsed()
595
596 @autoproperty
597 - def _set_parsed(self, parsed):
598 return self._owner_elt._set_content_parsed(parsed)
599
600 @autoproperty
601 - def _get_as_file(self):
602 return self._owner_elt._get_content_as_file()
603
606 __slots__ = ["_element",]
613
615 self.seek(0)
616 self._element._WithContentMixin__data = self.read()
617 self._element._WithContentMixin__as_synced_file = None
618 file.close(self)
619 self._element = None
620
621
622 -class ContentDataFile(object):
623 """FIXME: missing docstring.
624
625 R/W file-like object
626 """
627 - def __init__ (self, element):
628 self._element = element
629 self._file = f = tmpfile()
630 self.flush = f.flush
631 self.fileno = f.fileno
632 self.isatty = f.isatty
633 self.read = f.read
634 self.readlines = f.readlines
635 self.xreadlines = f.xreadlines
636 self.seek = f.seek
637 self.tell = f.tell
638
639 f.write(element._WithContentMixin__data or "")
640 f.seek(0)
641
643 mimetype = self._element._get_content_mimetype()
644 return {"content-type": mimetype,}
645
647 self.seek(0)
648 self._element._WithContentMixin__data = self.read()
649 self._element._WithContentMixin__as_synced_file = None
650 self._file.close()
651 self._element = None
652
653 - def truncate(self, *args):
654 self._file.truncate(*args)
655 self._element._WithContentMixin__store_data()
656
657 - def write(self, str_):
658 self._file.write(str_)
659 self._element._WithContentMixin__store_data()
660
661 - def writelines(self, seq):
662 self._file.writelines(seq)
663 self._element._WithContentMixin__store_data()
664
665 @property
666 - def closed(self): return self._file.closed
667
668 @property
669 - def encoding(self): return self._file.encoding
670
671 @property
672 - def mode(self): return self._file.mode
673
674 @property
675 - def name(self): return self._file.name
676
677 @property
678 - def newlines(self): return self._file.newlines
679
680 @autoproperty
681 - def _get_softspace(self): return self._file.softspace
682
683 @autoproperty
684 - def _set_softspace(self, val): self._file.softspace = val
685
693