The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl
package Elive::script::elive_query;
use warnings; use strict;
use YAML::Syck;
use Carp;
use Getopt::Long qw{};

use Elive;
use Elive::Util;

use Term::ReadLine;
use Pod::Usage;
use UNIVERSAL;

use IO::Interactive;

use URI;
use URI::Escape;

=head1 NAME

elive_query - simple query shell for Elluminate Live! Manager (ELM)

=head1 SYNOPSIS

  elive_query   http[s]://myserver.com/my-site[:port]

    -user someuser          # SDK/SOAP username
    -pass somepass          # SDK/SOAP password
    -c 'select ....'        # execute command(s), then exit
    -debug=n                # 1=some, 2=verbose, 3=verbose + soap
    -dump=yaml              # serialise results as YAML
    -? -help                # obtain help
    -v -version             # print Elive version
    -adapter type           # E.g. -adapter standardv3 (See Elive::StandardV3)

=head1 DESCRIPTION

Simple read-only query shell for Elluminate Live! Manager (ELM).

=head2 Entity Data Queries

This script lets you do simple queries, in a vaguely SQL like manner.
For example:

    % elive_query -user admin https://myserver.com/mysite
    Password: ********
    connecting to https://myserver.com/mysite...ok
    Elive query ... (Elluminate Live! ...)  (c) ... - type 'help' for help

    elive> select loginName,email,firstName,lastName,role from user where loginName like *m* 
    loginName   |email                 |firstName|lastName   |role
    ------------|----------------------|---------|-----------|----
    mr_miyagi   |mr_miyagi@hotmail.com |Pups     |Miyagi     |3
    sthrogmorton|sthrogmorton@gmail.com|Sebastion|Throgmorton|2

    elive>

=head2 Describing Entities

You can also use this script to describe Elive entity structures:

    elive> describe
    usage: describe group|meeting|meetingParameters|participantList|preload|recording|serverDetails|serverParameters|user

    elive> describe meeting
    meeting: Elive::Entity::Meeting:
    meetingId         : pkey Int        
    deleted           : Bool
    end               : HiResDate    -- meeting end time
    facilitatorId     : Str          -- userId of facilitator
    name              : Str          -- meeting name
    password          : Str          -- meeting password
    privateMeeting    : Bool         -- don't display meeting in public schedule
    start             : HiResDate    -- meeting start time

    elive> 

=head1 SEE ALSO

perldoc Elive
L<http://search.cpan.org/dist/Elive/>

=cut

main(@ARGV) unless caller;

