#!/usr/bin/perl
###########################################################
#
# sxtopdf - generate pdf document from feature database
# Author: Andreas Bauer <abauer@suse.de>
#
###########################################################

=head1 NAME

sxtopdf - generate pdf documents from features stored in a feature database

=head1 SYNOPSIS

B<sxtopdf> [I<options>] [B<-k> I<URL>] [B<-q> I<QUERY>] I<OUTPUTFILE>

B<sxtopdf> B<--help>

=head1 DESCRIPTION

sxtopdf connects to a keeper xml database at URL and generates a PDF file from the
fetched features, which is written to OUTPUTFILE. Options can be given at commandline
or in the environment variable SXTOPDF_OPTS. Options given at commandline have higher
precedence than those from SXTOPDF_OPTS.

=head1 OPTIONS

B<-v>

=over

Raise verbosity. Multiple occurances raise verbosity even more. Standard
level displays only error messages.

=back

B<--quiet>

=over

Don't print anything on STDERR. Overrides -v.

=back

B<-k> I<URL>, B<--keeper>=I<URL>

=over

Location of the keeper database. Default is 'https://keeper.suse.de/sxkeeper'

=back

B<-q> I<QUERY>, B<--query>=I<QUERY>


=over

XQuery to refine the set of features. The given query is expanded to
'/feature[I<QUERY>] if the enclosing /feature[] is not given'. Default query is '*'.

=back

B<-p> I<STRING>, B<--products>=I<STRING>

=over

Which products should be inlcuded in the generated PDF. This has to be a list
of product-id's, seperated by commas like for example "SLES-10,NLD10";
"all" (the default value) will include all products in the PDF file.

B<IMPORTANT>

This option does not alter the query in any way. It just prevents rendering of 
productcontext sections for other than the specified products. This leads to
features containing I<no> productcontext sections if the given query does not filter
out features that do not contain the selected products.

=back

B<-t> I<FILE>, B<--tree>=I<FILE>

=over

Path to the tree structure file, which describes how to order the features. By default
the shipped treefile is used (/usr/share/sxtopdf/fd-tree-structure.xml)

=back

B<--tmpdir>=I<DIR>

=over

Directory to store temporary files (keeper output, docbook, fo). If not specified, an
unique directory will be created below /tmp and deleted after the program finishes. If
explicitly set, the directory will be created if needed and I<not> be deleted after execution.

=back

B<--prefix>=I<PREFIX>

=over

All created files will have the prefix PREFIX. Dots '.' and slashes '.' 
will be escaped.

=back

B<--render>=I<SECTION>[,I<SECTION>[,I<SECTION>]...]

=over

Takes a comma-separated list of section names that should appear in the 
pdf. Valid section names are:

=over

=item partnercontext

=item productcontext

=item actor

=item description

=item references

=item documentationimpact

=item documentationstatus

=item effort

=item testcase

=item usecase

=item internalwhiteboard

=item discussion

=back

They correspond to the sections in the feature schema. The special section 'all'
includes all of the possible sections. 'all' is the default.

=back

B<--no-internal-comments>

=over

Exclude internal comments from the discussion section

=back

B<--partner>

=over

Format PDF for partners. No internal whiteboard, no private comments.

=back

B<-V, --version>

=over

Display version and exit

=back

B<-?, --help>

=over

Displays help

=back

=cut
package Log::Fake;

sub new {
    my $class = shift;
    my $self = {};
    
    $self->{levelmap} = {
        'debug'     => 0,
        'info'      => 1,
        'notice'    => 2,
        'warning'   => 3,
        'error'     => 4,
        'err'       => 4,
        'critical'  => 5,
        'crit'      => 5,
        'alert'     => 6,
        'emergency' => 7,
        'emerg'     => 7
    };
    
    bless $self, $class;
    return $self;
}

sub setLevel {
    $_[0]->{level} = $_[1];
}

sub AUTOLOAD {
    my $self = shift;
    my $level = $AUTOLOAD;
    $level =~ s/.*:://;
    return if( !exists $self->{levelmap}{$level} );
    print STDERR "[$level] @_\n" if( $self->{levelmap}{$level} >= $self->{levelmap}{$self->{level}} );
}


package main;

use strict;
use warnings;

use Getopt::Long qw(:config bundling);
use Pod::Usage;
use Data::Dumper;
use File::Temp qw(tempdir);

#try to enable logging
my $log_dispatch_available = 1;
eval {
    require Log::Dispatch;
    require Log::Dispatch::Screen;
};
if( $@ ) {
    $log_dispatch_available = 0;
}

