The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Prty::Confluence::Client;
use base qw/Prty::Hash/;

use strict;
use warnings;
use utf8;

our $VERSION = 1.124;

use LWP::UserAgent ();
use Prty::Option;
use Prty::Confluence::Markup;
use JSON ();
use Prty::Confluence::Page;
use HTTP::Request::Common ();

# -----------------------------------------------------------------------------

=encoding utf8

=head1 NAME

Prty::Confluence::Client - Confluence-Wiki Client

=head1 BASE CLASS

L<Prty::Hash>

=head1 DESCRIPTION

Ein Objekt der Klasse repräsentiert einen Client, der über die
L<Confluence REST API|https://docs.atlassian.com/confluence/REST/latest/> mit einem
Confluence-Server kommunizieren kann.

Die Implementierung der Klasse stellt die maßgeblichen Mechnismen
zur Kommunikation mit dem Server zur Verfügung, realisiert
z.Zt. jedoch nur einen kleinen Ausschnitt der Funktionalität der
Confluence REST API. Die Implementierung wird nach Bedarf
erweitert.

=head1 METHODS

=head2 Konstruktor

=head3 new() - Instantiiere Confluence-Client

=head4 Synopsis

    $cli = $class->new(@keyVal);

=head4 Arguments

=over 4

=item url => $url (Default: nichts)

Basis-URL des Confluence Wiki, z.B. "https://<name>.atlassian.net".

=item user => $user (Default: nichts)

Name des Confluence-Benutzers, z.B. "admin".

=item password => $password (Default: nichts)

Passwort des Confluence-Benutzers.

=item verbose => $bool (Default: 0)

Gib Laufzeit-Informationen auf STDERR aus.

=back

=head4 Returns

Client-Objekt (Typ Prty::Confluence::Client)

=head4 Description

Instantiiere einen Client für Confluence mit den Eigenschaften
@keyval und liefere eine Referenz auf dieses Objekt zurück.

=head4 Example

Client für Atlassian Demo-Instanz:

    $cli = Prty::Confluence::Client->new(
        url => 'https://<name>.atlassian.net',
        user => 'admin',
        password => '<password>',
        verbose => 1,
    );

=cut

# -----------------------------------------------------------------------------

sub new {
    my $class = shift;
    # @_: @keyVal

    my $self = $class->SUPER::new(
        url => undef,
        user => undef,
        password => undef,
        verbose => 0,
        ua => LWP::UserAgent->new,
    );
    $self->set(@_);

    return $self;
}

# -----------------------------------------------------------------------------

=head2 Confluence Operationen

=head3 createPage() - Erzeuge Confluence Seite

=head4 Synopsis

    $pag = $cli->createPage($parentId,$title,$markup,@opts);

=head4 Arguments

=over 4

=item parentId => $pageId

Die Page-Id der übergeordneten Seite.

=item $title

Der Titel der Seite.

=item $markup

Seiteninhalt in Confluence Wiki Markup.

=back

=head4 Options

=over 4

=item -warning => $bool (Default: 0)

Setze eine Warnung an den Anfang der Seite, dass die Seite automatisch
generiert wurde.

=back

=head4 Returns

nichts

=head4 Description

Erzeuge eine Confluence-Seite mit Titel $title und Inhalt $markup
(= Wiki Code) als Unterseite von der Seite mit der Seiten-Id
$parentId und liefere das Seiten-Objekt der der erzeugten Seite
zurück.

Die erzeugte Seite ist (notwendigerweise) demselben Space wie die
übergeordnete Seite zugeordnet.

=cut

# -----------------------------------------------------------------------------

sub createPage {
    my ($self,$parentId,$title,$markup,@opts) = @_;

    # Optionen

    my $warning = 0;

    Prty::Option->extract(\@_,
        -warning => \$warning,
    );

    # Führe Operation aus

    if ($warning) {
        my $gen = Prty::Confluence::Markup->new;
        $markup = $gen->paragraph(
            $gen->fmt('italic',q~
                Achtung: Diese Seite wurde von einem Programm erzeugt.
                Manuelle Änderungen gehen mit der nächsten Erzeugung
                verloren!
            ~),
        ).$markup;
    }

    my $pag = $self->getPage($parentId);

    my $res = $self->send(
        POST => "rest/api/content",
        'application/json',
        JSON::encode_json({
            title => $title,
            type => 'page',
            space => {
                key => $pag->space,
            },
            $parentId? (
                ancestors => [{
                    id => $parentId,
                }],
            ): (),
            body => {
                storage => {
                    representation => 'wiki',
                    value => $markup,
                },
            },
        })
    );

    # Instantiiere Seiten-Objekt der erzeugten Seite und liefere
    # dieses zurück

    $pag = Prty::Confluence::Page->new($res->content);
    if ($self->verbose) {
        warn sprintf "---RESULT---\n%s\n",$pag->asString;
    }

    return $pag;
}

