Package advene :: Package model :: Package core :: Module content
[hide private]
[frames] | no frames]

Source Code for Module advene.model.core.content

  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 
30 31 -class WithContentMixin:
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 # backend data, unless __as_synced_file is not 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 # it happens that luring _check_content into using cls as self works 153 # and prevents us from writing redundant code 154 _func(cls, mimetype, model, url)
155
156 - def _update_content_handler(self):
157 """See :class:`WithContentMixin` documentation.""" 158 # the following updates the handler for content_parsed 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
171 - def _load_content_info(self):
172 """Load the content info (mimetype, model, url).""" 173 # should actually never be called if _instantiate is used. 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 # NB: if the default value is _RAISE and the model is unreachable, 201 # and exception will be raised. 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: # non-empty string 233 if url.startswith("packaged:"): 234 # special URL scheme 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
253 - def _get_content_mimetype(self):
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
283 - def _get_content_is_textual(self):
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
300 - def _get_content_model(self):
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
337 - def _get_content_model_id(self):
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
351 - def _get_content_url(self):
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: # should not happen, but that's safer 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: # should not happen, but safer 376 self._load_content_info() 377 if url == self.__url: 378 return # no need to perform the complicate things below 379 self._check_content(url=url) 380 oldurl = self.__url 381 if oldurl.startswith("packaged:"): 382 # delete packaged data 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 # don't need to remove data from backend, that will be done 411 # when setting URL 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
421 - def _get_content_data(self):
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: # should not happen, but that's safer 434 self._load_content_info() 435 url = self.__url 436 f = self.__as_synced_file 437 if f: # backend data or "packaged:" url 438 # NB: this is not threadsafe 439 pos = f.tell() 440 f.seek(0) 441 r = f.read() 442 f.seek(pos) 443 elif url: # non-empty string 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: # should not happen, but that's safer 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 # TODO make a diff object 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 # TODO make a diff object 476 self.__data = data 477 self.__store_data() 478 self.emit("modified-content-data", diff)
479 480 @autoproperty
481 - def _get_content_parsed(self):
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
497 - def _get_content_as_file(self):
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
535 - def get_as_synced_file(self):
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
604 605 -class PackagedDataFile(file):
606 __slots__ = ["_element",]
607 - def __init__(self, filename, element):
608 if path.exists(filename): 609 file.__init__ (self, filename, "r+") 610 else: 611 file.__init__ (self, filename, "w+") 612 self._element = element
613
614 - def close(self):
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
642 - def info(self):
643 mimetype = self._element._get_content_mimetype() 644 return {"content-type": mimetype,}
645
646 - def close(self):
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
686 687 -def create_temporary_packaged_root(package):
688 d = mkdtemp() 689 tempdir_list.append(d) 690 package.set_meta(PACKAGED_ROOT, d) 691 # TODO use notification to clean it when package is closed 692 return d
693