The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Net::DNS::ZoneParse;

use 5.008000;
use strict;
use warnings;
use vars qw($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);

use Exporter;
use Net::DNS;
use Net::DNS::ZoneParse::Zone;

@ISA = qw(Exporter);
$VERSION = 0.103;
@EXPORT = qw( );
%EXPORT_TAGS = (
	parser => [ qw( parse writezone ) ],
);

@EXPORT_OK = map { @{$_} } values %EXPORT_TAGS;

=pod

=head1 NAME

Net::DNS::ZoneParse - Perl extension for Parsing and Writing BIND8/9
(and RFC1035) compatible zone-files.

=head1 SYNOPSIS

Plain interface

  use Net::DNS::ZoneParse qw(parse, writezone);

  my $rr = parse("db.example.com");
  my $zonetext = writezone($rr);

Object oriented interface - supporting cached parsing

  use Net::DNS::ZoneParse;

  my $parser = Net::DNS::ZoneParse->new;
  my $zone = $parser->zone("example.com");
  $zone->rr->[0]->newserial if($zone->rr->[0]->{type} eq "SOA");
  $zone->save;


=head1 DESCRIPTION

This module is yet another caching parser/generator for RFC1035 compatible
zone-files. It aims to have a fast interface for parsing and support all RRs
known to Net::DNS::RR.

In some circumstances the parsing of an entry is too complicated for the
default _from_string-logic of the corresponding RR. For this cases,
N::D::ZoneParse extends the Interface of N::D::RR with the function
new_from_filestring. Per default this will call the responding new_from_string,
but may be implemented differently for a given RR.

When dealing not just with one Zonefile, the Object-oriented Interface
even becomes more handy. It provides an interface loading only the zonefiles
needed and those only once. Each Zone is then represented by a
Net::DNS::ZoneParse::Zone object.

=cut

#####################
# private functions #
#####################

# test if the given directive may be valid and set it up
sub _dns_test_set_origin {
	my ($origin, $param) = @_;

	return unless $origin;
	$origin .= "." unless (substr($origin, -1) eq ".");
	$origin = substr($origin, 1) if(substr($origin, 0, 1) eq ".");
	$param->{origin} = $origin;
	$param->{name} = substr($origin, 0, -1);
}

# read and validate the arguments
sub _parser_param {
	my $self = { dummy => 1 };
	$self = shift if(ref($_[0]) eq "Net::DNS::ZoneParse");
	my $file = shift unless(ref($_[0]));
	my $fh = shift if(ref($_[0]) eq "GLOB");
	my $param = {};
	$param = shift if(ref($_[0]) eq "HASH");
	my $rrs = [];
	$rrs = shift if(ref($_[0]) eq "ARRAY");
	$rrs = \@_ if(ref($_[0]) =~ m/^Net::DNS::RR/);

	if($param->{origin}) {
		_dns_test_set_origin($param->{origin}, $param);
	} else {
		$param->{origin} = "";
		$param->{name} = "";
	}

	$param->{ttl} = $param->{ttl} || $self->{$param->{name}}->{ttl} || 0;
	$param->{file} = $file || $param->{file} ||
			$self->{$param->{name}}->{filename} || "";
	$param->{fh} = $fh || $param->{fh};
	$param->{rrs} = $rrs if $rrs;
	if($param->{file} and not $param->{fh}) {
		open($param->{fh}, "<", $param->{file}) or return;
		$param->{fileopen} = 1;
	}
	$param->{nocache} = $self->{conf}->{nocache} unless $param->{nocache};
	$param->{parser} = $self->{conf}->{parser} || [ qw( Native ) ]
       		unless $param->{parser};
	$param->{parser_args} = {} unless $param->{parser_args};
	$param->{self} = $self;
	return $param;
}

=pod

=head2 METHODS

=head3 new

	$parser = Net::DNS::ZoneParse->new( [ $config ] )

Creating a new Net::DNS::ZoneParse object with the given configuration
for file-autoloading. The following parameters are currently supported:

=over

=item path

Path to the directory where all of the ZoneFiles can be found. The default is
the current working directory.

=item prefix

The prefix to the generate the filename out of the zonename.
The default is "db.". Thus using the default, loading "example.com" will
search for the file "db.example.com"

=item suffix

Similar to prefix, the default is the empty string.

=item dontload

If set to true, the zone() wont parsed the corresponding file automaticly

=item nocache

If set to true, loaded files won't be cached within this object automaticly.
In this case, extent has to be used.

=item parser

The parsers to use when reading files. These must be given within a
arrayreference. All of the given parsers must be found within
Net::DNS::ZoneParse::Parser. The default is [ "Native" ]. All
Parsers in the list will be used consecutively unless one has returned
contents of the file to read.

=item parser_args

A hashref with parser-names as keys. Some parser may allow further
options; these can be accessed using this argument.

=item generator

The generators to use when generating a new zonefile. The use is corresponding
to parsers. Generators must be found within Net::DNS::ZoneParse::Generator

=back

=cut

sub new {
	my ($self, $config) = @_;
	$config = {} unless(defined $config);
	my %conf = (
		path => $config->{path},
		prefix => $config->{prefix} || "db.",
		suffix => $config->{suffix} || "",
		dontload => $config->{dontload},
		nocache => $config->{nocache},
	);
	return bless({conf => \%conf});
}

=head3 zone

	$zone = $parser->zone("example.com" [, $param])

Returns the Net::DNS::ZoneParse::Zone object for the given domain. If there
where no corresponding zonefile found, an empty Object will be returned.
If the zone was loaded for the first time the corresponding file will be loaded,
otherwise the cached object will be returned.

