File Coverage

blib/lib/Hopkins/Config/XML.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             package Hopkins::Config::XML;
2              
3 1     1   1051 use strict;
  1         3  
  1         47  
4 1     1   4 use warnings;
  1         1  
  1         44  
5              
6             =head1 NAME
7              
8             Hopkins::Config::XML - hopkins configuration via XML
9              
10             =head1 DESCRIPTION
11              
12             Hopkins::Config encapsulates all of the busywork associated
13             in the reading and post-processing of the XML configuration
14             in addition to providing a simple interface to accessing
15             values when required.
16              
17             Hopkins::Config::XML will validate your configuration using
18             XML Schema via XML::LibXML. for complete information on the
19             schema, see the XML Schema document in Hopkins::Config::XML.
20              
21             =head1 EXAMPLE
22              
23             <?xml version="1.0" encoding="utf-8"?>
24             <hopkins>
25             <state>
26             <root>/var/lib/hopkins</root>
27             </state>
28              
29             <plugin name="HMI">
30             <port>8088</port>
31             </plugin>
32              
33             <plugin name="RPC">
34             <port>8080</port>
35             </plugin>
36              
37             <database>
38             <dsn>dbi:mysql:database=hopkins;host=localhost</dsn>
39             <user>root</user>
40             <pass></pass>
41             <options>
42             <option name="AutoCommit" value="1" />
43             <option name="RaiseError" value="1" />
44             <option name="mysql_auto_reconnect" value="1" />
45             <option name="quote_char" value="" />
46             <option name="name_sep" value="." />
47             </options>
48             </database>
49              
50             <queue name="general">
51             <concurrency>16</concurrency>
52             </queue>
53              
54             <queue name="serial" onerror="halt">
55             <concurrency>1</concurrency>
56             </queue>
57              
58             <task name="Sum" onerror="disable">
59             <class>MyApp::Job::Sum</class>
60             <queue>general</queue>
61             </task>
62              
63             <task name="Report" onerror="disable" stack="no">
64             <class>MyApp::Job::Report</class>
65             <queue>serial</queue>
66             <schedule>
67             <cron>0 22 * 1-11 *</cron>
68             <cron>0 */4 * 12 * *</cron>
69             </schedule>
70             <options>
71             <option name="source" value="production" />
72             <option name="destination" value="reports@domain.com" />
73             </options>
74             <chain task="Sum">
75             <options>
76             <option name="categories" value="Books" />
77             <option name="categories" value="CDs" />
78             </options>
79             </chain>
80             </task>
81             </hopkins>
82              
83             =cut
84              
85 1     1   204 use DateTime::Set;
  0            
  0            