sub main {

    local(@ARGV) = @_;

    our $elive_version = ${Elive::VERSION};
    our %entity_collections;
    #
    # this may barf if /dev/tty can't be opened. E.g. when executing from cron
    our $interactive = eval { IO::Interactive::is_interactive() };

    our $term;
    if ($interactive) {
	$term = eval { Term::ReadLine->new('elive shell') }
    }

    my $prompt = "elive> ";
    our $connection;
    my %options;

    our $adapter_class;

    do {
	(my $url, %options) = _getopt();

	my $adapter = $options{adapter} || 'default';
	$adapter_class = {default    => 'Elive::Entity',
			  standard   => 'Elive::StandardV3',
			  standardv3 => 'Elive::StandardV3',
			  standardv2 => 'Elive::StandardV2',
	}->{$adapter};
	$adapter_class ||= $adapter;
	
	eval "require $adapter_class";
	die $@ if $@;

	our $debug = $options{debug};
	Elive->debug($debug) if defined $debug;
	# debug may also be set via $ENV{ELIVE_DEBUG}
	$debug = Elive->debug;

	$connection = _connect($adapter_class => $url, %options)
	    if $url;
    };

    warn "adapter class: $adapter_class\n" if Elive->debug;

    our @data_classes = $adapter_class->data_classes;
    our %entities;

    foreach my $class (@data_classes) {
	#
	# Make sure we're dealing with well formed DAO classes
	#
	eval "require $class";
	die $@ if $@;

	unless (eval {$class->entity_name}) {
	    warn "Omitting non entity class: $class";
	    next;
	}

	$entities{ lcfirst $class->entity_name } = $class;
	#
	# accept plurals, e.g. 'select * from user' vs 'select * from users'
	#
	if (my $collection_name = $class->collection_name) {
	    $entity_collections{lcfirst($collection_name)} = $class;
	}
    }

    my $server_version_str = '';

    if ($connection) {
	my $server_version =  eval {$connection->version}
	    or warn ($@ || "unable to get Elluminate version\n");

	$server_version_str  = "(Elluminate Live! $server_version) "
	    if $server_version;
    }

    my @command_opts = @{ $options{command} || [] };
    my $has_command_opts = @command_opts;

    print "Elive query $elive_version (c) 2009 - 2012 ${server_version_str}. Type 'help' for help\n"
	if $interactive && !$has_command_opts;

    my $cmd;
    my $keyw;
    my $args;

    binmode STDOUT, ":encoding(UTF-8)";

    while (($keyw||'') ne 'quit') {

	my $valid = 1;

	if ($has_command_opts) {
	    $cmd = shift( @command_opts );
	}
	else {
	    $cmd = $interactive && $term? $term->readline($prompt): <STDIN>;
	}

	last unless defined $cmd;

	$term->addhistory($cmd) if $term && $cmd ne '';
	#
	# strip leading white-space & trailing white-space + semicolon
	#
	$cmd =~ s{^ \s* (.*?) \s* ;? \s* $}{$1}x;

	if ($cmd =~ m{\S}x) {

	    if (($keyw, $args) = ($cmd =~ m{^ (\w+) \s* (.*)? $}x)) {

		if (lc($keyw) eq substr('help',0, length($keyw))) {
		    print "Elive query $elive_version help:\n\n";
		    print "connect url - connect to an Elluminate Live! server\n";
		    print "describe [entity_name] - list/show entities\n";
		    print "debug = [0|1|2|3] - set debugging level\n";
		    print "select <expr> from entity [where exp]\n";
		    print "    where expr is:\n";
		    print "      - prop1,prop2,.. - show selected properties\n";
		    print "      - *              - show all properties\n";
		    print "      - *?             - show only defined properties\n";
		    print "      - **             - show extra data (usually slower)\n";
		    print "quit                   - exit elive_query\n\n";
		}
		elsif ($keyw  =~ m{^(connect)$}i) {
		    my ($url, $user) = split(m{\s+}, $args);

		    $url ||= Elive::Util::prompt("Url ('http[s]://...'): ");

		    my $new_connection
			= _connect($adapter_class => $url,
				   username => $user,
				   %options);

		    if ($new_connection) {
			$connection->disconnect if $connection;
			$connection = $new_connection;
		    }
		}
		elsif ($keyw  =~ m{^(debug)$}i) {
		    my ($level) = ($args =~ m{^\s* = \s* ([0-9])}x);

		    if (defined $level) {
			warn "Debugging level set to $level\n";
			$level = $level + 0;

			$SIG{__WARN__} = $level > 1
			    ? \&Carp::cluck
			    : undef;

			$SIG{__DIE__} = $level
			    ? \&Carp::confess
			    : undef;

			Elive->debug($level);
		    }
		    else {
			print STDERR "usage: debug = 0..9";
		    }
		}
		elsif ($keyw =~ m{^(show|describe)$}ix) {

		    if (my ($entity_name) = ($args =~ m{^ \s* (\w+) \s* $}x)) {
			$valid = _show({%entities, %entity_collections}, $entity_name);
		    }
		    else {
			print 'usage: describe '.join('|', sort keys %entities)."\n";
			$valid = 0;
		    }
		}
		elsif (lc($keyw) eq 'select') {

		    warn "args: $args\n"
			if (Elive->debug);

		    $valid = 0;

		    if ($args =~ m{^
                          (.+?) \s+
                          from \s+ (\w+)?
                          (\s+ where \s+ (.*?))?
                          $}ix) {

			$valid = 1;

			my $entity = $2;
			my $filter = $4;
			my @props = split(m{\s* ,|\| \s*}x, $1);
			my $hide_undef = 0;

			my $entity_class = ($entities{$entity}
					    || $entity_collections{$entity});


			if (!$entity_class) {
			    print STDERR "Unknown entity: $entity\n";
			    $valid = 0;
			}
			else {

			    if (@props == 1 && $props[0] =~ m{^(\*)(\*)?(\??)$}x) {
				#
				# '*'  select all
				# '**' select all + derivable
				# '*?' select all defined
				#
				my $include_derivable = $2? 1: 0;
				$hide_undef = $3? 1: 0;
				my $property_types = $entity_class->property_types;
				@props = grep {$property_types->{$_} ne 'Any'} $entity_class->properties;
				if ($include_derivable) {
				    my %derivable = $entity_class->derivable;
				    push (@props, sort keys %derivable);
				}
			    }
			    else {
				foreach (@props) {
				    $valid = 0 unless m{^ [a-zA-Z_-]+ $}x;
				}
			    }
			}

			if ($valid) {
			    warn "entity: $entity, filter: $filter, props: @props\n"
				if (Elive->debug);

			    unless ($connection) {
				print STDERR "you'll need to connect first - see help connect\n";
			    }
			    else {
				_select($entity_class, \@props,
					connection => $connection,
					filter => $filter,
					hide_undef => $hide_undef,
					dump => $options{dump},
				    ); 
			    }
			}
		    }

		    unless ($valid) {
			print STDERR  "usage: select props|*|**|*? from ".join('|', sort keys %entities)." [where filter|id=val];\n";
		    }
		}
		elsif ($keyw !~ m{^(quit)$}x) {
		    print STDERR "unrecognised command: $keyw, type 'help' for help\n";
		    $valid = 0;
		}
	    }
	    else {
		print STDERR "unrecognised command  - type 'help' for help\n";
		$valid = 0;
	    }
	}
	exit 2 if !$valid && (!$interactive || $has_command_opts); 
    }

    Elive->disconnect;

    if (Elive->debug) {
	my @living = grep {$Elive::Entity::Elive_Objects->{$_}}
	(keys %Elive::Entity::Elive_Objects);

	warn "about to shutdown, live objects: @living\n";
    }

    return 0;
}

