File Coverage

blib/lib/Email/MIME/XPath.pm
Criterion Covered Total %
statement 83 106 78.3
branch 9 10 90.0
condition 5 14 35.7
subroutine 27 46 58.7
pod 1 16 6.2
total 125 192 65.1


line stmt bran cond sub pod time code
1 2     2   148226 use strict;
  2         5  
  2         65  
2 2     2   11 use warnings;
  2         4  
  2         124  
3              
4             package Email::MIME::XPath;
5              
6             our $VERSION = '0.005';
7 2     2   1821 use Tree::XPathEngine;
  2         59390  
  2         67  
8 2     2   27 use Scalar::Util ();
  2         3  
  2         32  
9 2     2   10 use Carp ();
  2         3  
  2         130  
10              
11             my (@EXTERNAL_AUTO, @EXTERNAL, @INTERNAL, @SPECIAL);
12             BEGIN {
13 2     2   7 @EXTERNAL_AUTO = qw(findnodes findnodes_as_string findvalue exists find);
14 2         4 @EXTERNAL = qw(findnode matches);
15 2         6 @INTERNAL = qw(get_name get_next_sibling get_previous_sibling get_root_node
16             get_parent_node get_child_nodes
17             is_element_node
18             is_document_node
19             is_attribute_node
20             is_text_node
21             cmp address
22             get_attributes
23             to_literal);
24 2         176 @SPECIAL = qw(__xpath_engine __xpath_engine_options __build_parents
25             __xpath_parent);
26             }
27              
28 2         75 use Sub::Exporter -setup => {
29             into => 'Email::MIME',
30             exports => [ @EXTERNAL, @SPECIAL, @INTERNAL ],
31             groups => {
32             external_auto => \&_build_external,
33             external => [ @EXTERNAL ],
34             internal => [ @INTERNAL ],
35             special => [ @SPECIAL ],
36             default => [
37             -external_auto => { -prefix => 'xpath_' },
38             -external => { -prefix => 'xpath_' },
39             -internal => { -prefix => 'xpath_' },
40             -internal => { -prefix => '__xpath_' },
41             -special,
42             ],
43             },
44 2     2   982 };
  2         10766  