# -----------------------------------------------------------------------------

=head3 deletePage() - Lösche Confluence Seite

=head4 Synopsis

    $pag = $cli->deletePage($pageId);

=head4 Arguments

=over 4

=item $pageId

Seiten-Id

=back

=head4 Returns

Nichts

=head4 Description

Lösche die Confluence-Seite mit der Seiten-Id $pageId.

=cut

# -----------------------------------------------------------------------------

sub deletePage {
    my ($self,$pageId) = @_;

    my $res = $self->send('DELETE',"rest/api/content/$pageId");
    my $pag = Prty::Confluence::Page->new($res->content);
    if ($self->verbose) {
        warn sprintf "---RESULT---\n%s\n",$pag->asString;
    }

    return;
}

# -----------------------------------------------------------------------------

=head3 getPage() - Liefere Confluence Seite

=head4 Synopsis

    $pag = $cli->getPage($pageId);

=head4 Arguments

=over 4

=item $pageId

Seiten-Id

=back

=head4 Returns

Seiten-Objekt (Typ Prty::Confluence::Page)

=head4 Description

Rufe die Confluence-Seite mit der Seiten-Id $pageId ab und liefere
ein Seiten-Objekt vom Typ Prty::Confluence::Page zurück.

=cut

# -----------------------------------------------------------------------------

sub getPage {
    my ($self,$pageId) = @_;

    my $res = $self->send('GET',"rest/api/content/$pageId");
    my $pag = Prty::Confluence::Page->new($res->content);
    if ($self->verbose) {
        warn sprintf "---RESULT---\n%s\n",$pag->asString;
    }

    return $pag;
}

# -----------------------------------------------------------------------------

=head3 updatePage() - Aktualisiere Confluence Seite

=head4 Synopsis

    $cli->updatePage($pageId,$markup,@opts);

=head4 Arguments

=over 4

=item $pageId

Seiten-Id

=item $markup

Seiteninhalt in Confluence Wiki Markup

=back

=head4 Options

=over 4

=item -warning => $bool (Default: 0)

Setze eine Warnung an den Anfang der Seite, dass die Seite automatisch
erzeugt wurde.

=item -title => $title

Setze den Seitentitel.

=back

=head4 Returns

nichts

=head4 Description

Ersetze den Inhalt der Confluence-Seite $pageId durch den neuen
Inhalt $markup. Für die Aktualisierung sind vier Angaben
erforderlich:

=over 2

=item *

die PageId der Seite

=item *

der Inhalt der Seite

=item *

der Titel der Seite

=item *

die I<neue> Versionsnummer der Seite

=back

Um die neue Versionsnummer der Seite vergeben zu können, wird
intern zunächst der aktuelle Stand der Seite abgerufen, der
u.a. die bestehende Versionsnummer enthält. Die Versionsnummer ist
eine ganze Zahl, die mit jeder Aktualisierung um 1 erhöht werden
muss.

Der Titel der Seite wird aus dem aktuellen Stand der Seite
übernommen, sofern er nicht mit der Option -title überschrieben
wird.

=cut

# -----------------------------------------------------------------------------

sub updatePage {
    my ($self,$pageId,$markup,@opts) = @_;

    # Optionen

    my $warning = 0;
    my $title = undef;

    Prty::Option->extract(\@_,
        -warning => \$warning,
        -title => \$title,
    );

    # Führe Operation aus

    if ($warning) {
        my $gen = Prty::Confluence::Markup->new;
        $markup = $gen->paragraph(
            $gen->fmt('italic',q~
                Achtung: Diese Seite wurde von einem Programm erzeugt.
                Manuelle Änderungen gehen mit der nächsten Erzeugung
                verloren!
            ~),
        ).$markup;
    }
            
    my $pag = $self->getPage($pageId);

    $self->send(
        PUT => "rest/api/content/$pageId",
        'application/json',
        JSON::encode_json({
            type => 'page',
            title => $title || $pag->title,
            body => {
                storage => {
                    representation => 'wiki',
                    value => $markup,
                },
            },
            version => {
                number => $pag->version+1,
            },
        })
    );

    return;
}

# -----------------------------------------------------------------------------

=head3 createAttachment() - Füge Attachment zu Confluence-Seite hinzu

=head4 Synopsis

    $pag = $cli->createAttachment($parentId,$file);

=head4 Arguments

=over 4

=item parentId => $pageId

Die Page-Id der übergeordneten Seite.