our $VERSION = "0.5.7";

my $datadir = "/usr/share/sxtopdf";
my $java_libdir = "$datadir/lib";
my $java_classpath = "$java_libdir/sxtotree.jar"
               .":$java_libdir/jdom.jar"
               .":$java_libdir/log4j-1.2.8.jar"
               .":$java_libdir/java-getopt-1.0.11.jar"
               .":$java_libdir/commons-httpclient-3.0-rc4.jar"
               .":$java_libdir/commons-logging.jar"
               .":$java_libdir/commons-codec-1.3.jar"
               .":$java_libdir/xercesImpl-2.9.0.jar"
               .":$java_libdir/xml-apis-2.0.2.jar"
                ;

my $docbook_to_fo_xslt = "$datadir/customize-fo.xsl";

unless( exists $ENV{FOP_OPTS} ) {
    $ENV{FOP_OPTS} = "-Xmx768m";
}

my $default_query = "/feature[*]";

#cmdline options (default values)
my %opt = (
        'verbose'               => 0,
        'keeperurl'             => 'https://keeper.novell.com/sxkeeper',
        'query'                 => '--DEFAULT--', #don't change this
        'treefile'              => "$datadir/fd-tree-structure.xml",
	'xsltfile'		=> undef,
        'tmpdir'                => tempdir(CLEANUP => 0),
        'prefix'                => '',
        'render'                => 'all',
        'no-internal-comments'  => '0',
        'partner'               => 0,
        'includedproducts'      => 'all',
        'show_version'          => 0,
	'docbookonly'		=> 0,
        'input'                 => "",
        'username'              => "", 
        'password'              => "",
	'eco'			=> 0
);

#option configuration
my %opt_conf = (
        'v:+'                   => \$opt{verbose},
        'verbose=i'             => \$opt{verbose},
        'keeper|k=s'            => \$opt{keeperurl},
        'query|q=s'             => \$opt{query},
        'quiet'                 => \$opt{quiet},
        'docbookonly'           => \$opt{docbookonly},
        'products|p=s'          => \$opt{includedproducts},
        'tree|t=s'              => \$opt{treefile},
        'xslt|x=s'              => \$opt{xsltfile},
        'tmpdir=s'              => \$opt{tmpdir},
        'prefix=s'              => \$opt{prefix},
        'render=s'              => \$opt{render},
        'input|i=s'             => \$opt{input},
        'username|u=s'          => \$opt{username},
        'password=s'            => \$opt{password},
        'noexec'                => \$opt{noexec},
        'no-internal-comments'  => \$opt{'no-internal-comments'},
        'partner'               => \$opt{partner},
        'version|V'             => \$opt{show_version},
	'eco|E'			=> \$opt{eco},
        'help|?'                => \&show_help_and_exit
);

#logger object
my $log;

###############
# Subroutines #
###############

sub show_help_and_exit {
    pod2usage( -exitval => 'NOEXIT', -verbose => 0 );
    print <<END;
Options:

  -v, --verbose [number]        increase verbosity level
  -k, --keeper [url]            Keeper location, default:
                                https://keeper.novell.com/sxkeeper
  -q, --query [string]          XQuery, default: /feature[*]
  -i, --input [string]          Read feature XML from local file (obsoletes -k, -q, -u, -p, --password, )
  -p, --products [name[,name]]  list of product sections
      --partner                 create pdf for partners
  -t, --tree [file]             location of treefile
  -x, --xslt [file]             location of xsltfile
  -E, --eco                     process ECO feature for archiving
  -u  --username                username for authenticating on the keeper host
      --password                password for authenticating on the keeper host
      --tmpdir [dir]            directory for temporary files
      --render [sect[,sect]]    list of sections to render
      --no-internal-comments    don't render comments marked internal
  -V, --version                 print version
  -?, --help                    this help

  view manpage for more information
  
END
    exit(0);
}

sub show_version_and_exit {
    print "sxtopdf $VERSION\n";
    exit(0);
}

sub show_error_and_exit {
    $log->error($_[0]);
    exit(1);
}

sub getBinaryLocations {
    my @cmd = @_;
    my $loc;
    my %loc;
    foreach (@cmd) {
        $log->debug( "trying to find out location of command '$_'" );
        $loc = `which $_`;
        chomp $loc;
        $log->debug( "found: $loc" );
        show_error_and_exit("'$_' is needed to run sxtopdf") if ($loc eq '');
        $loc{$_} = $loc;
    }
    return %loc;
}