########################################################################

sub _show {

    my ($entities, $entity_name, $nesting, $entity_class, %seen) = @_;

    $nesting ||= 0;
    my $sp = ' ' x ($nesting * 2);

    $entity_class ||= $entities->{$entity_name}
        or do {
	    print STDERR "${sp}Unknown entity: $entity_name\n";
	    return;
	};

    return if $seen{$entity_class}++;

    my $property_types = $entity_class->property_types;
    my $property_doco = $entity_class->property_doco;
    my ($pkey) = $entity_class->primary_key;

    print "${sp}$entity_name: $entity_class:\n"
	unless $nesting;

    foreach my $property ($entity_class->properties) {

	my $type = Elive::Util::inspect_type($property_types->{$property});
	my ($elemental_type, @_other_types) = $type->union;

	my $is_primary = $pkey && $property eq $pkey;

	my $primary_str = $is_primary? 'pkey ': '';
	my $array_str = $type->is_array? ' []': '';

	my $all_types = join('|', $elemental_type, @_other_types);
	printf("%-20s : %-16s", "$sp  $property", "${primary_str}${all_types}${array_str}");
	for ($property_doco->{$property}) {
	    print "\t-- $_" if $_;
	}
	print "\n";

	if ($type->is_struct) {
	    _show($entities, $property, $nesting + 1, $elemental_type, %seen);
	}
    }

    my %derivable = $entity_class->derivable;

    if (my @derivable_cols = sort keys %derivable) {
	print "${sp}  (also selectable: ".join(', ', sort @derivable_cols).")\n"
    }

    return 1;
}

########################################################################