$param is a hash-reference which can be used to adjust the behaviour.
If a parameter is not given here, it's value used at the time of the objects
generation will be used. Supported parameters are:

=over

=item path

=item filename

This will take precedence above prefix/suffix

=item prefix

=item suffix

=item dontload

=back

=cut

sub zone {
	my ($self, $zone, $prm) = @_;
	$prm = {} unless $prm;
	unless($self->{$zone}) {
		$self->{$zone} = Net::DNS::ZoneParse::Zone->new($zone, {
				path => $prm->{path} || $self->{conf}->{path},
				filename => $prm->{filename} || 
					($prm->{prefix} || $self->{conf}->{prefix} || "" ).$zone.($prm->{suffix} || $self->{conf}->{suffix} || ""),
				dontload => $prm->{dontload} ||
					$self->{conf}->{dontload},
				parent => $self,
			});
	}
	return $self->{$zone};
}

=pod

=head3 extent

	$parser->extent("example.com", $rrs);

Extent the cached entries for the given origin - "example.com" in the example
by the RRs given in $rrs. These might be an array or a reference to an array
of Net::DNS::RR-Objects.

=cut

sub extent {
	my ($self, $zone, @rrs) = @_;
	return unless $zone;
	$zone = $self->zone($zone, { dontload => 1 });
	$zone->add(@rrs);
	return;
}

=pod

=head3 uncache

	$parser->uncache("example.com");

Uncaching removes a zone from the zonecache. The exported instances of this zone
will stay alive, but the next call to zone() will generate a new object.

=cut

sub uncache {
	my ($self, $zone) = @_;

	return unless $self->{$zone};
	delete $self->{$zone};
}

=pod

=head2 EXPORT

=head3 writezone

	$zonetext = writezone($rr);
	# or
	$zonetext = $parser->writezone($zone);

$rr might be either an array of Net::DNS::RR object, or a reference to them.
If using the object-oriented interface, this can be used to by just using
the name of the zone to write. In that case, correct directives for $ORIGIN
and $TTL will be created, too.

writezone will then return a string with the contents of a corresponding
zone-file.

As last parameter, a additional Hash-ref can be used to tweak some of the
behavior. The following parameters are supported:

=over

=item origin

This may contain the zone-name, unless used in the object-oriented interface

=item ttl

The default TTL to be used in the generated file.

=item rr

The Resource-records to write

=item generator

the same as in new

=back

=cut

sub writezone {
	my $self = {};
	$self = shift if(ref($_[0]) eq "Net::DNS::ZoneParse");
	my $prm = (ref($_[-1]) eq "HASH")?pop(@_):{};
	my %param;
       	$param{origin} = ((not ref($_[0]))?shift():($prm->{origin} || ""));
	$param{ttl} = $prm->{ttl} || $self->{$param{origin}}->{ttl} || 0;
	$param{rr} = ((ref($_[0]) eq "ARRAY")?shift():
			(($#_>=0) ? \@_ :
				$prm->{rr} || $self->{$param{origin}}->rr));
	$param{generator} = $prm->{generator} || $self->{conf}->{generator} || [ qw( Native ) ];
	return unless $param{rr};

	for(@{$param{generator}}) {
		my $gen = $_;
		my $mod = "Net::DNS::ZoneParse::Generator::$gen";
		eval "require $mod";
		next if $@;
		$param{parser_arg} = $param{parser_args}->{$gen} || {};
		my $ret = $mod->generate(\%param);
		return $ret if $ret;
	}
	return undef;
}

=head3 parse

	$rr = parse($file);
	# or
	$rr = $parser->parse($file [, $param [, $rrin]]);

parse a specific zonefile and returns a reference to an array of 
Net::DNS::RR objects.

If the function-oriented interface is used and Net::DNS::ZoneFile::Fast is
installed, that parser is used instead.

$file might be either a filename or a open filehandle to be read.

$param is a HASH-ref, with the following entries. If given, those may change to
reflect the contents of the parsed file.

=over

=item origin

The current domains name

=item ttl

The default TTL for all RRs

=item fh

An already opened filehandle, $file can be ommitted, if this is given.

=item file

The name of the file to read, $file can be ommitted, if this is given

=item rrs

Resource Records, which will be added to these found in the file

=item nocache

if given, the parsed file will not be cached in the object.

=item parser

=item parser_args

Same as in the call of new().

=back

$rrin is a ARRAY-ref of Net::DNS::RR objects. If given, this list will be
extended.

=cut

sub parse {
	my $param = _parser_param(@_);
	return unless $param;
	return unless $param->{fh};

	my $ret;
	for(@{$param->{parser}}) {
		my $parser = $_;
		my $mod = "Net::DNS::ZoneParse::Parser::$_";
		eval "require $mod"; 
		next if $@;
		$param->{parser_arg} = $param->{parser_args}->{$parser} || {};
		$ret = $mod->parse($param);
		next unless $ret;
		last;
	}
	if($param->{fileopen}) {
		close($param->{fh});
		delete($param->{fh}); delete($param->{fileopen});
	};
	if((not $param->{self}->{dummy})
			and $param->{name} and not $param->{nocache}) {
		$param->{self}->extent($param->{name}, $ret);
	}
	delete $param->{self} if $param->{self}->{dummy};
	unshift(@{$ret}, @{$param->{rrs}}) if $param->{rrs};
	return $ret;
}

=head1 SEE ALSO

Net::DNS

=head1 AUTHOR

Benjamin Tietz E<lt>benjamin@micronet24.deE<gt>

=head1 COPYRIGHT

Copyright (C) 2010 by Benjamin Tietz

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.

=cut

1;