package Net::Gopher::Request; =head1 NAME Net::Gopher::Request - Class encapsulating Gopher/Gopher+ requests =head1 SYNOPSIS use Net::Gopher::Request; # Create a request object for a Gopher request: my $request = new Net::Gopher::Request ('Gopher', Host => 'gopher.host.com', Selector => '/menu', ItemType => 1 ); # ...or, for a Gopher+ request: my $request = new Net::Gopher::Request ('GopherPlus', Host => 'gopher.host.com', Selector => '/item', Representation => 'text/plain', ItemType => 0 ); # ...or, for a Gopher+ item attribute information request: $request = new Net::Gopher::Request ('ItemAttribute', Host => 'gopher.host.com', Selector => '/some_item.txt', Attributes => ['+INFO', '+VIEWS'] ); # ...or, for a Gopher+ directory attribute information request: $request = new Net::Gopher::Request ('DirectoryAttribute', Host => 'gopher.host.com', Selector => '/some_dir', Attributes => ['+INFO', '+ADMIN'] ); # ...or, you can use a URL to create one of the above types of requests: $request = new Net::Gopher::Request ('URL', 'gopher://gopher.host.com/1'); # You can also send arguments as a hashref instead; which ever style you # prefer: my $request = new Net::Gopher::Request ( Gopher => { Host => 'gopher.host.com', Selector => '/menu', ItemType => 1 } ); # all of the possible parameters to new() have corresponding accessor methods: my $item_type = $request->item_type; $request->selector('/another_item'); # change the selector string $request->host('gopher.anotherhost.com'); # change the hostname ... =head1 DESCRIPTION B is a generic class to encapsulate Gopher and Gopher+ requests. The typical usage of this module is to call the C method to create a new request object, then pass the object on to the B C method. As an aternative to the C consturctor, there are five named constructors detailed below in L that each bare the name of the type of object they create (C, C, C, C, and C). You can import them and call them like functions, and they'll return request objects like C. For storing and manipulating requests, this class provides accessor methods to manipulate every member of a request object. =head1 METHODS The following methods are available: =cut use 5.005; use strict; use warnings; use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); use base 'Exporter'; use Carp; use Net::Gopher::Constants qw(:request :item_types); use Net::Gopher::Debugging; use Net::Gopher::Exception; use Net::Gopher::Utility qw($CRLF get_named_params size_in_bytes); use URI; use constant DEFAULT_GOPHER_PORT => 70; push(@ISA, qw(Net::Gopher::Debugging Net::Gopher::Exception)); @EXPORT_OK = qw( Gopher GopherPlus ItemAttribute DirectoryAttribute URL ); %EXPORT_TAGS = ( gopher => [qw(Gopher URL)], gopher_plus => [qw(GopherPlus ItemAttribute DirectoryAttribute URL)], all => [ qw(Gopher GopherPlus ItemAttribute DirectoryAttribute URL) ] ); #==============================================================================# =head2 new(TYPE [, OPTIONS | URL]) This method creates a new B object encapsulating a Gopher or Gopher+ request. The first argument is a string specifying what type of request you're creating. You're options are as follows: "Gopher", for a Gopher request; "GopherPlus", for a Gopher+ request; "ItemAttribute", for a Gopher+ item attribute information request; "DirectoryAttribute", for a Gopher+ directory attribute information request; and "URL", which allows you to create one of the aforementioned requests using a string containing URL instead of using a series of named parameters. For all types other than I, following the type are a series of named parameters optionally in the form of a hash reference or array reference. Depending on which type you specify, your usage will vary: =over 4 =item I For Gopher requests the available C "value"> pairs are: Host = A hostname (e.g., "gopher.host.com"); Port = A port number (e.g., 70); Selector = A selector string (e.g., "/"); SearchWords = A string or list containing query words for an index-search server type item (e.g., "red blue green"); ItemType = The Gopher item type (e.g., "0", "1", "7", "g", etc.); Example: my $request = new Net::Gopher::Request ('Gopher', Host => 'gopher.host.com', Selector => '/doc.txt', SearchWords => "red green blue", ItemType => 0 ); Note that you can also send the searh words separately as an array reference: my $request = new Net::Gopher::Request ('Gopher', Host => 'gopher.host.com', Selector => '/doc.txt', SearchWords => ['red', 'green', 'blue'], ItemType => 0 ); =item I For Gopher+ requests, besides the I, I, I, I, and I parameters, you have the following additional C "value"> pairs: Representation = What format (MIME type) you want the resource in (e.g., 'text/plain'); DataBlock = For Gopher+ Ask forms. Data to be sent to the server. Example: my $request = new Net::Gopher::Request ('GopherPlus', Host => 'gopher.host.com', Selector => '/script', Representation => 'text/plain', DataBlock => "Some data" ); Note that a data heading for your data block will be generated for you when you send the request, so your data block should not contain one. =item I For item attribute information requests, in addition to the I, I, and I options, you have the following C "value"> pairs: Attributes = A string or list containing block names (e.g., "+VIEWS+ADMIN"); Example: my $request = new Net::Gopher::Request ('ItemAttribute', Host => 'gopher.host.com', Selector => '/item', Attributes => '+VIEWS+ADMIN' ); Also note that you can specify the block names in a list as opposed to a string and that when doing so, the leading '+' is optional (it will be added for you): my $request = new Net::Gopher::Request ('ItemAttribute', Host => 'gopher.host.com', Selector => '/item', Attributes => ['VIEWS', 'ADMIN'] ); =item I For directory attribute information requests, you have the same parameters as with item attribute information requests: my $request = new Net::Gopher::Request ('DirectoryAttribute', Host => 'gopher.host.com', Selector => '/directory', Attributes => ['INFO', 'VIEWS'] ); =back Keep in mind that the named parameters can also be sent separately as a hash or array reference: my $request = new Net::Gopher::Request ( Gopher => { Host => 'gopher.host.com', Selector => '/', ItemType => 1 } ); In addition to C, there are named constructors you can import and call like functions. See L below on how to import and use the functions. =cut sub new { my $invo = shift; my $class = ref $invo || $invo; my $type = shift; return Net::Gopher::Request->call_die( 'No request type specified.' ) unless ($type); return Net::Gopher::Request->call_die( "Type \"$type\" is not a valid request type. Supply " . 'either "Gopher", "GopherPlus", "ItemAttribute", ' . '"DirectoryAttribute", or "URL" instead.' ) unless (lc $type eq 'gopher' or lc $type eq 'gopherplus' or lc $type eq 'itemattribute' or lc $type eq 'directoryattribute' or lc $type eq 'url'); my ($host, $port, $selector, $search_words, $representation, $attributes, $data_block, $item_type); if (lc $type eq 'url') { # Since the calling convention is different for URL type # requests than all others, we first need to parse the URL # and set $type to whatever type of request ("Gopher," # "GopherPlus," "ItemAttribute," "DirectoryAttribute") this URL # contains, then put all of the notable elements of the URL # (the host, port, selector, search words, etc.) in their # corresponding scalars ($host, $port, $selector, # $search_words, etc.). my $url = shift; my $uri; if (defined $url and length $url) { # We need to manually add the scheme to the URL if one # isn't there yet. We have to do this instead of just # using URI.pm's scheme() method because that just adds # the scheme name plus a colon to the beginning of the # URL if a scheme isn't already there (e.g., if you # call $url->scheme("foo") on a URL like # subdomain.domain.com, you end up with # foo:subdomain.domain.com, which is not what we want): $url = "gopher://$url" unless ($url =~ m|^[a-zA-Z0-9]+?://|); $uri = new URI $url; # make sure the URL's scheme isn't something other # than gopher: return Net::Gopher::Request->call_die( sprintf('Protocol "%s" is not supported.', $uri->scheme ) ) unless (lc $uri->scheme eq 'gopher'); } else { $uri = new URI (undef, 'gopher'); } # get the host, port, selector, and item type: $host = $uri->host; $port = $uri->port; $selector = $uri->selector if (defined $uri->selector and length $uri->selector); $item_type = $uri->gopher_type if (defined $uri->gopher_type and length $uri->gopher_type); # look for a Gopher+ string: if (defined $uri->string and length $uri->string) { # get the request type character ('+', '?', '!', or # '$') and everything after it: my ($type_char, $string) = (substr($uri->string, 0, 1), substr($uri->string, 1)); if ($type_char eq '+' or $type_char eq '?') { $type = 'GopherPlus'; $representation = $string if (defined $string and length $string); $search_words = $uri->search if (defined $uri->search and length $uri->search); } elsif ($type_char eq '!') { $type = 'ItemAttribute'; $attributes = $string if (defined $string and length $string); } elsif ($type_char eq '$') { $type = 'DirectoryAttribute'; $attributes = $string if (defined $string and length $string); } else { return Net::Gopher::Request->call_die( "Can't convert malformed URL into a " . "request object: the Gopher+ string " . "contains an invalid request type " . "character (\"$type_char\"). It " . "should be \"+\", \"?\", \"!\", or " . "\"\$\" instead." ); } } else { $type = 'Gopher'; $search_words = $uri->search if (defined $uri->search and length $uri->search); } } else { # this isn't a URL type request, so extract every possible # named parameter instead: get_named_params({ Host => \$host, Port => \$port, Selector => \$selector, SearchWords => \$search_words, Representation => \$representation, Attributes => \$attributes, DataBlock => \$data_block, Itemtype => \$item_type }, \@_ ); } # now create and fill the request object: my $self; if (lc $type eq 'gopher') { $search_words = _search_words_as_string($search_words); $self = { request_type => GOPHER_REQUEST, host => $host, port => $port || DEFAULT_GOPHER_PORT, selector => $selector, search_words => $search_words, item_type => (defined $item_type) ? $item_type : GOPHER_MENU_TYPE }; } elsif (lc $type eq 'gopherplus') { $search_words = _search_words_as_string($search_words); $self = { request_type => GOPHER_PLUS_REQUEST, host => $host, port => $port || DEFAULT_GOPHER_PORT, selector => $selector, search_words => $search_words, representation => $representation, data_block => $data_block, item_type => (defined $item_type) ? $item_type : GOPHER_MENU_TYPE }; } elsif (lc $type eq 'itemattribute') { $attributes = _attributes_as_string($attributes); $self = { request_type => ITEM_ATTRIBUTE_REQUEST, host => $host, port => $port || DEFAULT_GOPHER_PORT, selector => $selector, attributes => $attributes }; } elsif (lc $type eq 'directoryattribute') { $attributes = _attributes_as_string($attributes); $self = { request_type => DIRECTORY_ATTRIBUTE_REQUEST, host => $host, port => $port || DEFAULT_GOPHER_PORT, selector => $selector, attributes => $attributes }; } bless($self, $class); return $self; } ################################################################################ # # The named constructor functions: # sub URL { return new Net::Gopher::Request ('URL', @_) } sub Gopher { return new Net::Gopher::Request ('Gopher', @_) } sub GopherPlus { return new Net::Gopher::Request ('GopherPlus', @_) } sub ItemAttribute { return new Net::Gopher::Request ('ItemAttribute', @_) } sub DirectoryAttribute { return new Net::Gopher::Request ('DirectoryAttribute', @_); } #==============================================================================# =head2 as_string() This method returns a string containing a textual representation of the request. (The B C method calls this method on the request object supplied to it and sends the result to the server.) =cut sub as_string { my $self = shift; my $request_string = (defined $self->selector) ? $self->selector : ''; if ($self->request_type == GOPHER_REQUEST) { $request_string .= "\t" . $self->search_words if (defined $self->search_words); $request_string .= $CRLF; } elsif ($self->request_type == GOPHER_PLUS_REQUEST) { $request_string .= "\t" . $self->search_words if (defined $self->search_words); $request_string .= "\t+"; $request_string .= $self->representation if (defined $self->representation); if (defined $self->data_block) { # add the data flag to indicate the presence of the # data block: $request_string .= "\t1"; $request_string .= $CRLF; # add the transfer type and then the data block itself: $request_string .= '+'; $request_string .= size_in_bytes($self->data_block); $request_string .= $CRLF; $request_string .= $self->data_block; } else { $request_string .= $CRLF; } } elsif ($self->request_type == ITEM_ATTRIBUTE_REQUEST) { $request_string .= "\t!"; $request_string .= $self->attributes if (defined $self->attributes); $request_string .= $CRLF; } elsif ($self->request_type == DIRECTORY_ATTRIBUTE_REQUEST) { $request_string .= "\t\$"; $request_string .= $self->attributes if (defined $self->attributes); $request_string .= $CRLF; } return $request_string; } #==============================================================================# =head2 as_url() This method returns a string containing a URL constructed from the elements of the request. =cut sub as_url { my $self = shift; my $uri = new URI (undef, 'gopher'); $uri->scheme('gopher'); $uri->host($self->host); $uri->port($self->port); $uri->selector($self->selector) if (defined $self->selector); $uri->search($self->search_words) if (defined $self->search_words); $uri->gopher_type($self->item_type) if (defined $self->item_type); my $gopher_plus_string; if ($self->request_type == GOPHER_PLUS_REQUEST) { $gopher_plus_string .= '+'; $gopher_plus_string .= $self->representation if (defined $self->representation); } elsif ($self->request_type == ITEM_ATTRIBUTE_REQUEST) { $gopher_plus_string .= '!'; $gopher_plus_string .= $self->attributes if (defined $self->attributes); } elsif ($self->request_type == DIRECTORY_ATTRIBUTE_REQUEST) { $gopher_plus_string .= '$'; $gopher_plus_string .= $self->attributes if (defined $self->attributes); } $uri->string($gopher_plus_string) if (defined $gopher_plus_string); return $uri->as_string; } #==============================================================================# =head2 request_type() This method returns a numeric value indicating the type of request. The number corresponds to one of the four request type constants: C, C, C, or C. E.g.: if ($request->request_type == GOPHER_PLUS_REQUEST) { print "It's a Gopher+ request.\n"; } elsif ($request->request_type == ITEM_ATTRIBUTE_REQUEST) { print "It's an item attribute information request.\n"; } elsif ($request->request_type == DIRECTORY_ATTRIBUTE_REQUEST) { print "It's a directory attribute information request.\n"; } else { print "It's just a Gopher request.\n"; } These four constants can be imported from B by name or when you C B with the I<:request> or I<:all> tags. See L. =cut sub request_type { return shift->{'request_type'} } #==============================================================================# =head2 host([HOST]) This is a get/set method for the I parameter. You can change the hostname of the request by supplying a new one. If you don't supply a new hostname, then the current one will be returned to you. =cut sub host { my $self = shift; if (@_) { $self->{'host'} = shift; } else { return $self->{'host'}; } } #==============================================================================# =head2 port([PORT_NUMBER]) This is a get/set method for the I parameter. You can change the port number by supplying a new one. If you don't supply a new port number, then the current port number will be returned to you. =cut sub port { my $self = shift; if (@_) { $self->{'port'} = shift; } else { return $self->{'port'}; } } #==============================================================================# =head2 selector([SELECTOR_STRING]) This is a get/set method for the I parameter. You can change the selector string by supplying a new one. If you don't supply a new selector string, then the current one will be returned to you. =cut sub selector { my $self = shift; if (@_) { $self->{'selector'} = shift; } else { return $self->{'selector'}; } } #==============================================================================# =head2 search_words([WORDS]) With I and I type requests, this is a get/set method for the I parameter. You can supply new search words in one of two formats: as a string containing the words or as a reference to a list containing individual words, which will be joined together by spaces. If you don't supply new words, then the current search words will be returned to you as a string containing all of the words. =cut sub search_words { my $self = shift; if (@_) { $self->{'search_words'} = _search_words_as_string(@_); } else { return $self->{'search_words'}; } } #==============================================================================# =head2 representation([MIME_TYPE]) With I type requests, this is a get/set method for the I parameter. You can change the representation by supplying a new one. If you don't supply a new representation, then the current one will be returned to you. =cut sub representation { my $self = shift; if (@_) { $self->{'representation'} = shift; } else { return $self->{'representation'}; } } #==============================================================================# =head2 data_block([DATA]) With I type requests, this is a get/set method for the I parameter. You can change the data block by supplying new data. If you don't supply new data, then the current data block will be returned to you. =cut sub data_block { my $self = shift; return unless (exists $self->{'data_block'}); if (@_) { $self->{'data_block'} = shift; } else { return $self->{'data_block'}; } } #==============================================================================# =head2 attributes([ATTRIBUTES]) With item attribute and directory attribute information requests, this is a get/set method for the I parameter. You can supply new block names in one of two formats: as a string containing the block names or as a reference to a list containing individual block names (with optional leading pluses, which will be added for you if you don't add them). If you don't supply new block names, then the current block names will be returned to you as either a list containing the individual names (in list context) or a string containing all of the names (in scalar context). =cut sub attributes { my $self = shift; return unless (exists $self->{'attributes'}); if (@_) { $self->{'attributes'} = _attributes_as_string(@_); } else { return $self->{'attributes'}; } } #==============================================================================# =head2 item_type([TYPE]) This is a get/set method for the I parameter. You can change the item type by supplying item type. If you don't supply new type, then the current item type character will be returned to you. =cut sub item_type { my $self = shift; if (@_) { $self->{'item_type'} = shift; } else { return $self->{'item_type'}; } } ################################################################################ sub _search_words_as_string { my $first_arg = $_[0]; return unless (defined $first_arg); my $search_words; if (ref $first_arg) { $search_words = join(' ', @$first_arg); } elsif (@_ > 1) { $search_words = join(' ', @_); } else { $search_words = $first_arg; } return $search_words; } ################################################################################ sub _attributes_as_string { my $first_arg = $_[0]; return unless (defined $first_arg); my $attributes; if (ref $first_arg or @_ > 1) { foreach my $attribute (ref $first_arg ? @$first_arg : @_) { # add the leading plus to our string of attributes # if this new attributye doesn't have one: $attributes .= '+' unless ($attribute =~ /^\+/); $attributes .= $attribute; } } else { $attributes = $first_arg; } return $attributes; } 1; __END__ =head1 NAMED CONSTRUCTORS Besides C, there are several functions you can export and call to create requests object. The functions are C, C, C, C, and C. These functions each take the same arguments that their C counterparts do: my $request = Gopher( Host => 'gopher.host.com', Selector => '/', ItemType => 1 ); is the same as: my $request = new Net::Gopher::Request ('Gopher', Host => 'gopher.host.com', Selector => '/', ItemType => 1 ); If you don't want to import each one explicitly, then you can use one of the following export tags: =over 4 =item :gopher Exports C and C. =item :gopher_plus Exports C, C, C, and C. =item :all Exports C, C, C, C, and C. =back =head1 BUGS Bugs in this package can reported and monitored using CPAN's request tracker: rt.cpan.org. If you wish to report bugs to me directly, you can reach me via email at . =head1 SEE ALSO L, L =head1 COPYRIGHT Copyright 2003-2004 by William G. Davis. This library is free software released under the terms of the GNU Lesser General Public License (LGPL), the full terms of which can be found in the "COPYING" file that comes with the distribution. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. =cut