86             use DateTime::Event::MultiCron;
87             use File::Monitor;
88             use Path::Class::Dir;
89             use XML::Simple;
90             use XML::LibXML;
91             use YAML;
92              
93             use Hopkins::Config::Status;
94             use Hopkins::Task;
95              
96             use Class::Accessor::Fast;
97              
98             use base qw(Class::Accessor::Fast Hopkins::Config);
99              
100             __PACKAGE__->mk_accessors(qw(config file monitor xml xsd));
101              
102             sub new
103             {
104             my $self = shift->SUPER::new(@_);
105              
106             $self->monitor(new File::Monitor);
107             $self->monitor->watch($self->file);
108              
109             $self->xml(new XML::LibXML);
110             $self->xsd(new XML::LibXML::Schema string => join '', <DATA>);
111              
112             return $self;
113             }
114              
115             sub load
116             {
117             my $self = shift;
118              
119             Hopkins->log_debug('loading XML configuration file');
120              
121             my $status = new Hopkins::Config::Status;
122             my $config = $self->parse($self->file, $status);
123              
124             # if we have an existing configuration, then we will be
125             # fine. we won't overwrite the existing configuration
126             # with a broken one, so no error condition will exist.
127              
128             $status->ok($self->config ? 1 : 0);
129              
130             if (not defined $config) {
131             $status->failed(1);
132             $status->parsed(0);
133              
134             return $status;
135             }
136              
137             $status->parsed(1);
138              
139             if (my $root = $config->{state}->{root}) {
140             $config->{state}->{root} = new Path::Class::Dir $root;
141             eval { $config->{state}->{root}->mkpath(0, 0700) };
142             if (my $err = $@) {
143             Hopkins->log_error("unable to create $root: $@");
144             $status->failed(1);
145             }
146             } else {
147             Hopkins->log_error('no root directory defined for state information');
148             $status->failed(1)
149             }
150              
151             # process task configuration data structures. each task
152             # definition is inflated into a Hopkins::Task instance.
153             # schedules are inflated into DateTime::Set objects via
154             # DateTime::Event::MultiCron. other forms of schedule
155             # definitions may be supported in the future, so long as
156             # they grok DateTime::Set.
157              
158             foreach my $name (keys %{ $config->{task} }) {
159             my $href = $config->{task}->{$name};
160              
161             # collapse the damn task queue from the ForceArray
162             # and interpret the value of the enabled attribute
163              
164             $href->{queue} = $href->{queue}->[0] if ref $href->{queue};
165             $href->{enabled} = lc($href->{enabled}) eq 'no' ? 0 : 1;
166              
167             my $task = new Hopkins::Task { name => $name, %$href };
168              
169             if (not $task->queue) {
170             Hopkins->log_error("task $name not assigned to a queue");
171             $status->failed(1);
172             }
173              
174             if (not $task->class || $task->cmd) {
175             Hopkins->log_error("task $name lacks a class or command line");
176             $status->failed(1);
177             }
178              
179             if ($task->class and $task->cmd) {
180             Hopkins->log_error("task $name using mutually exclusive class/cmd");
181             $status->failed(1);
182             }
183              
184             $task->stack(1) if $task->stack and $task->stack eq 'no';
185             $task->options([ $self->_setup_options($status, $task->options) ]);
186             $task->schedule($self->_setup_schedule($status, $task));
187              
188             $config->{task}->{$name} = $task;
189             }
190              
191             $self->_setup_chains($config, $status, values %{ $config->{task} });
192              
193             # check to see if the new configuration includes a
194             # modified database configuration.
195              
196             if (my $href = $self->config && $self->config->{database}) {
197             my @a = map { $href->{$_} || '' } qw(dsn user pass options);
198             my @b = map { $config->{database}->{$_} } qw(dsn user pass options);
199              
200             # replace the options hashref (very last element in
201             # the array) with a flattened representation
202              
203             splice @a, -1, 1, keys %{ $a[-1] }, values %{ $a[-1] };
204             splice @b, -1, 1, keys %{ $b[-1] }, values %{ $b[-1] };
205              
206             # temporarily change the list separator character
207             # (default 0x20, a space) to the subscript separator
208             # character (default 0x1C) for a precise comparison
209             # of the two configurations
210              
211             local $" = $;;
212              
213             $status->store_modified("@a" ne "@b");
214             }
215              
216             if (not $status->failed) {
217             $self->config($config);
218             $status->updated(1);
219             $status->ok(1);
220             }
221              
222             return $status;
223             }
224              
225             sub _setup_chains
226             {
227             my $self = shift;
228             my $config = shift;
229             my $status = shift;
230              
231             while (my $task = shift) {
232             my @chain;
233              
234             next if not defined $task->chain;
235              
236             foreach my $href (@{ $task->chain }) {
237             my $name = $href->{task};
238             my $next = $config->{task}->{$name};
239              
240             if (not defined $next) {
241             Hopkins->log_error("chained task $name for " . $task->name . " not found");
242             $status->failed(1);
243             }
244              
245             my $task = new Hopkins::Task $next;
246              
247             $task->options($href->{options});
248             $task->chain($href->{chain});
249             $task->schedule(undef);
250              
251             push @chain, $task;
252             }
253              
254             $self->_setup_chains($config, $status, @chain);
255              
256             $task->chain(\@chain);
257             }
258             }
259              
260             sub _setup_options
261             {
262             my $self = shift;
263             my $status = shift;
264             my $options = shift || {};
265              
266             return map { $self->_setup_option($_ => $options->{$_}) } keys %$options;
267             }
268              
269             sub _setup_option
270             {
271             my $self = shift;
272             my $name = shift;
273             my $attrs = shift;
274              
275             $attrs = { value => $attrs } if not ref $attrs;
276              
277             my $choices = delete $attrs->{choices};
278             my $option = new Hopkins::TaskOption { name => $name, %$attrs };
279              
280             $option->choices(new Hopkins::TaskOptionChoices $choices) if $choices;
281              
282             return $option;
283             }
284              
285             sub _setup_schedule
286             {
287             my $self = shift;
288             my $status = shift;
289             my $task = shift;
290             my $ref = $task->{schedule};
291              
292             return undef if not ref $ref eq 'HASH';
293              
294             my $superset = DateTime::Set->empty_set;
295              
296             if (my $aref = $ref->{cron}) {
297             my $set = eval { DateTime::Event::MultiCron->from_multicron(@$aref) };
298              
299             if (my $err = $@) {
300             Hopkins->log_error('unable to setup schedule for ' . $task->name . ': ' . $err);
301             $status->failed(1);
302             $status->errmsg($err);
303             } else {
304             $superset = $superset->union($set);
305             }
306             }
307              
308             return $superset;
309             }
310              
311             sub parse
312             {
313             my $self = shift;
314             my $file = shift;
315             my $status = shift;
316              
317             eval { $self->xsd->validate($self->xml->parse_file($file)) };
318              
319             if (my $err = $@) {
320             $status->errmsg($err);
321              
322             return undef;
323             }
324              
325             my %xmlsopts =
326             (
327             ValueAttr => [ 'value' ],
328             GroupTags => { options => 'option' },
329             SuppressEmpty => '',
330             ForceArray => [ 'plugin', 'task', 'chain', 'option', 'cron' ],
331             ContentKey => '-value',
332             ValueAttr => { option => 'value' },
333             KeyAttr =>
334             {
335             plugin => 'name',
336             option => 'name',
337             queue => 'name',
338             task => 'name'
339             }
340             );
341              
342             my $xs = new XML::Simple %xmlsopts;
343             my $ref = eval { $xs->XMLin($file) };
344              
345             if (my $err = $@) {
346             $status->errmsg($err);
347              
348             return undef;
349             }
350              
351             Hopkins->log_debug(Dump $ref);
352              
353             return $ref;
354             }
355              
356             sub scan
357             {
358             my $self = shift;
359              
360             return scalar $self->monitor->scan;
361             }
362              
363             sub get_queue_names
364             {
365             my $self = shift;
366             my $config = $self->config || {};
367              
368             return $config->{queue} ? keys %{ $config->{queue} } : ();
369             }
370              
371             sub get_task_names
372             {
373             my $self = shift;
374             my $config = $self->config || {};
375              
376             return $config->{task} ? keys %{ $config->{task} } : ();
377             }
378              
379             sub get_task_info
380             {
381             my $self = shift;
382             my $task = shift;
383              
384             return $self->config->{task}->{$task};
385             }
386              
387             sub get_queue_info
388             {
389             my $self = shift;
390             my $name = shift;
391              
392             return { name => $name, %{ $self->config->{queue}->{$name} } };
393             }
394              
395             sub get_plugin_names
396             {
397             my $self = shift;
398              
399             return keys %{ $self->config->{plugin} };
400             }
401              
402             sub get_plugin_info
403             {
404             my $self = shift;
405             my $name = shift;
406              
407             return $self->config->{plugin}->{$name};
408             }
409              
410             sub has_plugin
411             {
412             my $self = shift;
413             my $name = shift;
414              
415             return exists $self->config->{plugin}->{$name} ? 1 : 0;
416             }
417              
418             sub fetch
419             {
420             my $self = shift;
421             my $path = shift;
422              
423             $path =~ s/^\/+//;
424              
425             my $ref = $self->config;
426              
427             foreach my $spec (split '/', $path) {
428             for (ref($ref)) {
429             /ARRAY/ and do { $ref = $ref->[$spec] }, next;
430             /HASH/ and do { $ref = $ref->{$spec} }, next;
431              
432             $ref = undef;
433             }
434             }
435              
436             return $ref;
437             }
438              
439             sub loaded
440             {
441             my $self = shift;
442              
443             return $self->config ? 1 : 0;
444             }
445              
446             =head1 AUTHOR
447              
448             Mike Eldridge <diz@cpan.org>
449              
450             =head1 LICENSE
451              
452             =cut
453              
454             1;
455              
456             __DATA__
457             <?xml version="1.0" encoding="utf-8"?>
458             <xs:schema elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema">
459             <xs:element name="hopkins" type="hopkins" />
460              
461             <xs:complexType name="hopkins">
462             <xs:choice minOccurs="0" maxOccurs="unbounded">
463             <xs:element name="plugin" type="plugin" />
464             <xs:element name="database" type="database" />
465             <xs:element name="queue" type="queue" />
466             <xs:element name="task" type="task" />
467             <xs:element name="state" type="state" />
468             </xs:choice>
469             </xs:complexType>
470              
471             <xs:complexType name="state">
472             <xs:sequence>
473             <xs:element name="root" type="xs:string" />
474             </xs:sequence>
475             </xs:complexType>
476              
477             <xs:complexType name="plugin">
478             <xs:sequence>
479             <xs:any minOccurs="0" maxOccurs="unbounded" processContents="skip" />
480             </xs:sequence>
481             <xs:attribute name="name" type="xs:string" />
482             </xs:complexType>
483              
484             <xs:complexType name="database">
485             <xs:all>
486             <xs:element name="dsn" />
487             <xs:element name="user" />
488             <xs:element name="pass" />
489             <xs:element name="options" type="dboptions" minOccurs="0" />
490             </xs:all>
491             </xs:complexType>
492              
493             <xs:complexType name="dboptions">
494             <xs:sequence>
495             <xs:element name="option" maxOccurs="unbounded" />
496             </xs:sequence>
497             </xs:complexType>
498              
499             <xs:complexType name="options">
500             <xs:sequence>
501             <xs:element name="option" type="option" maxOccurs="unbounded" />
502             </xs:sequence>
503             </xs:complexType>
504              
505             <xs:complexType name="option">
506             <xs:all>
507             <xs:element name="choices" type="choices" minOccurs="0" />
508             </xs:all>
509              
510             <xs:attribute name="name" type="xs:string" />
511             <xs:attribute name="value" type="xs:string" />
512             <xs:attribute name="type">
513             <xs:simpleType>
514             <xs:restriction base="xs:string">
515             <xs:enumeration value="bool" />
516             <xs:enumeration value="text" />
517             <xs:enumeration value="combo" />
518             </xs:restriction>
519             </xs:simpleType>
520             </xs:attribute>
521             </xs:complexType>
522              
523             <xs:complexType name="choices">
524             <xs:sequence>
525             <xs:element name="choice" minOccurs="0" maxOccurs="unbounded" />
526             </xs:sequence>
527              
528             <xs:attribute name="type">
529             <xs:simpleType>
530             <xs:restriction base="xs:string">
531             <xs:enumeration value="json" />
532             <xs:enumeration value="xml" />
533             </xs:restriction>
534             </xs:simpleType>
535             </xs:attribute>
536             <xs:attribute name="src" type="xs:string" />
537             <xs:attribute name="root" type="xs:string" />
538             <xs:attribute name="name" type="xs:string" />
539             <xs:attribute name="value" type="xs:string" />
540             </xs:complexType>
541              
542             <xs:complexType name="queue">
543             <xs:sequence>
544             <xs:element name="concurrency" type="xs:integer" />
545             </xs:sequence>
546              
547             <xs:attribute name="name" type="xs:string" />
548             <xs:attribute name="onerror">
549             <xs:simpleType>
550             <xs:restriction base="xs:string">
551             <xs:enumeration value="halt" />
552             <xs:enumeration value="freeze" />
553             <xs:enumeration value="shutdown" />
554             </xs:restriction>
555             </xs:simpleType>
556             </xs:attribute>
557             </xs:complexType>
558              
559             <xs:complexType name="task">
560             <xs:all>
561             <xs:element name="cmd" type="xs:string" minOccurs="0" />
562             <xs:element name="class" type="xs:string" minOccurs="0" />
563             <xs:element name="queue" type="xs:string" />
564             <xs:element name="schedule" type="schedule" minOccurs="0" />
565             <xs:element name="options" type="options" minOccurs="0" />
566             <xs:element name="chain" type="chain" minOccurs="0" />
567             </xs:all>
568              
569             <xs:attribute name="name" type="xs:string" />
570             <xs:attribute name="run">
571             <xs:simpleType>
572             <xs:restriction base="xs:string">
573             <xs:enumeration value="serial" />
574             <xs:enumeration value="parallel" />
575             </xs:restriction>
576             </xs:simpleType>
577             </xs:attribute>
578             <xs:attribute name="enabled">
579             <xs:simpleType>
580             <xs:restriction base="xs:string">
581             <xs:enumeration value="yes" />
582             <xs:enumeration value="no" />
583             </xs:restriction>
584             </xs:simpleType>
585             </xs:attribute>
586             <xs:attribute name="stack">
587             <xs:simpleType>
588             <xs:union>
589             <xs:simpleType>
590             <xs:restriction base="xs:string">
591             <xs:enumeration value="yes" />
592             <xs:enumeration value="no" />
593             </xs:restriction>
594             </xs:simpleType>
595             <xs:simpleType>
596             <xs:restriction base="xs:integer">
597             <xs:minInclusive value="-1" />
598             </xs:restriction>
599             </xs:simpleType>
600             </xs:union>
601             </xs:simpleType>
602             </xs:attribute>
603             <xs:attribute name="onerror">
604             <xs:simpleType>
605             <xs:restriction base="xs:string">
606             <xs:enumeration value="disable" />
607             </xs:restriction>
608             </xs:simpleType>
609             </xs:attribute>
610             </xs:complexType>
611              
612             <xs:complexType name="schedule">
613             <xs:sequence>
614             <xs:element name="cron" type="xs:string" maxOccurs="unbounded" />
615             </xs:sequence>
616             </xs:complexType>
617              
618             <xs:complexType name="chain">
619             <xs:all>
620             <xs:element name="options" type="options" minOccurs="0" />
621             <xs:element name="chain" type="chain" minOccurs="0" />
622             </xs:all>
623              
624             <xs:attribute name="task" type="xs:string" />
625             </xs:complexType>
626             </xs:schema>