sub _select {
    my ($entity_class, $props, %opt) = @_;

    my $filter = $opt{filter};
    my $hide_undef = $opt{hide_undef};
    my $connection = $opt{connection}
    or die "not copnnected";

    my $property_types = $entity_class->property_types;
    my %derivable = $entity_class->derivable;
    my $entity_name = $entity_class->entity_name;
    my ($pkey) = $entity_class->primary_key;

    foreach (@$props) {
	
	unless (exists $property_types->{$_} || exists $derivable{$_}) {

	    print STDERR "unknown property: $_\n";
	    print STDERR "$entity_name has properties: ".join(', ', sort keys %$property_types)."\n";
	    if (keys %derivable) {
		print STDERR "(also selectable: ".join(', ', sort keys %derivable).")\n";
	    }
	    return;
	}
    }

    #
    # See if our filter is in the format: keyprop=val
    #
    warn "filter=$filter"
	if (defined $filter && Elive->debug);

    warn "entity: $entity_name, class: $entity_class\n"
	if (Elive->debug);
    warn join(' ', 'properties:',  $entity_class->properties)."\n"
	if (Elive->debug);

    #
    # Possible fetch on primary key or alternative key.
    # Detect and trap this as a simple fetch.
    #
    my $id;
    
    if (defined $filter) {
	if (my ($fld, $val) = ($filter =~ m{^ (\w+) \s* = \s* ([\w_\-\@\!\#\$\%\^\&\.\+]+) \s* $}x)) {
	    my $type = $property_types->{$fld} || '';
	    
	    if ($pkey && ($fld eq $pkey || $fld eq 'id')) {
		$id = $val;
	    }
	}
    }

    my @output;
    my $rows;

    eval {
	if ($id) {
	    $rows =  [grep {$_} $entity_class->retrieve([$id], connection => $connection)];
	}
	else {
	    $rows = $entity_class->list(filter => $filter, connection => $connection);
	}
    };

    if ($@) {
	print "error: $@";
    }
    elsif (!@$rows) {
	print "No results.\n";
    }
    elsif ($opt{dump} && $opt{dump} =~ /^(yaml)$/i) {
	foreach my $row (@$rows) {
	    print YAML::Syck::Dump({$entity_name => _fetch_row_href($props, \%derivable, $row)});
	}
    }
    else {
	foreach my $row (@$rows) {
	    
	    my $row =  _fetch_row_aref($props, \%derivable, $row);
	    push(@output, $row);
	}

	#
	# pass 1: filter columns
	#

	my @show;

	foreach my $row (@output) {
	    
	    my $col = 0;

	    foreach (@$row) {
		$show[ $col++ ] ||= !$hide_undef || defined $_
	    }
	}

	#
	# pass 2: compute output widths
	#

	my @widths;

	foreach  my $row ($props,@output) {

	    my $col = 0;

	    foreach (@$row) {

		my $this_width = length(defined($_)? $_ : '(undef)');

		for ($widths[ $col++ ]) {
		    $_ = $this_width
			if (!defined || $this_width > $_);
		}
	    }
	}
	
	#
	# pass 3: output
	#

	my @hrule = map {'-' x ($_||0)} @widths;

	foreach my $row ($props, \@hrule, @output) {

	    my $col = 0;
	    my @cols = (

		grep {$_}

		map {
		    my $v = defined($_) ? $_ : '(undef)';
		    my $s = $show[ $col ];
		    my $w = $widths[ $col ];
		    ++$col;
		    $s ? sprintf('%-*s', $w, $v)
			: undef;
		}

		@$row);

	    print join('|', map{defined($_) ? $_ : '(undef)'} @cols)."\n"; 
	}
    }

    return;
}

########################################################################

sub _fetch_row_aref {
    my $props = shift;
    my $derivable = shift;
    my $row = shift;

    my @vals = (map {Elive::Util::string($_)} 
		map {$row->$_}
		map {$derivable->{$_} || $_}
		@$props);

    s{\n}{\\n}g foreach grep {defined} @vals;

    return \@vals;
}

########################################################################

sub _fetch_row_href {
    my $props = shift;
    my $derivable = shift;
    my $row = shift;


    my %vals = (map {
	my $meth = $derivable->{$_} || $_;
	my $val = $row->$meth;
	$_ => $val
	} @$props);

    return \%vals;
}

########################################################################

sub _getopt {

    my %options;

    Getopt::Long::GetOptions(\%options,
			     'username|user=s',
			     'password|pass=s',
			     'command|c=s@',
                             'dump=s',
			     'debug=i',
			     'adapter=s',
			     'help|?',
			     'version|v',
			     'help|?'
	) or pod2usage(2);

    pod2usage(0) if ($options{help});

    if ($options{version}) {
	print "Elive v${Elive::VERSION} (c) 2009 - 2012\n";
	exit 0;
    }

    if ($options{dump} && $options{dump} !~ /^(yaml)$/i) {
	pod2usage("unknown dump format: $options{dump}");
    }

    my $url = shift(@ARGV);

    return ($url, %options);
}

########################################################################

sub _connect {
    my ($adapter_class, $url, %options) = @_;

    my $username = $options{username};
    my $password = $options{password};

    my $uri_obj = URI->new($url);
    if (my $userinfo = $uri_obj->userinfo) {
	#
	# credentials supplied in URI

	my ($uri_user, $uri_pass) = split(':', $userinfo, 2);
	$username ||= URI::Escape::uri_unescape($uri_user);
	$password ||= URI::Escape::uri_unescape($uri_pass)
	    if $uri_pass;

	$url =~ s{\Q${userinfo}\E\@}{};
    }

    $username ||= Elive::Util::prompt('Username: ');
    $password ||= Elive::Util::prompt('Password: ', password => 1);

    print STDERR "connecting to $url...";
    my $connection = eval {
	$adapter_class->connect($url, $username, $password);
    };

    if ($@) {
	print STDERR "\nconnection failed: $@";
	return;
    }

    print STDERR "ok\n";
    return $connection;
}

########################################################################

1;