sub inflateRenderOption {
    my @opts = split( /,/, shift );
    my $opt_string;
    my %sections = (
          partnercontext        => 0,
          productcontext        => 0,
          actor                 => 0,
          description           => 0,
          references            => 0,
          documentationimpact   => 0,
          documentationstatus   => 0,
          effort                => 0,
          testcase              => 0,
          usecase               => 0,
          internalwhiteboard    => 0,
          discussion            => 0
        
    );
    
    foreach (@opts) {
        show_error_and_exit("illegal render section: $_") if( !exists($sections{$_}) and $_ !~ /^all$/ );
        return '' if( /^all$/ );
        $sections{$_} = 1;
    }
    foreach (keys %sections) {
        next if( $sections{$_} == 1 );
        $opt_string .= "--stringparam $_ 0 ";
    }
    return $opt_string;
}

sub execute {
    return if $opt{noexec};
    my $cmd = shift;
    $cmd .= " >/dev/null" if( $opt{quiet} );
    my $ret = system($cmd);
    my $prog = shift || (split(m# #, $cmd))[0];
    if( $ret != 0 ) {
        if ($? == -1) {
            show_error_and_exit("$prog failed to execute: $!");
        }
        elsif ($? & 127) {
            show_error_and_exit( sprintf "$prog died with signal %d, %s coredump\n", 
                ($? & 127),  ($? & 128) ? 'with' : 'without' );
        }
        else {
            show_error_and_exit( sprintf "$prog exited with value %d\n", $? >> 8 );
        }
    }
    return $ret; 
}


###############
# Main Script #
###############

#process options from environment
if( exists $ENV{SXTOPDF_OPTS} )
{
    local @ARGV = split( m/ /, $ENV{SXTOPDF_OPTS} );
    GetOptions( %opt_conf );
}

#process cmdline options
GetOptions( %opt_conf ) || pod2usage();

#check for version option
show_version_and_exit() if( $opt{show_version} );

if ($opt{eco}) {
	die "Usage: Cannot specify --eco and --xsltfile at the same time\n"
		if ($opt{xsltfile});
	$opt{xsltfile} = "$datadir/ecotodocbook.xsl"
}

$opt{xsltfile} = "$datadir/treetodocbook.xsl" unless($opt{xsltfile});

#process remaining cmdline arguments
$opt{query} = shift(@ARGV) if( scalar @ARGV > 1 );
pod2usage("Outputfile missing") if scalar @ARGV == 0;
$opt{outputfile} = shift(@ARGV);

#setup logging facility
my $loglevel = $opt{quiet} ? 'error'
              :$opt{verbose} == 0 ? 'notice'
              :$opt{verbose} <= 1 ? 'info'
              :'debug';

if( $log_dispatch_available ) {
    $log = Log::Dispatch->new( callbacks => sub {
        my %msg = @_;
        chomp $msg{message};
        my $msg = "[$msg{level}] $msg{message}\n";
        return $msg;
    } );

    $log->add( Log::Dispatch::Screen->new(
             name => 'screen',
             min_level => $loglevel
    ));
    $log->debug("logging set up (using Log::Dispatch)");
} else {
    $log = Log::Fake->new();
    $log->setLevel($loglevel);
    $log->debug("logging set up (using Log::Fake)");
}
$log->info("loglevel: $loglevel");
$log->notice("sxtopdf version $VERSION");

#get binary locations
my %bin = getBinaryLocations(qw(java xsltproc xmllint fop));

#TODO: check options
$log->debug("----- starting options check -----" );
if( $opt{treefile} eq '' ) {
    pod2usage("Treestructure missing");
}

#append / to tmpdir if not there
if( $opt{tmpdir} =~ m#[^/]$# ) {
    $opt{tmpdir} .= "/";
}

#escape prefix
$opt{prefix} =~ s/\./\\\./g;
$opt{prefix} =~ s#/##g;

#setup query
if( $opt{query} eq "--DEFAULT--" ) {
    if( $opt{includedproducts} ne 'all' ) {
        my $orglist = join ' or ', map {"productcontext/product/productid='$_'"} split( /,/, $opt{includedproducts} );
        $opt{query} = "/feature[$orglist]";
        $log->notice( "partner option without explicit query, setting query to $opt{query}" );
    } else {
        $opt{query} = $default_query;
        $log->notice( "no query given, using default query: $opt{query}" );
    }
}

#expand query
$opt{query} =~ s/([^\\])'/$1"/g;
if( $opt{query} !~ m#^/feature\[.+\]$# ){
    $opt{query} = "/feature[$opt{query}]";
    $log->notice("imploded query: $opt{query}");
}

#set partner defaults
if( $opt{partner} ) {
    $opt{render} = 'partnercontext,productcontext,actor,description,references,documentationimpact,documentationstatus,effort,testcase,usecase,discussion';
    $opt{'no-internal-comments'} = 1;
}

#inflate xsltproc options
$log->debug( "render: $opt{render}" );
$opt{render} = inflateRenderOption( $opt{render} );
$log->debug( "render: $opt{render}" );

$log->debug("----- options check finished -----" );

#create temporary directory
if( !-d $opt{tmpdir} )
{
    $log->debug("creating temporary directory $opt{tmpdir}");
    mkdir $opt{tmpdir} or show_error_and_exit("Unable to create $opt{tmpdir}: $!");
}


#start
$log->debug("debugging output active");
$log->info("Keeper URL: $opt{keeperurl}");
$log->info("XQuery: $opt{query}");
$log->info("Output to: $opt{outputfile}");
$log->info("Treefile: $opt{treefile}");
$log->info("Input file: $opt{input}");
$log->notice("creating tree...");

my $cmd = "$bin{java} -Xmx256m -cp $java_classpath de.suse.sxtopdf.SXToTree ";
if ($opt{query} ne "") {
    $opt{query} =~ s/"/'/g;
    $cmd .= "-q \"$opt{query}\" ";
}
if ($opt{keeperurl} ne "") {
    $cmd .= "-u \"$opt{keeperurl}/feature\" ";
}
$cmd .= "-o \"$opt{tmpdir}$opt{prefix}tree.xml\" ";
$cmd .= "-t \"$opt{treefile}\" ";
if ($opt{input} ne "") {
    $cmd .= "-i \"$opt{input}\" ";
}
if ($opt{username} ne "") {
    $cmd .= "-n \"$opt{username}\" ";
}
if ($opt{password} ne "") {
    $cmd .= "-p \"$opt{password}\" ";
}

$cmd .= " -d" if $opt{verbose} > 1;

#call sxtotree (get features from keeper + generate a tree.xml)
$log->info( "calling: $cmd" );
execute( $cmd, "sxtotree" );

#quit if tree.xml was not generated (no features matching query)
exit(0) if( !-f $opt{tmpdir}.$opt{prefix}."tree.xml" );

my $constructdate = `/bin/date -R`;
my $dbfile;
chomp $constructdate;

if( $opt{docbookonly} ) {
	$dbfile=$opt{outputfile};
} else {
	$dbfile=$opt{tmpdir}.$opt{prefix}."docbook.xml";
}

#transform to docbook
$cmd = sprintf( "%s %s %s %s -o %s %s %s",
        $bin{xsltproc},
        $opt{render},
        " --stringparam includedproducts '".$opt{includedproducts}."' --stringparam construct-xquery '".$opt{query}."' --stringparam construct-date '$constructdate' ",
        $opt{'no-internal-comments'} ? " --stringparam internal-comments 0 " : "",
        $dbfile,
        $opt{xsltfile},
        $opt{tmpdir}.$opt{prefix}."tree.xml"
);
$log->info( "calling: $cmd" );
$log->notice( "transforming to docbook..." );
execute( $cmd );

#validate docbook
$cmd = sprintf( "%s --valid --noout %s",
        $bin{xmllint},
        $dbfile
);
$log->info( "calling: $cmd" );
$log->notice( "Validating docbook..." );
execute( $cmd );

if( ! $opt{docbookonly} ) {

	#transform to fo
	$cmd = sprintf( "%s -o %s %s %s",
        $bin{xsltproc},
        $opt{tmpdir}.$opt{prefix}."fo.xml",
        $docbook_to_fo_xslt,
		$dbfile
	);
	$log->info( "calling: $cmd" );
	$log->notice( "Transforming to XML-FO..." );
	$log->notice( "This can take a few minutes if the feature set is large." );
	execute( $cmd );

	#generate pdf
	$cmd = sprintf( "%s -q %s %s 2> /dev/null",
        $bin{fop},
        $opt{tmpdir}.$opt{prefix}."fo.xml",
        $opt{outputfile}
	);
	$log->info( "calling: $cmd" );
	$log->notice( "Generating PDF..." );

}

my $ret = execute($cmd);