=item $file

Pfad zur Attchment-Datei.

=back

=head4 Returns

nichts

=head4 Description

Füge Datei $file als Attachment zur Confluence-Seite mit der
Seiten-Id $pageId hinzu.

=cut

# -----------------------------------------------------------------------------

sub createAttachment {
    my ($self,$parentId,$path) = @_;

    $self->send(
        POST => "rest/api/content/$parentId/child/attachment",
        'form-data',[
            file => [$path],
        ],
    );

    return;
}

# -----------------------------------------------------------------------------

=head2 Hilfsmethoden

Die folgenden Methoden bilden die Grundlage für die Kommunikation
mit dem Confluence-Server. Sie werden normalerweise nicht direkt
gerufen.

=head3 send() - Sende HTTP-Request an Confluence

=head4 Synopsis

    $res = $cli->send($method,$path);
    $res = $cli->send($method,$path,$contentType,$content);

=head4 Arguments

=over 4

=item $method

Die HTTP-Methode, z.B. 'PUT'.

=item $path

Der REST-Pfad, z.B. 'rest/api/content/32788'.

=item $contentType

Der Content-Type des HTTP-Body, z.B. 'application/json'.

=item $content

Der Inhalt des HTTP-Body, z.B. (auf die Toplevel-Attribute umbrochen)

    {"version":{"number":24},
    "body":{"storage":{"representation":"wiki","value":"{cheese}"}},
    "title":"Testseite",
    "type":"page"}

=back

=head4 Returns

HTTP-Antwort (Typ HTTP::Response)

=head4 Description

Sende einen HTTP-Request vom Typ $method mit dem REST-Pfad $path
und dem Body $content vom Typ $contentType an den Confluence-Server
und liefere die resultierende HTTP-Anwort zurück. Im Fehlerfall
wirft die Methode eine Exception.

=cut

# -----------------------------------------------------------------------------

sub send {
    my ($self,$method,$path,$contentType,$content) = @_;
    
    my ($ua,$user,$password,$verbose) =
        $self->get(qw/ua user password verbose/);

    my $req;
    if ($method eq 'POST' && $contentType eq 'form-data') {
        # Attachments. Bei dieser POST-Funktion kann $content
        # eine Array-Referenz mit mehreren Inhalten sein, die
        # per multipart/form-data gepostet werden.

        $req = HTTP::Request::Common::POST(
            $self->url($path),
            'X-Atlassian-Token' => 'nocheck',
            Content_Type => $contentType,
            Content => $content,
        );
    }
    else {
        $req = HTTP::Request->new(
            $method => $self->url($path),
        );
        if ($contentType) {
            $req->header('Content-Type' => "$contentType; charset=utf-8");
            $req->content($content) ;
        }
    }
    $req->authorization_basic($user,$password);

    if ($verbose) {
        warn sprintf "---REQUEST---\n%s",$req->as_string;
    }

    my $res = $ua->request($req);
    if (!$res->is_success) {
        $self->throw(
            q~CLIENT-00001: HTTP request failed~,
            StatusLine => $res->status_line,
            Response => $res->content,
        );
    }
    if ($verbose) {
        warn sprintf "---RESPONSE---\n%s",$res->as_string;
    }

    return $res;
}

# -----------------------------------------------------------------------------

=head3 url() - Erzeuge Request URL

=head4 Synopsis

    $url = $cli->url;
    $url = $cli->url($path);

=head4 Arguments

=over 4

=item $path

REST-API Pfad I<ohne> führenden Slash,
z.B. 'wiki/rest/api/content/32788'.

=back

=head4 Returns

URL (String)

=head4 Description

Erzeuge einen REST-API URL bestehend aus dem beim Konstruktor-Aufruf
angegebenen Server-URL und dem Pfad $path und liefere diesen zurück.
Ohne Argument wird der Server-URL geliefert.

=head4 Example

Der Code

    $cli = Prty::Confluence::Client->new(
        url => 'https://<name>.atlassian.net',
        ...
    );
    $url = $cli->url('wiki/rest/api/content/32788');

liefert

    https://<name>.atlassian.net/wiki/rest/api/content/32788

=cut

# -----------------------------------------------------------------------------

sub url {
    my ($self,$path) = @_;

    my $url = $self->get('url');
    if ($path) {
        $url .= "/$path";
    }

    return $url;
}

# -----------------------------------------------------------------------------

=head1 VERSION

1.124

=head1 AUTHOR

Frank Seitz, L<http://fseitz.de/>

=head1 COPYRIGHT

Copyright (C) 2018 Frank Seitz

=head1 LICENSE

This code is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

# -----------------------------------------------------------------------------

1;

# eof