File Coverage

blib/lib/Document/Manager.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1              
2             =head1 NAME
3              
4             Document::Manager - A web service for managing documents in a central
5             repository.
6              
7             =head1 SYNOPSIS
8              
9             my $dms = new Document::Manager;
10              
11             $dms->checkout($dir, $doc_id, $revision);
12              
13             $dms->add();
14             $dms->checkin();
15             $dms->query();
16             $dms->revert();
17             $dms->lock();
18             $dms->unlock();
19             $dms->properties();
20             $dms->stats();
21              
22             print $dms->get_error();
23              
24             =head1 DESCRIPTION
25              
26             B provides a simple interface for managing a
27             collection of revision-controlled documents. A document is a collection
28             of one or more files that are checked out, modified, and checked back in
29             as a unit. Each revision of a document is numbered, and documents can
30             be reverted to older revisions if needed. A document can also have an
31             arbitrary set of metadata associated with it.
32              
33             =head1 FUNCTIONS
34              
35             =cut
36              
37             package Document::Manager;
38             @Document::Manager::ISA = qw(WebService::TicketAuth::DBI);
39              
40 1     1   6711 use strict;
  1         3  
  1         42  
41 1     1   1182 use Config::Simple;
  1         31847  
  1         15  
42 1     1   555 use WebService::TicketAuth::DBI;
  0            
  0            
