File Coverage

blib/lib/WWW/EFA.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package WWW::EFA;
2 1     1   28382 use Moose;
  1         571789  
  1         14  
3 1     1   9429 use MooseX::Params::Validate;
  1         12668  
  1         9  
4              
5             # CPAN modules
6 1     1   1630 use LWP::UserAgent;
  1         55018  
  1         37  
7 1     1   1004 use YAML;
  1         8125  
  1         53  
8 1     1   10 use Carp;
  1         2  
  1         50  
9 1     1   5 use Try::Tiny;
  1         2  
  1         42  
10 1     1   880 use File::Spec::Functions;
  1         740  
  1         79  
11 1     1   459 use XML::LibXML;
  0            
  0            
12             use Class::Date qw/now/;
13              
14             ### Local modules
15             # Objects
16             use WWW::EFA::Departure;
17             use WWW::EFA::Line;
18             use WWW::EFA::Location;
19             use WWW::EFA::Place;
20             use WWW::EFA::Station;
21             use WWW::EFA::ResultHeader;
22              
23             # Factories
24             use WWW::EFA::DepartureFactory;
25             use WWW::EFA::LineFactory;
26             use WWW::EFA::LocationFactory;
27             use WWW::EFA::PlaceFactory;
28             use WWW::EFA::HeaderFactory;
29             use WWW::EFA::RouteFactory;
30              
31             use WWW::EFA::Request;
32             use WWW::EFA::DeparturesResult;
33             use WWW::EFA::ConnectionsResult;
34              
35              
36             =head1 NAME
37              
38             WWW::EFA - Interface to EFA sites (Elektronische Fahrplanauskunft)
39              
40             =head1 VERSION
41              
42             Version 0.04
43              
44             =cut
45              
46             our $VERSION = '0.04';
47              
48              
49             =head1 SYNOPSIS
50              
51             Get location of public transport stops and connection details.
52              
53             use WWW::EFA;
54              
55             my $efa = WWW::EFA->new();
56             ...
57              
58             =head1 PARAMS/ACCESSORS
59              
60             TODO: RCL 2012-01-22 document params/accessors
61              
62             =cut
63              
64             has 'base_url' => ( is => 'ro', isa => 'Str', required => 1, );
65             has 'cache_dir' => ( is => 'ro', isa => 'Str', );
66              
67             has 'agent' => (
68             is => 'ro',
69             isa => 'LWP::UserAgent',
70             required => 1,
71             lazy => 1,
72             default => sub{ LWP::UserAgent->new() },
73             );
74              
75              
76             has 'place_factory' => (
77             is => 'ro',
78             isa => 'WWW::EFA::PlaceFactory',
79             required => 1,
80             lazy => 1,
81             default => sub{ WWW::EFA::PlaceFactory->new() },
82             );
83              
84             has 'line_factory' => (
85             is => 'ro',
86             isa => 'WWW::EFA::LineFactory',
87             required => 1,
88             lazy => 1,
89             default => sub{ WWW::EFA::LineFactory->new() },
90             );
91              
92             has 'location_factory' => (
93             is => 'ro',
94             isa => 'WWW::EFA::LocationFactory',
95             required => 1,
96             lazy => 1,
97             default => sub{ WWW::EFA::LocationFactory->new() },
98             );
99              
100             has 'departure_factory' => (
101             is => 'ro',
102             isa => 'WWW::EFA::DepartureFactory',
103             required => 1,
104             lazy => 1,
105             default => sub{ WWW::EFA::DepartureFactory->new() },
106             );
107              
108             has 'route_factory' => (
109             is => 'ro',
110             isa => 'WWW::EFA::RouteFactory',
111             required => 1,
112             lazy => 1,
113             default => sub{ WWW::EFA::RouteFactory->new() },
114             );
115              
116             has 'header_factory' => (
117             is => 'ro',
118             isa => 'WWW::EFA::HeaderFactory',
119             required => 1,
120             lazy => 1,
121             default => sub{ WWW::EFA::HeaderFactory->new() },
122             );
123              
124             # Requests per minute
125             has 'sleep_between_requests' => (
126             is => 'ro',
127             isa => 'Int',
128             required => 1,
129             default => 0,
130             );
131              
132             has 'last_request_time' => (
133             is => 'rw',
134             isa => 'Int',
135             );
136              
137             =head1 METHODS
138              
139             =head2 departures
140              
141             Queries the XSLT_DM_REQUEST method from the EFA server.
142              
143             =head3 Params
144              
145             =over 4
146              
147             =item location => L<WWW::EFA::Location> (which must have the id (stopID) defined)
148              
149             =back
150              
151             =cut
152             sub departures {
153             my ( $self, %params ) = validated_hash(
154             \@_,
155             location => { isa => 'WWW::EFA::Location' },
156             equivs => { isa => 'Bool', default => '0' },
157             limit => { isa => 'Int' , optional => 1 },
158             );
159             if( not $params{location}->id ){
160             croak( "Need a location with an id (stopID) to get departures" );
161             }
162              
163             # Build the request for the stopfinder request
164             # The suffix '_sf' in many arguments is for the 'stop finder' requst
165             my $req = WWW::EFA::Request->new(
166             base_url => $self->base_url,
167             service => 'XSLT_DM_REQUEST'
168             );
169              
170             $req->set_argument( 'type_dm' , 'stop' );
171             $req->set_argument( 'useRealtime' , 1 );
172             $req->set_argument( 'mode' , 'direct' );
173             $req->set_argument( 'name_dm' , $params{location}->id );
174             $req->set_argument( 'deleteAssignedStops_dm' , $params{equivs} );
175             $req->set_argument( 'limit' , $params{limit} ) if $params{limit};
176              
177             # Get the reader
178             my $doc = $self->_get_doc( request => $req );
179              
180             # Move into the itdDepartureMonitorRequest element
181             ( $doc ) = $doc->findnodes( 'itdDepartureMonitorRequest' );
182            
183             # Sanity checks
184             my( $odv_elem ) = $doc->findnodes( "itdOdv" );
185             if( not $odv_elem or $odv_elem->getAttribute( 'usage' ) ne 'dm' ){
186             croak( "Could not find itdOdv/attribute::usage = 'dm'" );
187             }
188              
189             # This is the result we will return
190             my $result = WWW::EFA::DeparturesResult->new();
191              
192             my( $name_elem ) = $odv_elem->findnodes( 'itdOdvName' );
193             if( not $name_elem or not $name_elem->hasAttribute( 'state' ) or $name_elem->getAttribute( 'state' ) ne 'identified' ){
194             # TODO: RCL 2011-11-14 Deal with list or other error options better
195             return $result;
196             }
197              
198             my $departure_location = $self->location_factory->location_from_odvNameElem( $odv_elem->findnodes( 'itdOdvName/odvNameElem' ) );
199             if( not $departure_location ){
200             return $result;
201             }
202              
203             # If the Location does not have a name, get it from the place
204             if( not $departure_location->name ){
205             my $place = $self->place_factory->place_from_itdOdvPlace( $odv_elem->findnodes( 'itdOdvPlace' ) );
206             $departure_location->name( $place->name );
207             }
208            
209             $result->add_departure_station( WWW::EFA::Station->new( location => $departure_location ) );
210              
211             # Alternative (further away) departure stations
212             foreach my $alt_station_element ( $odv_elem->findnodes( 'itdOdvAssignedStops/itdOdvAssignedStop' ) ){
213             # TODO: RCL 2011-11-06 This hasn't been tested yet - I have never seen an example with more stops...
214             my $location = $self->location_factory->location_from_itdOdvAssignedStop( $alt_station_element );
215             $result->add_departure_station( WWW::EFA::Station->new( location => $location ) );
216             }
217              
218             # Get the lines
219             foreach my $line_elem ( $doc->findnodes( 'itdServingLines/itdServingLine' ) ) {
220             my $line = $self->line_factory->line_from_itdServingLine( $line_elem );
221             $result->add_line( $line );
222             }
223              
224             # Get the departures
225             foreach my $dep_elem ( $doc->findnodes( 'itdDepartureList/itdDeparture' ) ){
226             my $departure = $self->departure_factory->departure_from_itdDeparture( $dep_elem );
227             $result->add_departure( $departure );
228             }
229            
230             return $result;
231             }
232              
233             =head2 trips
234              
235             Queries the XSLT_TRIP_REQUEST2 method from the EFA server.
236              
237             =head3 Required Params
238              
239             =over 4
240              
241             =item I<from> => L<WWW::EFA::Location>
242              
243             =item I<to> => L<WWW::EFA::Location>
244              
245             =item I<date> => L<Class::Date> of the time to be searched
246              
247             =back
248              
249             =head3 Optional Params
250              
251             =over 4
252              
253             =item I<via> => L<WWW::EFA::Location> (default undef)
254              
255             =item I<is_departure> => $boolean (set to true if the date is the departure time)
256            
257             =item I<language> => $string (language to return results in. Default 'de')
258              
259             =item I<walk_speed> => $number (override default walk speed.)
260             TODO: RCL 2011-08-23 What is walk speed? km/h? m/s?
261              
262             =back
263              
264             =cut
265             sub trips {
266             my ( $self, %params ) = validated_hash(
267             \@_,
268             from => { isa => 'WWW::EFA::Location' },
269             via => { isa => 'WWW::EFA::Location' , optional => 1 },
270             to => { isa => 'WWW::EFA::Location' },
271             date => { isa => 'Class::Date' , default => now() },
272             is_departure => { isa => 'Bool' , default => 1 },
273             language => { isa => 'Str' , default => 'de' },
274             walk_speed => { isa => 'Num', , optional => 1 },
275             products => { isa => 'ArrayRef' , optional => 1 },
276             );
277              
278             # Build the request for the stopfinder request
279             # The suffix '_sf' in many arguments is for the 'stop finder' requst
280             my $req = WWW::EFA::Request->new(
281             base_url => $self->base_url,
282             service => 'XSLT_TRIP_REQUEST2',
283             );
284              
285             $req->set_argument( 'sessionID' , '0' );
286             $req->set_argument( 'requestID' , '0' );
287             $req->set_argument( 'ptOptionsActive' , '1' );
288             $req->set_argument( 'useRealtime' , '1' );
289             $req->set_argument( 'useProxyFootSearch' , '1' );
290             $req->set_argument( 'language' , $params{language} );
291             $req->set_argument( 'itdTripDateTimeDepArr' , $params{is_departure} ? 'dep' : 'arr' );
292             $req->set_argument( 'changeSpeed' , $params{walk_speed} ) if $params{walk_speed};
293              
294             $req->set_argument( 'itdDate' , $params{date}->strftime( '%Y%m%d' ) );
295             $req->set_argument( 'itdTime' , $params{date}->strftime( '%H%M' ) );
296              
297             # Add the locations
298             $req->add_location( 'origin' , $params{from} );
299             $req->add_location( 'destination' , $params{to} );
300             $req->add_location( 'via' , $params{via} ) if( $params{via} );
301              
302             # TODO: RCL 2011-11-10 make mapping homogeneous with DepartureFactory for mot_type
303             if ( $params{products} ){
304             $req->set_argument( 'includedMeans' , 'checkbox' );
305            
306             my %products =
307             map{ $_ => 1 }
308             @{ $params{products} };
309              
310             if ( $products{I} or $products{R} ){
311             $req->set_argument( 'inclMOT_0', 'on' );
312             }
313             if( $products{S} ){
314             $req->set_argument( 'inclMOT_1', 'on' );
315             }
316             if( $products{U} ){
317             $req->set_argument( 'inclMOT_2', 'on' );
318             }
319             if( $products{T} ){
320             $req->set_argument( 'inclMOT_3', 'on' );
321             $req->set_argument( 'inclMOT_4', 'on' );
322             }
323             if( $products{B} ){
324             $req->set_argument( 'inclMOT_5', 'on' );
325             $req->set_argument( 'inclMOT_6', 'on' );
326             $req->set_argement( 'inclMOT_7', 'on' );
327             }
328             if( $products{P} ){
329             $req->set_argument( 'inclMOT_10', 'on' );
330             }
331              
332             if( $products{F} ){
333             $req->set_argument( 'inclMOT_9', 'on' );
334             }
335             if( $products{C} ){
336             $req->set_argument( 'inclMOT_8', 'on' );
337             }
338              
339             $req->set_argument( 'inclMOT_11', 'on' ); # 11 == 'others'. Always on for now
340              
341             # workaround for highspeed trains: fails when you want highspeed, but not regional
342             if ( $products{I} ){
343             $req->set_argument( 'lineRestriction', 403 ); # means: all but ice
344             }
345             }
346              
347             # Get the data
348             my $doc = $self->_get_doc( request => $req );
349             my $header = $self->header_factory->header_from_result( $doc );
350            
351             my $result = WWW::EFA::ConnectionsResult->new(
352             request => $req,
353             );
354              
355             # Sanity checks
356             # Valid date?
357             # TODO: RCL 2011-11-11 Check for valid date
358             # my( $date_elem ) = $doc->findnodes( 'itdTripdateTime/itdDateTime/itdDate/itdMessage' );
359              
360             # Get the requestID
361             my( $request_elem ) = $doc->findnodes( 'itdTripRequest' );
362             if( $request_elem and $request_elem->hasAttribute( 'requestID' ) ){
363             $result->request_id( $request_elem->getAttribute( 'requestID' ) );
364             }
365              
366             # Get the to/from/via/...
367             STOP:
368             foreach my $stop_elem ( $request_elem->findnodes( 'itdOdv' ) ){
369             my $usage = $stop_elem->getAttribute( 'usage' );
370             my( $state_elem ) = $stop_elem->findnodes( 'itdOdvPlace' );
371             if( not $state_elem or not $state_elem->hasAttribute( 'state' )
372             or $state_elem->getAttribute( 'state' ) ne 'identified' ){
373             # TODO: RCL 2011-11-11 Deal with ambiguous (not identified) results here.
374             next STOP;
375             }
376              
377             my( $name_elem ) = $stop_elem->findnodes( 'itdOdvName/odvNameElem' );
378             if( not $name_elem ){
379             #carp( "No odvNameElem inside itdOdv:\n" . $stop_elem->toString( 2 ) );
380             next STOP;
381             }
382             my $location = $self->location_factory->location_from_odvNameElem( $name_elem );
383            
384             # If there was no location (e.g. no via), then just jump to next STOP
385             if( not $location ){
386             #carp( "Could not get a location from:\n" . $name_elem->toString( 2 ) );
387             next STOP;
388             }
389            
390             my $location_attribute = $usage . '_location';
391            
392             $result->$location_attribute( $location );
393             }
394              
395             foreach my $route_elem( $request_elem->findnodes( 'itdItinerary/itdRouteList/itdRoute' ) ){
396             my $route = $self->route_factory->route_from_itdRoute( $route_elem );
397             $result->add_route( $route );
398             }
399              
400             return $result;
401             }
402              
403             =head2 stop_finder
404              
405             Queries the XML_STOPFINDER_REQUEST method from the EFA server.
406              
407             Used to get an address from coordinates
408              
409             Returns an ArrayRef of L<WWW::EFA::Location>.
410              
411             =head3 Usage
412              
413            
414             my $location = WWW::EFA::Location->new(
415             coordinates => WWW::EFA::Coordinates->new(
416             lat => 12.12345,
417             lon => 48.12345,
418             );
419            
420             my( $address ) = $efa->stop_finder(
421             location => $location,
422             );
423              
424              
425             =head3 Params
426              
427             =over 4
428              
429             =item location => L<WWW::EFA::Location>
430              
431             =back
432              
433             =cut
434             sub stop_finder {
435             my ( $self, %params ) = validated_hash(
436             \@_,
437             location => { isa => 'WWW::EFA::Location' },
438             );
439              
440             # Build the request for the stopfinder request
441             # The suffix '_sf' in many arguments is for the 'stop finder' requst
442             my $req = WWW::EFA::Request->new(
443             base_url => $self->base_url,
444             service => 'XML_STOPFINDER_REQUEST',
445             );
446              
447             # 1=place 2=stop 4=street 8=address 16=crossing 32=poi 64=postcode
448             $req->set_argument( 'anyObjFilter_sf' , 126 );
449              
450             $req->set_argument( 'reducedAnyPostcodeObjFilter_sf' , 64 );
451             $req->set_argument( 'reducedAnyTooManyObjFilter_sf' , 2 );
452             $req->set_argument( 'useHouseNumberList' , 'true' );
453             $req->set_argument( 'regionID_sf' , 1 );
454             $req->add_location( 'sf' , $params{location} );
455              
456             # Get the doc
457             my $doc = $self->_get_doc( request => $req );
458              
459             # Make sure the state is defined and a known value.
460             # if not, then the XML was not the way we expect it...
461             my( $place_elem ) = $doc->findnodes( 'itdStopFinderRequest/itdOdv/itdOdvPlace' );
462             if( not $place_elem or not $place_elem->hasAttribute( 'state' ) ){
463             croak( "state not found in itdOdvPlace" );
464             }
465             my $state = $place_elem->getAttribute( 'state' );
466             if( $state !~ m/^(identified|list|notidentified)$/ ){
467             croak( "Unknown state: $state" );
468             }
469              
470             # If the location could not be identified, return empty arrayref
471             return [] if( $state eq 'notidentified' );
472              
473             # Usually there will only be one match, but there could be more (see state 'list' above)
474             my @locations;
475             foreach my $name_elem( $doc->findnodes( 'itdStopFinderRequest/itdOdv/itdOdvName/odvNameElem' ) ){
476             my $location = $self->location_factory->location_from_odvNameElem( $name_elem );
477             push( @locations, $location ) if $location->id;
478             }
479              
480             # nearby stops
481             foreach my $alt_station_element ( $doc->findnodes( 'itdStopFinderRequest/itdOdv/itdOdvAssignedStops/itdOdvAssignedStop' ) ){
482             # TODO: RCL 2011-11-06 This hasn't been tested yet - I have never seen an example with more stops...
483             my $location = $self->location_factory->location_from_itdOdvAssignedStop( $alt_station_element );
484             push( @locations, $location );
485             }
486              
487             # TODO: RCL 2011-11-10 This request also returns a list of itdOdvAssignedStops - it is also suitable
488             # for finding the closest stop. What is the difference to coord request? Maybe this method is
489             # superfluous legacy?
490              
491             return @locations;
492             }
493              
494             =head2 coord_request
495              
496             Queries the XML_COORD_REQUEST method from the EFA server.
497             Returns an array reference of L<WWW::EFA::Location> objects.
498              
499              
500             =head3 Params
501              
502             =over 4
503              
504             =item I<location> => L<WWW::EFA::Location>
505             Must have either id or lon/lat defined
506              
507             =item I<max_results> => $integer
508             Maximum number of results to return
509              
510             =item I<max_distance> => $integer
511             Maximum distance (meters) around the given location to search
512              
513             =back
514              
515             =cut
516             sub coord_request {
517             my ( $self, %params ) = validated_hash(
518             \@_,
519             location => { isa => 'WWW::EFA::Location' },
520             max_results => { isa => 'Int', default => 50 },
521             max_distance => { isa => 'Int', default => 1320 },
522             );
523              
524             # Build the request
525             my $req = WWW::EFA::Request->new(
526             base_url => $self->base_url,
527             service => 'XML_COORD_REQUEST',
528             );
529              
530             $req->set_argument( 'coordListOutputFormat' , 'STRING' );
531             $req->set_argument( 'type_1' , 'STOP' );
532             $req->set_argument( 'inclFilter' , 1 );
533             $req->set_argument( 'max' , $params{max_results} );
534             $req->set_argument( 'radius_1' , $params{max_distance} );
535             # Cannot use the $req->add_location method here because it would add the location by id
536             $req->set_argument( 'coord' , sprintf( "%.6f:%.6f:WGS84",
537             $params{location}->coordinates->longitude,
538             $params{location}->coordinates->latitude,
539             ) );
540             my $doc = $self->_get_doc( request => $req );
541              
542             # Move into the itdDepartureMonitorRequest element
543             ( $doc ) = $doc->findnodes( 'itdCoordInfoRequest' );
544            
545             my @locations;
546             foreach my $coord_elem( $doc->findnodes( 'itdCoordInfo/coordInfoItemList/coordInfoItem' ) ){
547             my $location = $self->location_factory->location_from_coordInfoItem( $coord_elem );
548             push( @locations, $location );
549             }
550             return @locations;
551             }
552              
553             =head2 complete_location_from_anything
554              
555             Give any valid combination from which a Location object may be completed (id, lat/lon, latitude/longitude, or location) and it will return a complete L<WWW::EFA::Location>.
556              
557             This can be handy in some contexts when you don't have a complete location object...
558              
559             =head3 Params
560              
561             =over 4
562              
563             =item I<id> => $integer
564              
565             =item I<lat> / I<latitude> => $number
566              
567             =item I<lon> / I<longitude> => $number
568              
569             =item I<location> => L<WWW::EFA::Location>
570              
571             =back
572              
573             =cut
574             sub complete_location_from_anything {
575             my ( $self, %params ) = validated_hash(
576             \@_,
577             id => { isa => 'Int', optional => 1 },
578             lat => { isa => 'Num', optional => 1 },
579             lon => { isa => 'Num', optional => 1 },
580             latitude => { isa => 'Num', optional => 1 },
581             longitude => { isa => 'Num', optional => 1 },
582             location => { isa => 'WWW::EFA::Location', optional => 1 },
583             );
584              
585             if( $params{lat} ){
586             $params{latitude} = $params{lat};
587             delete( $params{lat} );
588             }
589             if( $params{lon} ){
590             $params{longitude} = $params{lon};
591             delete( $params{lon} );
592             }
593              
594             if( not $params{location} and $params{id} ){
595             $params{location} = $self->get_location( $params{id} );
596             }
597              
598             # We don't have a location, but hopefully lat/lon
599             if( not $params{location} ){
600             # Can't go on if no coords
601             if( not $params{longitude} or not $params{latitude} ){
602             croak( "Cannot set an origin without latitude, longitude or location!\n" );
603             }
604             $params{location} = WWW::EFA::Location->new(
605             coordinates => WWW::EFA::Coordinates->new(
606             latitude => $params{latitude},
607             longitude => $params{longitude},
608             ),
609             );
610             }
611              
612             # We have a rough location, without ID - see if we can make it one with an ID
613             if( not $params{location}->id or not $params{location}->coordinates ){
614             my @stops = $self->stop_finder(
615             location => $params{location},
616             );
617             if( scalar( @stops ) < 1 ){
618             croak( "No stops found near location:\n" . $params{location}->string );
619             }
620             $params{location} = $stops[0];
621             }
622             if( not $params{location}->id ){
623             croak( "I still don't have an ID for your location, even after searching for it...\n" );
624             }
625             return $params{location};
626             }
627              
628             # Private method to wrap around:
629             # * the http request to the EFA server
630             # * parse the XML content
631             # * error handling if any of the above fail or are unexpected
632             # Returns the XML as got from the EFA server
633             sub _get_xml {
634             my ( $self, %params ) = validated_hash(
635             \@_,
636             request => { isa => 'WWW::EFA::Request' },
637             );
638              
639              
640             my $xml;
641             # If the XML source is defined, use it rather than a live request
642             my $cache_file = ( $self->cache_dir
643             ? catfile( $self->cache_dir, $params{request}->digest )
644             : undef );
645              
646             if( $cache_file and -f $cache_file ){
647             # TODO: RCL 2011-11-20 add debug
648             # printf "#RCL reading from: %s\n", $cache_file;
649             open( my $fh_in, '<:encoding(ISO-8859-1)', $cache_file ) or die( $! );
650             while( my $line = readline( $fh_in ) ){
651             $xml .= $line;
652             }
653             close $fh_in;
654             }else{
655             # Don't hammer the server - sleep if need be...
656             if( $self->sleep_between_requests and $self->last_request_time ){
657             my $sleep = $self->sleep_between_requests - ( time() - $self->last_request_time );
658             if( $sleep > 0 ){
659             sleep( $sleep );
660             }
661             }
662            
663             # Use post - it is more robust than GET, and we don't have to encode parameters
664             my $result = $self->agent->post( $params{request}->url, $params{request}->arguments );
665             $self->last_request_time( time() );
666            
667             # If response code is not 2xx, something went wrong...
668             if( not $result->is_success ){
669             croak( "Response from posting request for stop_finder was not a success:\n" . Dump( {
670             URL => $result->request->uri,
671             Status => $result->code,
672             Content => $result->decoded_content,
673             } ) );
674             }
675             $xml = $result->decoded_content;
676            
677             if( $cache_file ){
678             # TODO: RCL 2011-11-13 Do all operators send in ISO-8859-1 encoding?
679             open( my $fh_out, '>:encoding(ISO-8859-1)', $cache_file ) or die( $! );
680             print $fh_out $xml;
681             close $fh_out;
682             }
683             }
684              
685             return $xml;
686             }
687              
688             # Private method to wrap around:
689             # * get_xml
690             # * make L<XML::LibXML> parser
691             # * move to the /itdRequest element in the document
692             # Returns a L<XML::LibXML> document
693             sub _get_doc {
694             my( $self, %params ) = validated_hash(
695             \@_,
696             request => { isa => 'WWW::EFA::Request' },
697             );
698             my $xml = $self->_get_xml( %params );
699              
700             my $parser = XML::LibXML->new();
701             my $doc = $parser->parse_string( $xml, ) or croak( "Could not read XML" );
702              
703             # We always want to be in the itdRequest section
704             ( $doc ) = $doc->findnodes( '/itdRequest' );
705              
706             return $doc;
707             }
708              
709              
710              
711             =head1 AUTHOR
712              
713             Robin Clarke, C<< <perl at robinclarke.net> >>
714              
715             =head1 BUGS
716              
717             Please report any bugs or feature requests to C<bug-www-efa at rt.cpan.org>, or through
718             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=WWW-EFA>. I will be notified, and then you'll
719             automatically be notified of progress on your bug as I make changes.
720              
721              
722             =head1 SUPPORT
723              
724             You can find documentation for this module with the perldoc command.
725              
726             perldoc WWW::EFA
727              
728              
729             You can also look for information at:
730              
731             =over 4
732              
733             =item * Github - this is my preferred path to receive input on the project!
734              
735             L<https://github.com/robin13/WWW-EFA>
736              
737             =item * RT: CPAN's request tracker (report bugs here)
738              
739             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=WWW-EFA>
740              
741             =item * AnnoCPAN: Annotated CPAN documentation
742              
743             L<http://annocpan.org/dist/WWW-EFA>
744              
745             =item * CPAN Ratings
746              
747             L<http://cpanratings.perl.org/d/WWW-EFA>
748              
749             =item * Search CPAN
750              
751             L<http://search.cpan.org/dist/WWW-EFA/>
752              
753             =back
754              
755              
756             =head1 ACKNOWLEDGEMENTS
757              
758              
759             =head1 LICENSE AND COPYRIGHT
760              
761             Copyright 2011 Robin Clarke.
762              
763             This program is free software; you can redistribute it and/or modify it
764             under the terms of either: the GNU General Public License as published
765             by the Free Software Foundation; or the Artistic License.
766              
767             See http://dev.perl.org/licenses/ for more information.
768              
769              
770             =cut
771              
772             1; # End of WWW::EFA