45            
46             sub _build_external {
47 2     2   2818 my ($class, $group, $arg) = @_;
48             return {
49 10         23 map {
50 2         13 my $method = $_;
51             $method => sub {
52 2     2   1892 my $self = shift;
53 2         9 $self->__build_parents;
54 2         6 return $self->__xpath_engine->$method(@_, $self);
55             }
56 10         55 } @EXTERNAL_AUTO
57             };
58             }
59              
60             sub matches {
61 0     0 0 0 my $self = shift;
62 0         0 $self->__build_parents;
63 0         0 my ($path, $context) = @_;
64 0   0     0 $context ||= $self;
65 0         0 return $self->__xpath_engine->matches($self, $path, $context);
66             };
67              
68             sub findnode {
69 4     4 0 1884 my $self = shift;
70 4         17 $self->__build_parents;
71 4         9 my (@nodes) = $self->__xpath_engine->findnodes(@_, $self);
72 4 50       363 Carp::croak "findnode found more than one node" if @nodes > 1;
73 4         29 return $nodes[0];
74             }
75              
76 2     2   25 sub __xpath_engine_options { () }
77              
78             sub __xpath_engine {
79 104   66 104   8532 return $_[0]->{__xpath_engine} ||= Tree::XPathEngine->new(
80             $_[0]->__xpath_engine_options
81             );
82             }
83              
84             # this is a terrible, terrible hack. something like this should be in
85             # Email::MIME instead. try to future-proof it somewhat. -- hdp, 2007-04-20
86             sub __is_multipart {
87 6     6   24 return grep { $_ != $_[0] } $_[0]->parts;
  8         92  
88             }
89              
90             # XXX a lot of trickery here is necessary because Email::MIME objects can be
91             # shared among multiple trees at once. We keep track of parent/address
92             # information inside the XPathEngine object, which is (originally) only inside
93             # the top-level part.
94             sub __build_parents {
95 6     6   12 my $self = shift;
96 6 100       19 return if $self->__xpath_engine->{__parent};
97 2         58 my $parent = $self->__xpath_engine->{__parent} = {};
98 2         8 my $address = $self->__xpath_engine->{__address} = {};
99 2         9 $self->__xpath_engine->{__root} = $self;
100 2         5 Scalar::Util::weaken($self->__xpath_engine->{__root});
101 2         4 my $id = 0;
102 2         19 $address->{$self} = sprintf("%03d", $id++);
103 2 100       7 if (__is_multipart($self)) {
104 1         3 my @q = $self;
105 1         4 while (@q) {
106 2         5 my $part = shift @q;
107 2         7 my @subparts = $part->parts;
108 2         23 for (@subparts) {
109 4         14 $parent->{$_} = $part;
110 4         14 Scalar::Util::weaken $parent->{$_};
111 4         17 $address->{$_} = sprintf("%03d", $id++);
112             # XXX this will cause collisions if more than one Email::MIME::XPath
113             # shares parts
114 4         17 $_->{__xpath_engine} = $self->__xpath_engine;
115 4         20 Scalar::Util::weaken $_->{__xpath_engine};
116             }
117 2         5 push @q, grep { __is_multipart($_) } @subparts;
  4         9  
118             }
119             }
120             }
121              
122             sub __xpath_parent {
123 6     6   17 $_[0]->__xpath_engine->{__parent}->{$_[0]}
124             }
125              
126             sub address {
127 67     67 1 1248 $_[0]->__xpath_engine->{__address}->{$_[0]}
128             }
129              
130             sub get_name {
131             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
132 24   50 24 0 818 my $name = (split /;/, $_[0]->content_type || 'text/plain')[0];
133 24         1181 $name =~ tr{/+}{._};
134 24         65 $name = (split /\./, $name)[1];
135             #my $name = __is_multipart($_[0]) ? 'multi' : 'part';
136             #warn "name = $name";
137 24         72 return $name;
138             }
139             sub get_next_sibling {
140             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
141 0     0 0 0 return;
142             }
143             sub get_previous_sibling {
144             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
145 0     0 0 0 return;
146             }
147             sub get_root_node {
148             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
149 6     6 0 8310 $_[0]->__xpath_engine->{__root}->__xpath_get_parent_node;
150             }
151             sub get_parent_node {
152             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
153 6     6 0 31 my $node = shift;
154 6   50     21 return $node->__xpath_parent || bless { root => $node }, 'Email::MIME::XPath::Root';
155             }
156             sub get_child_nodes {
157             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
158 41     41 0 2072 my @kids = grep { $_ != $_[0] } $_[0]->parts;
  57         549  
159 41         116 return @kids;
160             }
161 22     22 0 978 sub is_element_node { 1 }
162 0     0 0 0 sub is_document_node { 0 }
163 0     0 0 0 sub is_attribute_node { 0 }
164 0     0 0 0 sub is_text_node { }
165              
166             sub get_attributes {
167             #my $subname = (caller(0))[3]; warn "$subname from " . $_[0]->__xpath_address;
168 16     16 0 2311 my $node = shift;
169 64         1314 my %attr = (
170             content_type => (split /;/, $node->content_type || 'text/plain')[0],
171             address => $node->__xpath_address,
172             $node->header('Content-Disposition') ? (filename => $node->filename) : (),
173             map {
174 16 100 50     61 my $val = $node->header($_);
175 64 100       1940 defined $val ? (lc($_) => $val) : ()
176             } qw(from to cc subject),
177             );
178             #use Data::Dumper; warn Dumper(\%attr);
179 45         320 return map {
180 16         61 bless {
181             name => $_,
182             value => $attr{$_},
183             node => $node,
184             } => 'Email::MIME::XPath::Attribute'
185             } keys %attr;
186             }
187             sub cmp {
188 29     29 0 357 return $_[0]->__xpath_address <=> $_[1]->__xpath_address
189             }
190 0     0 0 0 sub to_literal { }
191              
192             package Email::MIME::XPath::Root;
193              
194 8     8   23 sub __xpath_address { -1 } # root is always first
195 10     10   610 sub xpath_get_child_nodes { $_[0]->{root} }
196 0     0   0 sub xpath_get_attributes { () }
197 0     0   0 sub xpath_is_document_node { 1 }
198 0     0   0 sub xpath_is_element_node { 0 }
199 0     0   0 sub xpath_is_attribute_node { 0 }
200              
201             # my testing doesn't seem to use this, but I've gotten test failures saying
202             # that it's necessary. I'm tempted to simply @ISA = Email::MIME::XPath, but
203             # that might have other undesirable ramifications.
204              
205 0     0   0 sub xpath_cmp { $_[0]->__xpath_address <=> $_[1]->__xpath_address }
206              
207             package Email::MIME::XPath::Attribute;
208              
209 0     0   0 sub xpath_get_value { return $_[0]->{value} }
210 45     45   735 sub xpath_get_name { return $_[0]->{name} }
211 12     12   1241 sub xpath_string_value { return $_[0]->{value} }
212 0     0     sub xpath_is_document_node { 0 }
213 0     0     sub xpath_is_element_node { 0 }
214 0     0     sub xpath_is_attribute_node { 1 }
215 0     0     sub to_string { return sprintf('%s="%s"', $_[0]->{name}, $_[0]->{value}) }
216 0   0 0     sub address { return join(":", $_[0]->{node}, $_[0]->{rank} || 0) }
217 0     0     sub xpath_cmp { $_[0]->address cmp $_[1]->address }
218              
219             1;
220              
221             __END__