43             use Document::Repository;
44             use Document::Object;
45             use MIME::Base64;
46             use File::stat;
47             use File::Spec::Functions;
48             use DBI;
49             use SVG::Metadata;
50              
51             use vars qw($VERSION %FIELDS);
52             our $VERSION = '0.35';
53              
54             our $CONF = "/etc/webservice_dms/dms.conf";
55              
56             use base 'WebService::TicketAuth::DBI';
57             use fields qw(
58             repo_dir
59             repository
60             _error_msg
61             _debug
62             _dbh
63             );
64              
65              
66             =head2 new()
67              
68             Creates a new document manager object.
69              
70             =cut
71              
72             sub new {
73             my $class = shift;
74             my Document::Manager $self = fields::new($class);
75              
76             # Load up configuration parameters from config file
77             my %config;
78             my $errormsg = '';
79             if (! Config::Simple->import_from($CONF, \%config)) {
80             $errormsg = "Could not load config file '$CONF': " .
81             Config::Simple->error()."\n";
82             }
83              
84             $self->SUPER::new(%config);
85              
86             if (defined $config{'repo_dir'}) {
87             $self->{'repo_dir'} = $config{'repo_dir'};
88             }
89              
90             $self->{repository} = new Document::Repository( repository_dir => $self->{'repo_dir'} );
91              
92             if (! $self->{repository}) {
93             $self->_set_error("Could not connect to repository\n");
94             warn "Error: Could not establish connection to repository\n";
95             }
96              
97             return $self;
98             }
99              
100             sub _repo {
101             my $self = shift;
102              
103             if (! defined $self->{repository}) {
104             $self->{'repository'} =
105             new Document::Repository( repository_dir => $self->{'repo_dir'} );
106             }
107             return $self->{'repository'};
108             }
109              
110             # Internal routine for setting the error message
111             sub _set_error {
112             my $self = shift;
113             $self->{'_error_msg'} = shift;
114             }
115              
116             =head2 get_error()
117              
118             Retrieves the most recent error message
119              
120             =cut
121              
122             sub get_error {
123             my $self = shift;
124             return $self->{'_error_msg'};
125             }
126              
127             =head2 checkout()
128              
129             Checks out a copy of the document specified by $doc_id, placing
130             a copy into the directory specified by $dir. By default it will
131             return the most recent revision, but a specific revision can be
132             retrieved by specifying $revision.
133              
134             Returns the filename(s) copied into $dir on success. If there is an
135             error, it returns undef. The error message can be retrieved via
136             get_error().
137              
138             =cut
139              
140             sub checkout {
141             my $self = shift;
142             my $dir = shift;
143             my $doc_id = shift;
144             my $revision = shift;
145             $self->_set_error('');
146              
147             if (! $doc_id || $doc_id !~ /^\d+/) {
148             $self->_set_error("Invalid doc_id specified to checkout()");
149             return undef;
150             }
151              
152             if (! $dir || ! -d $dir) {
153             $self->_set_error("Invalid dir specified to checkout()");
154             return undef;
155             }
156              
157             return $self->_repo()->get($doc_id, $revision, $dir);
158             }
159              
160             =head2 add()
161              
162             Takes a hash of filenames => content pairs, and inserts each into the
163             dms as a separate document. The documents are scanned for valid RDF
164             metadata which, if present, will be made available for use in the
165             system. [Note that for metadata, currently only SVG documents are
166             supported.]
167              
168             Returns the new ID number of the document added, or undef if failed.
169              
170             =cut
171              
172             sub add {
173             my $self = shift;
174              
175             my (%files) = (@_);
176             my @doc_ids;
177             my $doc_id;
178             my ($sec, $min, $hr, $day, $month, $year) = (gmtime)[0..5];
179             my $now = sprintf("%04s-%02s-%02s %02s:%02s:%02s",
180             $year+1900, $month+1, $day, $hr, $min, $sec);
181             foreach my $filename (keys %files) {
182             my $content = $files{$filename};
183             next unless $content;
184             ($filename) = (File::Spec->splitpath($filename))[2];
185             my $local_filename = catfile('/tmp', $filename);
186             my $decoded = decode_base64($content);
187             if (! open(FILE, ">$local_filename") ) {
188             warn "Error: Could not open file '$local_filename' for writing: $!\n";
189             next;
190             }
191             binmode(FILE);
192             print FILE $decoded;
193             if (! close(FILE) ) {
194             warn "Error: Could not close file '$local_filename': $!\n";
195             }
196              
197             $doc_id = $self->_repo()->add($local_filename);
198             if ($doc_id) {
199             push @doc_ids, $doc_id;
200             } else {
201             $self->_set_error($self->_repo()->get_error());
202             }
203              
204             # Generate metadata
205             my %properties;
206             # TODO: Determine file type. For now assume SVG
207             my $format = 'svg';
208              
209             # Based on file type, extract metadata
210             if ($format eq 'svg') {
211             my $svgmeta = new SVG::Metadata;
212             if (! $svgmeta->parse($local_filename) ) {
213             $self->_set_error($svgmeta->errormsg());
214             warn $svgmeta->errormsg()."\n";
215             }
216             $properties{title} = $svgmeta->title();
217             $properties{author} = $svgmeta->author();
218             $properties{creator} = $svgmeta->creator();
219             $properties{creator_url} = $svgmeta->creator_url();
220             $properties{owner} = $svgmeta->owner();
221             $properties{owner_url} = $svgmeta->owner_url();
222             $properties{publisher} = $svgmeta->publisher();
223             $properties{publisher_url} = $svgmeta->publisher_url();
224             $properties{license} = $svgmeta->license();
225             $properties{license_date} = $svgmeta->license_date();
226             $properties{description} = $svgmeta->description();
227             $properties{language} = $svgmeta->language();
228             $properties{keywords} = join('; ', $svgmeta->keywords());
229             }
230              
231             $properties{title} ||= $filename;
232              
233             my $inode = stat($local_filename);
234              
235             $properties{state} = 'new';
236             $properties{size} = $inode->size;
237             $properties{date} = $now;
238             $properties{mimetype} = `file -bi $local_filename`; # TODO: PApp::MimeType?
239             chomp $properties{mimetype};
240              
241             if (! $self->properties($doc_id, %properties) ) {
242             warn "Error: ".$self->get_error()."\n";
243             }
244              
245             # Remove the temporary file
246             unlink($local_filename);
247             }
248              
249             return $doc_id;
250             }
251              
252             =head2 checkin()
253              
254             Commits a new revision to the document. Returns the document's new
255             revision number, or undef if failed.
256              
257             =cut
258              
259             sub checkin {
260             my $self = shift;
261             my $doc_id = shift;
262             my @files = @_;
263              
264             # Given a valid document id,
265             if (! $doc_id || $doc_id != /^\d+/) {
266             $self->_set_error("Invalid doc_id specified to checkout()");
267             return undef;
268             }
269              
270             my $new_revision = $self->_repo()->put($doc_id, @files);
271              
272             # TODO log / trigger notifications
273             return $new_revision;
274             }
275              
276             =head2 query()
277              
278             Returns a list of documents with property constraints meeting certain
279             conditions.
280              
281             Note: Currently this function is unimplemented, and simply returns a
282             list of all document IDs.
283              
284             =cut
285              
286             sub query {
287             my $self = shift;
288              
289             # Pass in a function pointer we'll use for determine matching docs
290             # Could we cache properties? Store in a database? Or is that higher level?
291             # Return list of matching documents
292              
293             my @objs = $self->_repo()->documents();
294              
295             # SELECT id FROM document WHERE $criteria
296             # $criteria could be: keywords, author, latest N
297              
298             return \@objs;
299             }
300              
301              
302             =head2 properties()
303              
304             Gets or updates the properties for a given document id. Returns undef
305             on error, such as if an invalid document id is given.
306              
307             =cut
308              
309             sub properties {
310             my $self = shift;
311             my $doc_id = shift;
312              
313             # Given a valid document id
314             if (! $doc_id || ($doc_id !~ /^\d+/)) {
315             $self->_set_error("Invalid doc_id specified to properties()");
316             print "Document id '$doc_id' provided to properties()\n";
317             return undef;
318             }
319              
320             # Retrieve the properties for this document
321             my $doc = new Document::Object(repository => $self->_repo(),
322             doc_id => $doc_id);
323             if (@_ > 1) {
324             return $doc->set_properties(@_);
325             } else {
326             return $doc->get_properties();
327             }
328             }
329              
330             =head2 stats()
331              
332             Returns a hash containing statistics about the document repository as a
333             whole, including the following:
334              
335             * Stats from Document::Repository::stats()
336             * Number of pending documents
337             * Number of documents new today
338             * Number of authors
339              
340             Note: Currently this is unimplemented.
341              
342             =cut
343              
344             sub stats {
345             my $self = shift;
346              
347             my $stats = $self->_repo()->stats();
348              
349             $stats->{num_pending_docs} = 0; # TODO
350             $stats->{num_new_today_docs} = 0; # TODO
351             $stats->{num_authors} = 0; # TODO
352              
353             return $stats;
354             }
355              
356             =head2 state(doc_id[, state[, comment]])
357              
358             Gets or sets the state of document in the system. Returns undef if the
359             specified doc_id does not exist, or does not have a valid state set.
360              
361             The following states are allowed:
362              
363             new
364             open
365             accepted
366             rejected
367             broken
368             retired
369              
370             =cut
371              
372             sub state {
373             my $self = shift;
374             my $doc_id = shift;
375             my $state = shift;
376             my $comment = shift;
377              
378             if (! $doc_id) {
379             $self->_set_error("No doc_id specified to Document::Manager::state\n");
380             return undef;
381             }
382              
383             my $doc = new Document::Object(repository => $self->_repo(),
384             doc_id => $doc_id);
385             $state = $doc->state($state);
386             if (! $state) {
387             $self->_set_error($doc->get_error());
388             return undef;
389             }
390              
391             if (defined $comment) {
392             $doc->log($comment);
393             }
394              
395             return $state;
396             }
397              
398             sub metrics_pending_docs {
399             my $self = shift;
400             return 'Unimplemented';
401             }
402              
403             sub metrics_new_docs_today {
404             my $self = shift;
405             return 'Unimplemented';
406             }
407              
408             sub metrics_new_docs_this_month {
409             my $self = shift;
410             return 'Unimplemented';
411             }
412              
413             sub metrics_authors {
414             my $self = shift;
415             return 'Unimplemented';
416             }
417              
418             =head2 keyword_add($doc_id, @keywords)
419              
420             Adds a given keyword or list of keywords to the document's metadata.
421             Returns undef if the keywords could not be added; the error can be
422             retrieved from get_error().
423              
424             Leading and trailing spaces are stripped. Any ';' characters in the
425             keywords will be converted to ',' characters. The keywords are also
426             lowercased.
427              
428             Note: Currently this does not add the keywords to the original SVG file,
429             only the metadata in the document system.
430              
431             =cut
432              
433             sub keyword_add {
434             my $self = shift;
435             my $doc_id = shift;
436             my @keywords = @_;
437              
438             if (! defined $doc_id) {
439             $self->_set_error("No doc_id specified to Document::Manager::keyword_add\n");
440             return undef;
441             }
442              
443             if (@keywords < 1) {
444             $self->_set_error("No keywords specified to Document::Manager::keyword_add\n");
445             return undef;
446             }
447              
448             my $doc = new Document::Object(repository => $self->_repo(),
449             doc_id => $doc_id);
450              
451             # Ensure we have the unique union of keywords
452             my $retval = $doc->add_keywords(@keywords);
453              
454             $doc->log("Added keywords: @keywords\n");
455              
456             return $retval;
457             }
458              
459             =head2 keyword_remove()
460              
461             Removes one or more keywords from the document metadata, if present.
462              
463             Note: Currently this does not actually alter the SVG file itself.
464              
465             =cut
466              
467             sub keyword_remove {
468             my $self = shift;
469             my $doc_id = shift;
470              
471             my $doc = new Document::Object(repository => $self->_repo(),
472             doc_id => $doc_id);
473              
474             my $retval = $doc->remove_keywords(@_);
475              
476             $doc->log("Removed keywords: @_\n");
477             return $retval;
478             }
479              
480             1;