File Coverage

blib/lib/WebService/Cmis/Document.pm
Criterion Covered Total %
statement 10 12 83.3
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 14 16 87.5


line stmt bran cond sub pod time code
1             package WebService::Cmis::Document;
2              
3             =head1 NAME
4              
5             WebService::Cmis::Document - Representation of a cmis document
6              
7             =head1 DESCRIPTION
8              
9             Document objects are the elementary information entities managed by the repository.
10             This class represents a document object as returned by a L.
11              
12             See CMIS specification document 2.1.4 Document Object
13              
14             Parent class: L
15              
16             =cut
17              
18 1     1   43816 use strict;
  1         4  
  1         51  
19 1     1   9 use warnings;
  1         2  
  1         49  
20 1     1   6 use WebService::Cmis qw(:collections :contenttypes :namespaces :relations :utils);
  1         3  
  1         442  
21 1     1   103 use WebService::Cmis::Object ();
  0            
  0            
22             use WebService::Cmis::NotImplementedException ();
23             use WebService::Cmis::NotSupportedException ();
24             use XML::LibXML qw(:libxml);
25             use Error qw(:try);
26             our @ISA = ('WebService::Cmis::Object');
27              
28             our $CMIS_XPATH_CONTENT_LINK = new XML::LibXML::XPathExpression('//*[local-name() = "content" and namespace-uri() = "'.ATOM_NS.'"]/@src');
29             our $CMIS_XPATH_RENDITIONS = new XML::LibXML::XPathExpression('//*[local-name()="object" and namespace-uri()="'.CMISRA_NS.'"]/*[local-name()="rendition" and namespace-uri()="'.CMIS_NS.'"]');
30              
31             =head1 METHODS
32              
33             =over 4
34              
35             =cut
36              
37             # clear document-specific caches
38             sub _initData {
39             my $this = shift;
40              
41             $this->SUPER::_initData();
42             undef $this->{renditions};
43             }
44              
45             =item checkOut() -> $pwc
46              
47             performs a checkOut on this document and returns the
48             Private Working Copy (PWC), which is also an instance of
49             Document
50              
51             See CMIS specification document 2.2.7.1 checkOut
52              
53             =cut
54              
55             sub checkOut {
56             my $this = shift;
57              
58             require WebService::Cmis::Property;
59              
60             # get the checkedout collection URL
61             my $checkoutUrl = $this->{repository}->getCollectionLink(CHECKED_OUT_COLL);
62             throw Error::Simple("Could not determine the checkedout collection url.") unless defined $checkoutUrl;
63              
64             # get this document's object ID
65             # build entry XML with it
66             my $entryXmlDoc = $this->{repository}->createEntryXmlDoc(
67             properties => [
68             WebService::Cmis::Property::newId(
69             id=>"cmis:objectId",
70             queryName=>"cmis:objectId",
71             value=>$this->getId
72             )
73             ]
74             );
75              
76             #print STDERR "entryXmlDoc=".$entryXmlDoc->toString(1)."\n";
77              
78             # post it to to the checkedout collection URL
79             my $result = $this->{repository}{client}->post($checkoutUrl, $entryXmlDoc->toString, ATOM_XML_ENTRY_TYPE);
80              
81             $this->{xmlDoc} = $result;
82             $this->_initData;
83             $this->reload;
84              
85             return $this;
86             }
87              
88             =item isCheckedOut() -> $boolean
89              
90             Returns true if the document is checked out.
91              
92             =cut
93              
94             sub isCheckedOut {
95             my $this = shift;
96              
97             my $prop = $this->getProperties->{'cmis:isVersionSeriesCheckedOut'};
98             return 0 unless defined $prop;
99             return $prop->getValue;
100             }
101              
102              
103             =item getCheckedOutBy() -> $userId
104              
105             returns the ID who currently has the document checked out.
106              
107             =cut
108              
109             sub getCheckedOutBy {
110             my $this = shift;
111              
112             my $prop = $this->getProperties->{'cmis:versionSeriesCheckedOutBy'};
113             return unless defined $prop;
114             return $prop->getValue;
115             }
116              
117             =item getPrivateWorkingCopy() -> L<$cmisDocument|WebService::Cmis::Document>
118              
119             retrieves the object using the object ID in the property:
120             cmis:versionSeriesCheckedOutId then uses getObject to instantiate
121             the object.
122              
123             =cut
124              
125             sub getPrivateWorkingCopy {
126             my $this = shift;
127              
128             my $pwcDocId = $this->getProperty('cmis:versionSeriesCheckedOutId');
129             return unless $pwcDocId;
130             return $this->{repository}->getObject($pwcDocId);
131             }
132              
133             =item cancelCheckOut() -> L<$this|WebService::Cmis::Document>
134              
135             cancels the checkout of this object by retrieving the Private Working
136             Copy (PWC) and then deleting it. After the PWC is deleted, this object
137             will be reloaded to update properties related to a checkout.
138              
139             See CMIS specification document 2.2.7.2 cancelCheckOut
140              
141             =cut
142              
143             sub cancelCheckOut {
144             my $this = shift;
145              
146             my $pwcDoc = $this->getPrivateWorkingCopy;
147             $pwcDoc->delete if defined $pwcDoc;
148              
149             return $this->getLatestVersion;
150             }
151              
152             =item checkIn($checkinComment, %params) -> $this
153              
154             checks in this Document which must be a private
155             working copy (PWC).
156              
157             See CMIS specification document 2.2.7.3 checkIn
158              
159             The following optional arguments are supported:
160              
161             =over 4
162              
163             =item * major
164              
165             =back
166              
167             These aren't supported:
168              
169             =over 4
170              
171             =item * properties
172              
173             =item * contentStream
174              
175             =item * policies
176              
177             =item * addACEs
178              
179             =item * removeACEs
180              
181             =back
182              
183             TODO: support repositories without PWCUpdate capabilities
184              
185             =cut
186              
187             sub checkIn {
188             my $this = shift;
189             my $checkinComment = shift;
190              
191             # build an empty ATOM entry
192             my $xmlDoc = new XML::LibXML::Document('1.0', 'UTF-8');
193             my $entryElement = $xmlDoc->createElementNS(ATOM_NS, "entry");
194             $xmlDoc->setDocumentElement($entryElement);
195              
196             # Get the self link
197             # Do a PUT of the empty ATOM to the self link
198             my $url = $this->getSelfLink;
199              
200             my $result = $this->{repository}{client}->put($url, $xmlDoc->toString, ATOM_XML_TYPE,
201             "checkin"=>'true',
202             "checkinComment"=>$checkinComment,
203             @_ # here goes our params
204             );
205              
206             # reload the current object with the result
207             $this->{xmlDoc} = $result;
208             $this->_initData;
209             $this->reload;
210              
211             return $this;
212             }
213              
214             =item getContentLink(%params) -> $url
215              
216             returns the source link to this document
217              
218             The params are added to the url.
219              
220             =cut
221              
222             sub getContentLink {
223             my $this = shift;
224             my %params = @_;
225              
226             my $url = $this->_getDocumentElement->find($CMIS_XPATH_CONTENT_LINK);
227             $url = $this->getLink('enclosure') unless defined $url;
228             return unless defined $url;
229             $url = "".$url;
230              
231             my $gotUrlParams = ($url =~ /\?/)?1:0;
232              
233             foreach my $key (keys %params) {
234             if ($gotUrlParams) {
235             $url .= '&';
236             } {
237             $url .= '?';
238             $gotUrlParams = 1;
239             }
240             $url .= $key.'='._urlEncode($params{$key});
241             }
242              
243             return $url;
244             }
245              
246             =item getContentStream($streamId) -> $data
247              
248             returns the CMIS service response from invoking the 'enclosure' link.
249             it will return the binary content of the document stored on the server.
250              
251             The optional argument:
252              
253             =over 4
254              
255             =item * streamId: id of the content rendition (TODO: not implemented yet)
256              
257             =back
258              
259             See CMIS specification document 2.2.4.10 getContentStream
260              
261             my $doc = $repo->getObjectByPath("/User homes/jeff/sample.pdf");
262             my $content = $doc->getContentStream;
263              
264             my $FILE;
265             unless (open($FILE, '>', $name)) {
266             die "Can't create file $name - $!\n";
267             }
268             print $FILE $text;
269             close($FILE);
270              
271              
272             =cut
273              
274             sub getContentStream {
275             my $this = shift;
276            
277             my $url = $this->getContentLink;
278              
279             if ($url) {
280             # if the url exists, follow that
281             #print STDERR "url=$url\n";
282              
283             my $client = $this->{repository}{client};
284             $client->GET($url, @_);
285              
286             my $code = $client->responseCode;
287             return $client->responseContent if $code >= 200 && $code < 300;
288             $client->processErrors;
289             } else {
290             # otherwise, try to return the value of the content element
291             return $this->_getDocumentElement->findvalue("./*[local-name() = 'content' and namespace-uri() = '".ATOM_NS."']");
292             }
293              
294             # never reach
295             return;
296             }
297              
298             =item getAllVersions(%params) -> $atomFeed
299              
300             returns a AtomFeed` of document objects for the entire
301             version history of this object, including any PWC's.
302              
303             See CMIS specification document 2.2.7.5 getAllVersions
304              
305             The optional filter and includeAllowableActions are
306             supported.
307              
308             TODO: is it worth caching these inside?
309              
310             =cut
311              
312             sub getAllVersions {
313             my $this = shift;
314              
315             # get the version history link
316             my $versionsUrl = $this->getLink(VERSION_HISTORY_REL);
317              
318             # invoke the URL
319             my $result = $this->{repository}{client}->get($versionsUrl, @_);
320              
321             # return the result set
322             require WebService::Cmis::AtomFeed::Objects;
323             return new WebService::Cmis::AtomFeed::Objects(repository=>$this->{repository}, xmlDoc=>$result);
324             }
325              
326             =item getRenditions(%params) -> %renditions
327              
328             returns a hash of associated Renditions for the specified object. Only
329             rendition attributes are returned, not rendition stream.
330              
331             The following optional arguments are currently supported:
332              
333             =over 4
334              
335             =item * renditionFilter
336              
337             =item * maxItems
338              
339             =item * skipCount
340              
341             =back
342              
343             A rendition has the following attributes:
344              
345             =over 4
346              
347             =item * streamId: Identifies the rendition stream
348              
349             =item * mimetype: The MIME type of the rendition stream
350              
351             =item * kind: A categorization String associated with the rendition
352            
353             =item * length: The length of the rendition stream in bytes (optional)
354            
355             =item * title: Human readable information about the rendition (optional)
356            
357             =item * height: Typically used for 'image' renditions (expressed as pixels).
358             SHOULD be present if kind = C (optional)
359            
360             =item * width: Typically used for 'image' renditions (expressed as pixels).
361             SHOULD be present if kind = C (optional)
362            
363             =item * renditionDocumentId: If specified, then the rendition can also be accessed as
364             a document object in the CMIS services. If not set, then the rendition can only
365             be accessed via the rendition services. Referential integrity of this ID is
366             repository-specific. (optional)
367              
368             =back
369              
370             See CMIS specification document 2.1.4.2 Renditions
371              
372             TODO: use
373              
374             =cut
375              
376             sub getRenditions {
377             my $this = shift;
378             my %params = @_;
379              
380             # if Renditions capability is None, return notsupported
381             unless ($this->{repository}->getCapabilities->{'Renditions'}) {
382             throw WebService::Cmis::NotSupportedException("This repository does not support Renditions");
383             }
384              
385             unless ($this->{renditions}) {
386             unless ($this->_getDocumentElement->exists($CMIS_XPATH_RENDITIONS)) {
387             # reload including renditions
388             $this->reload(renditionFilter=>'*');
389             }
390             my $elem = $this->_getDocumentElement;
391             $this->{renditions} = ();
392             foreach my $node ($elem->findnodes($CMIS_XPATH_RENDITIONS)) {
393             my $rendition = ();
394             foreach my $child ($node->childNodes) {
395             next unless $child->nodeType == XML_ELEMENT_NODE;
396             my $key = $child->localname;
397             my $val = $child->string_value;
398             #print STDERR "key=$key, value=".($val||'undef')."\n";
399             $rendition->{$key} = $val;
400             }
401             $this->{renditions}{$rendition->{streamId}} = $rendition;
402             }
403             }
404              
405             return $this->{renditions};
406             }
407              
408             =item getRenditionLink(%params)
409              
410             returns a link to the documents rendition
411              
412             Use the renditions properties to get a specific one (see L):
413              
414             =over 4
415              
416             =item * streamId
417              
418             =item * mimetype
419              
420             =item * kind
421              
422             =item * height
423              
424             =item * width
425              
426             =item * length
427              
428             =item * title
429              
430             =item * renditionDocumentId
431              
432             =back
433              
434             my $doc = $repo->getObjectByPath("/User homes/jeff/sample.pdf");
435             my $thumbnailUrl => $doc->getRenditionLink(kind=>"thumbnail");
436             my $iconUrl = $doc->getRenditionLink(kind=>"icon", width=>16);
437              
438             =cut
439              
440             sub getRenditionLink {
441             my $this = shift;
442             my %params = @_;
443              
444             # if Renditions capability is None, return notsupported
445             unless ($this->{repository}->getCapabilities->{'Renditions'}) {
446             throw WebService::Cmis::NotSupportedException("This repository does not support Renditions");
447             }
448              
449             my $renditions = $this->getRenditions;
450             foreach my $rendi (values %$renditions) {
451             my $found = 1;
452             foreach my $key (keys %params) {
453             if (defined $rendi->{$key} && $rendi->{$key} !~ /$params{$key}/i) {
454             $found = 0;
455             last;
456             }
457             }
458             next unless $found;
459              
460             return $this->getContentLink(streamId=>$rendi->{streamId});
461             }
462              
463             return;
464             }
465              
466             =item getLatestVersion(%params) -> $document
467              
468             returns a cmis Document representing the latest version in the version series.
469              
470             See CMIS specification document 2.2.7.4 getObjectOfLatestVersion
471              
472             The following optional arguments are supported:
473              
474             =over 4
475              
476             =item * major
477              
478             =item * filter
479              
480             =item * includeRelationships
481              
482             =item * includePolicyIds
483              
484             =item * renditionFilter
485              
486             =item * includeACL
487              
488             =item * includeAllowableActions
489              
490             =back
491              
492             $latestDoc = $doc->getLatestVersion;
493             $latestDoc = $doc->getLatestVersion(major=>1);
494              
495             print $latestDoc->getProperty("cmis:versionLabel")."\n";
496              
497             =cut
498              
499             sub getLatestVersion {
500             my $this = shift;
501             my %params = @_;
502            
503             my $major = delete $params{major};
504             $params{returnVersion} = $major?'latestmajor':'latest';
505              
506             $this->_initData;
507              
508             $params{id} = $this->getProperty("cmis:versionSeriesId");
509             return $this->reload(%params);
510             }
511              
512             =item copy($targetFolder, $propertyList, $versionState) -> $cmisDocument
513              
514             TODO: This is not yet implemented.
515              
516             Creates a document object as a copy of the given source document in the (optionally)
517             specified location.
518              
519             The $targetFolder specifies the folder that becomes the parent
520             of the new document. This parameter must be specified if the repository does not
521             have the "unfiling" capability.
522              
523             The $propertyList is a list of WebService::Cmis::Property objects optionally specifieds
524             the propeties about to change in the newly created Document object.
525              
526             Valid values for $versionState are:
527              
528             =over 4
529              
530             =item * none: the document is created as a non-versionable object
531              
532             =item * checkedout: the document is created in checked-out state
533              
534             =item * major (default): the document is created as a new major version
535              
536             =item * minor: the document is created as a minor version
537              
538             =back
539              
540             The following optional arguments are not yet supported:
541              
542             =over 4
543              
544             =item * policies
545              
546             =item * addACEs
547              
548             =item * removeACEs
549              
550             =back
551              
552             See CMIS specification document 2.2.4.2 (createDocumentFromSource)
553              
554             =cut
555              
556             sub copy { throw WebService::Cmis::NotImplementedException; }
557              
558             =item getPropertiesOfLatestVersion
559              
560             TODO: This is not yet implemented.
561              
562             =cut
563              
564             sub getPropertiesOfLatestVersion { throw WebService::Cmis::NotImplementedException; }
565              
566             =item setContentStream(%params) -> $this
567              
568             This sets the content stream of a document.
569              
570             The following parameters are supported:
571              
572             =over 4
573              
574             =item * contentFile: the absolute path to the file to be used.
575              
576             =item * contentData: the data to be posted to the documents content stream link.
577             use either C or C.
578              
579             =item * contentType: the mime type of the data. will be guessed automatically if not specified manually.
580              
581             =item * overwriteFlag: if 'true' (default), replace the existing content stream.
582             if 'false', set the input contentStream if the object currently does not have a content-stream.
583              
584             =item * changeToken
585              
586             =back
587              
588             See CMIS specification document 2.2.4.18 setContentStream
589              
590             =cut
591              
592             sub setContentStream {
593             my $this = shift;
594             my %params = @_;
595              
596             my $contentStreamUpdatability = $this->{repository}->getCapabilities->{'ContentStreamUpdatability'};
597             #print STDERR "contentStreamUpdatability=$contentStreamUpdatability\n";
598              
599             throw WebService::Cmis::NotSupportedException("This repository does not allow to set the content stream")
600             if $contentStreamUpdatability eq 'none';
601              
602             my $contentFile = delete $params{contentFile};
603             my $contentData = delete $params{contentData};
604             my $contentType = delete $params{contentType};
605              
606             unless (defined $contentData) {
607             my $fh;
608              
609             open($fh, '<', $contentFile)
610             or throw Error::Simple("can't open file $contentFile"); # SMELL: use a custom exception
611              
612             local $/ = undef;# set to read to EOF
613             $contentData = <$fh>;
614             close($fh);
615             $contentData = '' unless $contentData; # no undefined
616             }
617              
618             unless (defined $contentType) {
619              
620             # get the file mage used for checking
621             require File::MMagic;
622             my $fileMage = new File::MMagic;
623              
624             $contentType = $fileMage->checktype_contents($contentData);
625              
626             # contentType fallback
627             $contentType = 'application/binary' unless defined $contentType;
628             }
629              
630              
631             # SMELL: not sure whether we need to encode or not
632             #require MIME::Base64;
633             #$contentData = MIME::Base64::encode_base64($contentData);
634              
635             my $url = $this->getContentLink;
636             throw Error::Simple("Can't find content link for ".$this->getId) unless $url;
637              
638             # SMELL: CMIS specification document 2.2.4.18 and 3.1.9 don't agree whether to return the new object Id or not.
639             # In addition it is not clear whether setContentStream should create a new revision or not.
640             # So we make sure the document is checked out and will create a new minor version at least.
641              
642             if ($contentStreamUpdatability eq 'pwconly') {
643             $this->checkOut unless $this->isCheckedOut;
644             }
645              
646             my $result = $this->{repository}{client}->put($url, $contentData, $contentType, %params) || '';
647              
648             # reload with this result
649             if (defined $result && $result ne '') {
650             $this->{xmlDoc} = $result;
651             $this->_initData;
652             }
653              
654             $this->reload;
655              
656             # make sure this is checked in
657             if ($contentStreamUpdatability eq 'pwconly') {
658             $this->checkIn("setting content stream", major=>0) if $this->isCheckedOut;
659             }
660              
661             return $this;
662             }
663              
664             =item deleteContentStream
665              
666             TODO: This is not yet implemented.
667              
668             See CMIS specification document 2.2.4.17 deleteContentStream
669              
670             =cut
671              
672             sub deleteContentStream { throw WebService::Cmis::NotImplementedException; }
673              
674             =back
675              
676             =head1 COPYRIGHT AND LICENSE
677              
678             Copyright 2012-2013 Michael Daum
679              
680             This module is free software; you can redistribute it and/or modify it under
681             the same terms as Perl itself. See F.
682              
683             =cut
684              
685             1;