Group
Extension

Artifactory-Client/lib/Artifactory/Client.pm

package Artifactory::Client;

use strict;
use warnings FATAL => 'all';

use Moose;

use Data::Dumper;
use URI;
use JSON::MaybeXS;
use LWP::UserAgent;
use Path::Tiny qw();
use MooseX::StrictConstructor;
use URI::Escape qw(uri_escape);
use File::Basename qw(basename);
use HTTP::Request::StreamingUpload;

use namespace::autoclean;

=head1 NAME

Artifactory::Client - Perl client for Artifactory REST API

=head1 VERSION

Version 1.8.0

=cut

our $VERSION = 'v1.8.0';

=head1 SYNOPSIS

This is a Perl client for Artifactory REST API:
https://www.jfrog.com/confluence/display/RTF/Artifactory+REST+API Every public method provided in this module returns a
HTTP::Response object.

    use Artifactory::Client;

    my $h = HTTP::Headers->new();
    $h->authorization_basic( 'admin', 'password' );
    my $ua = LWP::UserAgent->new( default_headers => $h );

    my $args = {
        artifactory  => 'http://artifactory.server.com',
        port         => 8080,
        repository   => 'myrepository',
        context_root => '/', # Context root for artifactory. Defaults to 'artifactory'.
        ua           => $ua  # Dropping in custom UA with default_headers set.  Default is a plain LWP::UserAgent.
    };

    my $client = Artifactory::Client->new( $args );
    my $path = '/foo'; # path on artifactory

    # Properties are a hashref of key-arrayref pairs.  Note that value must be an arrayref even for a single element.
    # This is to conform with Artifactory which treats property values as a list.
    my $properties = {
        one => ['two'],
        baz => ['three'],
    };
    my $file = '/local/file.xml';

    # Name of methods are taken straight from Artifactory REST API documentation.  'Deploy Artifact' would map to
    # deploy_artifact method, like below.  The caller gets HTTP::Response object back.
    my $resp = $client->deploy_artifact( path => $path, properties => $properties, file => $file );

    # Custom requests can also be made via usual get / post / put / delete requests.
    my $resp = $client->get( 'http://artifactory.server.com/path/to/resource' );

    # Repository override for calls that have a repository in the endpoint.  The passed-in repository will not persist.
    my $resp = $client->calculate_yum_repository_metadata( repository => 'different_repo', async => 1 );

=cut

=head1 Dev Env Setup / Running Tests

    carton install

    # to run unit tests
    prove -r t

=cut

has 'artifactory' => (
    is       => 'ro',
    isa      => 'Str',
    required => 1,
    writer   => '_set_artifactory',
);

has 'port' => (
    is      => 'ro',
    isa     => 'Int',
    default => 80,
);

has 'context_root' => (
    is      => 'ro',
    isa     => 'Str',
    default => 'artifactory',
);

has 'ua' => (
    is      => 'rw',
    isa     => 'LWP::UserAgent',
    builder => '_build_ua',
    lazy    => 1,
);

has 'repository' => (
    is      => 'ro',
    isa     => 'Str',
    default => '',
    writer  => '_set_repository',
);

has '_json' => (
    is      => 'ro',
    builder => '_build_json',
    lazy    => 1,
);

has '_api_url' => (
    is       => 'ro',
    isa      => 'Str',
    init_arg => undef,
    writer   => '_set_api_url',
);

has '_art_url' => (
    is       => 'ro',
    isa      => 'Str',
    init_arg => undef,
    writer   => '_set_art_url',
);

sub BUILD {
    my ($self) = @_;

    # Save URIs
    my $uri = URI->new( $self->artifactory() );
    $uri->port( $self->port );
    my $context_root = $self->context_root();
    $context_root = '' if ( $context_root eq '/' );

    $uri->path_segments( $context_root, );
    my $_art_url = $uri->canonical()->as_string();
    $_art_url =~ s{\/$}{}xi;
    $self->_set_art_url($_art_url);

    $uri->path_segments( $context_root, 'api' );
    $self->_set_api_url( $uri->canonical()->as_string() );

    # Save Repository
    my $repo = $self->repository;
    $repo =~ s{^\/}{}xi;
    $repo =~ s{\/$}{}xi;
    $self->_set_repository($repo);

    return 1;
}

=head1 GENERIC METHODS

=cut

=head2 get( @args )

Invokes GET request on LWP::UserAgent-like object; params are passed through.

=cut

sub get {
    my ( $self, @args ) = @_;
    return $self->_request( 'get', @args );
}

=head2 post( @args )

nvokes POST request on LWP::UserAgent-like object; params are passed through.

=cut

sub post {
    my ( $self, @args ) = @_;
    return $self->_request( 'post', @args );
}

=head2 put( @args )

Invokes PUT request on LWP::UserAgent-like object; params are passed through.

=cut

sub put {
    my ( $self, @args ) = @_;
    return $self->_request( 'put', @args );
}

=head2 delete( @args )

Invokes DELETE request on LWP::UserAgent-like object; params are passed
through.

=cut

sub delete {
    my ( $self, @args ) = @_;
    return $self->_request( 'delete', @args );
}

=head2 request( @args )

Invokes request() on LWP::UserAgent-like object; params are passed through.

=cut

sub request {
    my ( $self, @args ) = @_;
    return $self->_request( 'request', @args );
}

=head1 BUILDS

=cut

=head2 all_builds

Retrieves information on all builds from artifactory.

=cut

sub all_builds {
    my $self = shift;
    return $self->_get_build('');
}

=head2 build_runs( $build_name )

Retrieves information of a particular build from artifactory.

=cut

sub build_runs {
    my ( $self, $build ) = @_;
    return $self->_get_build($build);
}

=head2 build_upload( $path_to_json )

Upload Build

=cut

sub build_upload {
    my ( $self, $json_file ) = @_;

    open( my $fh, '<', $json_file );
    chomp( my @lines = <$fh> );
    my $json_input = join( "", @lines );
    my $data       = $self->_json->decode($json_input);
    my $url        = $self->_api_url() . "/build";
    return $self->put(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode($data)
    );
}

=head2 build_info( $build_name, $build_number )

Retrieves information of a particular build number.

=cut

sub build_info {
    my ( $self, $build, $number ) = @_;
    return $self->_get_build("$build/$number");
}

=head2 builds_diff( $build_name, $new_build_number, $old_build_number )

Retrieves diff of 2 builds

=cut

sub builds_diff {
    my ( $self, $build, $new, $old ) = @_;
    return $self->_get_build("$build/$new?diff=$old");
}

=head2 build_promotion( $build_name, $build_number, $payload )

Promotes a build by POSTing payload

=cut

sub build_promotion {
    my ( $self, $build, $number, $payload ) = @_;

    my $url = $self->_api_url() . "/build/promote/$build/$number";
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode($payload)
    );
}

=head2 promote_docker_image( targetRepo => "target_repo", dockerRepository => "dockerRepository", tag => "tag", copy => 'false' )

Promotes a Docker image from one repository to another

=cut

sub promote_docker_image {
    my ( $self, %args ) = @_;

    my $repo = $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/docker/$repo/v2/promote";
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 delete_builds( name => $build_name, buildnumbers => [ buildnumbers ], artifacts => 0,1, deleteall => 0,1 )

Removes builds stored in Artifactory. Useful for cleaning up old build info data

=cut

sub delete_builds {
    my ( $self, %args ) = @_;
    my $build        = $args{name};
    my $buildnumbers = $args{buildnumbers};
    my $artifacts    = $args{artifacts};
    my $deleteall    = $args{deleteall};

    my $url = $self->_api_url() . "/build/$build";
    my @params = $self->_gather_delete_builds_params( $buildnumbers, $artifacts, $deleteall );

    if (@params) {
        $url .= "?";
        $url .= join( "&", @params );
    }
    return $self->delete($url);
}

=head2 build_rename( $build_name, $new_build_name )

Renames a build

=cut

sub build_rename {
    my ( $self, $build, $new_build ) = @_;

    my $url = $self->_api_url() . "/build/rename/$build?to=$new_build";
    return $self->post($url);
}

=head2 distribute_build( 'build_name', $build_number, %hash_of_json_payload )

Deploys builds from Artifactory to Bintray, and creates an entry in the corresponding Artifactory distribution
repository specified.

=cut

sub distribute_build {
    my ( $self, $build_name, $build_number, %args ) = @_;

    my $url = $self->_api_url() . "/build/distribute/$build_name/$build_number";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%args )
    );
}

=head2 control_build_retention( 'build_name', deleteBuildArtifacts => 'true', count => 100, ... )

Specifies retention parameters for build info.

=cut

sub control_build_retention {
    my ( $self, $build_name, %args ) = @_;

    my $url = $self->_api_url() . "/build/retention/$build_name";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%args )
    );
}

=head1 ARTIFACTS & STORAGE

=cut

=head2 folder_info( $path )

Returns folder info

=cut

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

    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path";

    return $self->get($url);
}

=head2 file_info( $path )

Returns file info

=cut

sub file_info {
    my ( $self, $path ) = @_;
    return $self->folder_info($path);    # should be OK to do this
}

=head2 get_storage_summary_info

Returns storage summary information regarding binaries, file store and repositories

=cut

sub get_storage_summary_info {
    my $self = shift;
    my $url  = $self->_api_url() . '/storageinfo';
    return $self->get($url);
}

=head2 item_last_modified( $path )

Returns item_last_modified for a given path

=cut

sub item_last_modified {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path?lastModified";
    return $self->get($url);
}

=head2 file_statistics( $path )

Returns file_statistics for a given path

=cut

sub file_statistics {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path?stats";
    return $self->get($url);
}

=head2 item_properties( path => $path, properties => [ key_names ] )

Takes path and properties then get item properties.

=cut

sub item_properties {
    my ( $self, %args ) = @_;

    my $path       = $args{path};
    my $properties = $args{properties};

    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path?properties";

    if ( ref($properties) eq 'ARRAY' ) {
        my $str = join( ',', @{$properties} );
        $url .= "=" . $str;
    }
    return $self->get($url);
}

=head2 set_item_properties( path => $path, properties => { key => [ values ] }, recursive => 0,1 )

Takes path and properties then set item properties.  Supply recursive => 0 if you want to suppress propagation of
properties downstream.  Note that properties are a hashref with key-arrayref pairs, such as:

    $prop = { key1 => ['a'], key2 => ['a', 'b'] }

=cut

sub set_item_properties {
    my ( $self, %args ) = @_;

    my $path       = $args{path};
    my $properties = $args{properties};
    my $recursive  = $args{recursive};

    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path?properties=";

    my $request = $url . $self->_attach_properties( properties => $properties );
    $request .= "&recursive=$recursive" if ( defined $recursive );
    return $self->put($request);
}

=head2 delete_item_properties( path => $path, properties => [ key_names ], recursive => 0,1 )

Takes path and properties then delete item properties.  Supply recursive => 0 if you want to suppress propagation of
properties downstream.

=cut

sub delete_item_properties {
    my ( $self, %args ) = @_;

    my $path       = $args{path};
    my $properties = $args{properties};
    my $recursive  = $args{recursive};

    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/storage/$path?properties=" . join( ",", @{$properties} );
    $url .= "&recursive=$recursive" if ( defined $recursive );
    return $self->delete($url);
}

=head2 set_item_sha256_checksum( repoKey => 'foo', path => 'bar' )

Calculates an artifact's SHA256 checksum and attaches it as a property (with key "sha256"). If the artifact is a folder,
then recursively calculates the SHA256 of each item in the folder and attaches the property to each item.

=cut

sub set_item_sha256_checksum {
    my ( $self, %args ) = @_;
    my $url = $self->_api_url() . '/checksum/sha256';
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 retrieve_artifact( $path, $filename )

Takes path and retrieves artifact on the path.  If $filename is given, artifact content goes into the $filename rather
than the HTTP::Response object.

=cut

sub retrieve_artifact {
    my ( $self, $path, $filename ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_art_url() . "/$path";
    return ($filename)
      ? $self->get( $url, ":content_file" => $filename )
      : $self->get($url);
}

=head2 retrieve_latest_artifact( path => $path, version => $version, release => $release, integration => $integration,
 flag => 'snapshot', 'release', 'integration' )

Takes path, version, flag of 'snapshot', 'release' or 'integration' and retrieves artifact

=cut

sub retrieve_latest_artifact {
    my ( $self, %args ) = @_;

    my $path        = $args{path};
    my $version     = $args{version};
    my $release     = $args{release};
    my $integration = $args{integration};
    my $flag        = $args{flag};
    $path = $self->_merge_repo_and_path($path);

    my $base_url = $self->_art_url() . "/$path";
    my $basename = basename($path);
    my $url;
    $url = "$base_url/$version-SNAPSHOT/$basename-$version-SNAPSHOT.jar" if ( $version && $flag eq 'snapshot' );
    $url = "$base_url/$release/$basename-$release.jar"                   if ( $flag eq 'release' );
    $url = "$base_url/$version-$integration/$basename-$version-$integration.jar"
      if ( $version && $flag eq 'integration' );
    return $self->get($url);
}

=head2 retrieve_build_artifacts_archive( $payload )

Takes payload (hashref) then retrieve build artifacts archive.

=cut

sub retrieve_build_artifacts_archive {
    my ( $self, $payload ) = @_;

    my $url = $self->_api_url() . "/archive/buildArtifacts";
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode($payload)
    );
}

=head2 retrieve_folder_or_repository_archive( path => '/foobar', archiveType => 'zip' )

Retrieves an archive file (supports zip/tar/tar.gz/tgz) containing all the artifacts that reside under the specified
path (folder or repository root). Requires Enable Folder Download to be set.

=cut

sub retrieve_folder_or_repository_archive {
    my ( $self, %args ) = @_;
    my $path = delete $args{path};
    my $url = $self->_api_url() . '/archive/download' . $path . '?' . $self->_stringify_hash( '', %args );
    return $self->get($url);
}

=head2 trace_artifact_retrieval( $path )

Takes path and traces artifact retrieval

=cut

sub trace_artifact_retrieval {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_art_url() . "/$path?trace";
    return $self->get($url);
}

=head2 archive_entry_download( $path, $archive_path )

Takes path and archive_path, retrieves an archived resource from the specified archive destination.

=cut

sub archive_entry_download {
    my ( $self, $path, $archive_path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_art_url() . "/$path!$archive_path";
    return $self->get($url);
}

=head2 create_directory( path => $path, properties => { key => [ values ] } )

Takes path, properties then create a directory.  Directory needs to end with a /, such as "/some_dir/".

=cut

sub create_directory {
    my ( $self, %args ) = @_;
    return $self->deploy_artifact(%args);
}

=head2 deploy_artifact( path => $path, properties => { key => [ values ] }, file => $file )

Takes path on Artifactory, properties and filename then deploys the file.  Note that properties are a hashref with
key-arrayref pairs, such as:

    $prop = { key1 => ['a'], key2 => ['a', 'b'] }

=cut

sub deploy_artifact {
    my ( $self, %args ) = @_;

    my $path       = $args{path};
    my $properties = $args{properties};
    my $file       = $args{file};
    my $header     = $args{header};

    $path = $self->_merge_repo_and_path($path);
    my @joiners = ( $self->_art_url() . "/$path" );
    my $props = $self->_attach_properties( properties => $properties, matrix => 1 );
    push @joiners, $props if ($props);    # if properties aren't passed in, the function returns empty string

    my $url = join( ";", @joiners );
    my $req = HTTP::Request::StreamingUpload->new(
        PUT     => $url,
        headers => HTTP::Headers->new( %{$header} ),
        ( $file ? ( fh => Path::Tiny::path($file)->openr_raw() ) : () ),
    );
    return $self->request($req);
}

=head2 deploy_artifact_by_checksum( path => $path, properties => { key => [ values ] }, file => $file, sha1 => $sha1 )

Takes path, properties, filename and sha1 then deploys the file.  Note that properties are a hashref with key-arrayref
pairs, such as:

    $prop = { key1 => ['a'], key2 => ['a', 'b'] }

=cut

sub deploy_artifact_by_checksum {
    my ( $self, %args ) = @_;

    my $sha1   = $args{sha1};
    my $header = {
        'X-Checksum-Deploy' => 'true',
        'X-Checksum-Sha1'   => $sha1,
    };
    $args{header} = $header;
    return $self->deploy_artifact(%args);
}

=head2 deploy_artifacts_from_archive( path => $path, file => $file )

Path is the path on Artifactory, file is path to local archive.  Will deploy $file to $path.

=cut

sub deploy_artifacts_from_archive {
    my ( $self, %args ) = @_;

    my $header = { 'X-Explode-Archive' => 'true', };
    $args{header} = $header;
    return $self->deploy_artifact(%args);
}

=head2 push_a_set_of_artifacts_to_bintray( descriptor => 'foo', gpgPassphrase => 'top_secret', gpgSign => 'true' )

Push a set of artifacts to Bintray as a version.  Uses a descriptor file (that must have 'bintray-info' in it's filename
and a .json extension) that was deployed to artifactory, the call accepts the full path to the descriptor as a
parameter.

=cut

sub push_a_set_of_artifacts_to_bintray {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . "/bintray/push";
    my $params = $self->_stringify_hash( '&', %args );
    $url .= "?" . $params if ($params);
    return $self->post($url);
}

=head2 push_docker_tag_to_bintray( dockerImage => 'jfrog/ubuntu:latest', async => 'true', ... )

Push Docker tag to Bintray.  Calculation can be synchronous (the default) or asynchronous.  You will need to enter your
Bintray credentials, for more details, please refer to Entering your Bintray credentials.

=cut

sub push_docker_tag_to_bintray {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . '/bintray/docker/push/' . $self->repository();
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 distribute_artifact( publish => 'true', async => 'false' )

Deploys artifacts from Artifactory to Bintray, and creates an entry in the corresponding Artifactory distribution
repository specified

=cut

sub distribute_artifact {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . '/distribute';
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 file_compliance_info( $path )

Retrieves file compliance info of a given path.

=cut

sub file_compliance_info {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/compliance/$path";
    return $self->get($url);
}

=head2 delete_item( $path )

Delete $path on artifactory.

=cut

sub delete_item {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_art_url() . "/$path";
    return $self->delete($url);
}

=head2 copy_item( from => $from, to => $to, dry => 1, suppressLayouts => 0/1, failFast => 0/1 )

Copies an artifact from $from to $to.  Note that for this particular API call, the $from and $to must include repository
names as copy source and destination may be different repositories.  You can also supply dry, suppressLayouts and
failFast values as specified in the documentation.

=cut

sub copy_item {
    my ( $self, %args ) = @_;
    $args{method} = 'copy';
    return $self->_handle_item(%args);
}

=head2 move_item( from => $from, to => $to, dry => 1, suppressLayouts => 0/1, failFast => 0/1 )

Moves an artifact from $from to $to.  Note that for this particular API call, the $from and $to must include repository
names as copy source and destination may be different repositories.  You can also supply dry, suppressLayouts and
failFast values as specified in the documentation.

=cut

sub move_item {
    my ( $self, %args ) = @_;
    $args{method} = 'move';
    return $self->_handle_item(%args);
}

=head2 get_repository_replication_configuration

Get repository replication configuration

=cut

sub get_repository_replication_configuration {
    my $self = shift;
    return $self->_handle_repository_replication_configuration('get');
}

=head2 set_repository_replication_configuration( $payload )

Set repository replication configuration

=cut

sub set_repository_replication_configuration {
    my ( $self, $payload ) = @_;
    return $self->_handle_repository_replication_configuration( 'put', $payload );
}

=head2 update_repository_replication_configuration( $payload )

Update repository replication configuration

=cut

sub update_repository_replication_configuration {
    my ( $self, $payload ) = @_;
    return $self->_handle_repository_replication_configuration( 'post', $payload );
}

=head2 delete_repository_replication_configuration

Delete repository replication configuration

=cut

sub delete_repository_replication_configuration {
    my $self = shift;
    return $self->_handle_repository_replication_configuration('delete');
}

=head2 scheduled_replication_status

Gets scheduled replication status of a repository

=cut

sub scheduled_replication_status {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/replication/$repository";
    return $self->get($url);
}

=head2 pull_push_replication( payload => $payload, path => $path )

Schedules immediate content replication between two Artifactory instances

=cut

sub pull_push_replication {
    my ( $self, %args ) = @_;
    my $payload = $args{payload};
    my $path    = $args{path};
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/replication/execute/$path";
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode($payload)
    );
}

=head2 create_or_replace_local_multi_push_replication( $payload )

Creates or replaces a local multi-push replication configuration. Supported by local and local-cached repositories

=cut

sub create_or_replace_local_multi_push_replication {
    my ( $self, $payload ) = @_;
    return $self->_handle_multi_push_replication( $payload, 'put' );
}

=head2 update_local_multi_push_replication( $payload )

Updates a local multi-push replication configuration. Supported by local and local-cached repositories

=cut

sub update_local_multi_push_replication {
    my ( $self, $payload ) = @_;
    return $self->_handle_multi_push_replication( $payload, 'post' );
}

=head2 delete_local_multi_push_replication( $url )

Deletes a local multi-push replication configuration. Supported by local and local-cached repositories

=cut

sub delete_local_multi_push_replication {
    my ( $self, $url, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    my $call_url = $self->_api_url() . "/replications/$repository?url=$url";
    return $self->delete($call_url);
}

=head2 enable_or_disable_multiple_replications( 'enable|disable', include => [ ], exclude => [ ] )

Enables/disables multiple replication tasks by repository or Artifactory server based in include and exclude patterns.

=cut

sub enable_or_disable_multiple_replications {
    my ( $self, $flag, %args ) = @_;
    my $url = $self->_api_url() . "/replications/$flag";
    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 get_global_system_replication_configuration

Returns the global system replication configuration status, i.e. if push and pull replications are blocked or unblocked.

=cut

sub get_global_system_replication_configuration {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/replications";
    return $self->get($url);
}

=head2 get_remote_repositories_registered_for_replication

Returns a list of all the listeners subscribed for event-based pull replication on the specified repository.

=cut

sub get_remote_repositories_registered_for_replication {
    my ( $self, $repo ) = @_;
    my $repository = $repo || $self->repository();

    my $url  = $self->_api_url() . "/replications/$repository";
    return $self->get($url);
}

=head2 block_system_replication( push => 'false', pull => 'true' )

Blocks replications globally. Push and pull are true by default. If false, replication for the corresponding type is not
blocked.

=cut

sub block_system_replication {
    my ( $self, %args ) = @_;
    return $self->_handle_block_system_replication( 'block', %args );
}

=head2 unblock_system_replication( push => 'false', pull => 'true' )

Unblocks replications globally. Push and pull are true by default. If false, replication for the corresponding type is
not unblocked.

=cut

sub unblock_system_replication {
    my ( $self, %args ) = @_;
    return $self->_handle_block_system_replication( 'unblock', %args );
}

=head2 artifact_sync_download( $path, content => 'progress', mark => 1000 )

Downloads an artifact with or without returning the actual content to the client. When tracking the progress marks are
printed (by default every 1024 bytes). This is extremely useful if you want to trigger downloads on a remote Artifactory
server, for example to force eager cache population of large artifacts, but want to avoid the bandwidth consumption
involved in transferring the artifacts to the triggering client. If no content parameter is specified the file content
is downloaded to the client.

=cut

sub artifact_sync_download {
    my ( $self, $path, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/download/$repository" . $path;
    $url .= "?" . $self->_stringify_hash( '&', %args ) if (%args);
    return $self->get($url);
}

=head2 file_list( $dir, %opts )

Get a flat (the default) or deep listing of the files and folders (not included by default) within a folder

=cut

sub file_list {
    my ( $self, $dir, %opts ) = @_;
    $dir = $self->_merge_repo_and_path($dir);
    my $url = $self->_api_url() . "/storage/$dir?list";

    for my $opt ( keys %opts ) {
        my $val = $opts{$opt};
        $url .= "&${opt}=$val";
    }
    return $self->get($url);
}

=head2 get_background_tasks

Retrieves list of background tasks currently scheduled or running in Artifactory. In HA, the nodeId is added to each
task. Task can be in one of few states: scheduled, running, stopped, canceled. Running task also shows the task start
time.

=cut

sub get_background_tasks {
    my $self = shift;
    my $url  = $self->_api_url() . "/tasks";
    return $self->get($url);
}

=head2 empty_trash_can

Empties the trash can permanently deleting all its current contents.

=cut

sub empty_trash_can {
    my $self = shift;
    my $url  = $self->_api_url() . "/trash/empty";
    return $self->post($url);
}

=head2 delete_item_from_trash_can($path)

Permanently deletes an item from the trash can.

=cut

sub delete_item_from_trash_can {
    my ( $self, $path ) = @_;
    my $url = $self->_api_url() . "/trash/$path";
    return $self->delete($url);
}

=head2 restore_item_from_trash_can( $from, $to )

Restore an item from the trash can.

=cut

sub restore_item_from_trash_can {
    my ( $self, $from, $to ) = @_;
    my $url = $self->_api_url() . "/trash/restore/$from?to=$to";
    return $self->post($url);
}

=head2 optimize_system_storage

Raises a flag to invoke balancing between redundant storage units of a sharded filestore following the next garbage
collection.

=cut

sub optimize_system_storage {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/storage/optimize";
    return $self->post($url);
}

=head1 SEARCHES

=cut

=head2 artifactory_query_language( $aql_statement )

Flexible and high performance search using Artifactory Query Language (AQL).

=cut

sub artifactory_query_language {
    my ( $self, $aql ) = @_;

    my $url = $self->_api_url() . "/search/aql";
    return $self->post(
        $url,
        "Content-Type" => 'text/plain',
        Content        => $aql
    );
}

=head2 artifact_search( name => $name, repos => [ @repos ], result_detail => [qw(info properties)], )

Artifact search by part of file name

=cut

sub artifact_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search( 'artifact', %args );
}

=head2 archive_entry_search( name => $name, repos => [ @repos ] )

Search archive entries for classes or any other jar resources

=cut

sub archive_entry_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search( 'archive', %args );
}

=head2 gavc_search( g => 'foo', c => 'bar', result_detail => [qw(info properties)], )

Search by Maven coordinates: groupId, artifactId, version & classifier

=cut

sub gavc_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'gavc', %args );
}

=head2 property_search( p => [ 'v1', 'v2' ], repos => [ 'repo1', 'repo2' ], result_detail => [qw(info properties)], )

Search by properties

=cut

sub property_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'prop', %args );
}

=head2 checksum_search( md5 => '12345', repos => [ 'repo1', 'repo2' ], result_detail => [qw(info properties)], )

Artifact search by checksum (md5 or sha1)

=cut

sub checksum_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'checksum', %args );
}

=head2 bad_checksum_search( type => 'md5', repos => [ 'repo1', 'repo2' ]  )

Find all artifacts that have a bad or missing client checksum values (md5 or
sha1)

=cut

sub bad_checksum_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'badChecksum', %args );
}

=head2 artifacts_not_downloaded_since( notUsedSince => 12345, createdBefore => 12345, repos => [ 'repo1', 'repo2' ] )

Retrieve all artifacts not downloaded since the specified Java epoch in msec.

=cut

sub artifacts_not_downloaded_since {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'usage', %args );
}

=head2 artifacts_with_date_in_date_range( from => 12345, repos => [ 'repo1', 'repo2' ], dateFields => [ 'created' ] )

Get all artifacts with specified dates within the given range. Search can be limited to specific repositories (local or
caches).

=cut

sub artifacts_with_date_in_date_range {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'dates', %args );
}

=head2 artifacts_created_in_date_range( from => 12345, to => 12345, repos => [ 'repo1', 'repo2' ] )

Get all artifacts created in date range

=cut

sub artifacts_created_in_date_range {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'creation', %args );
}

=head2 pattern_search( $pattern )

Get all artifacts matching the given Ant path pattern

=cut

sub pattern_search {
    my ( $self, $pattern, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/search/pattern?pattern=$repository:$pattern";
    return $self->get($url);
}

=head2 builds_for_dependency( sha1 => 'abcde' )

Find all the builds an artifact is a dependency of (where the artifact is included in the build-info dependencies)

=cut

sub builds_for_dependency {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'dependency', %args );
}

=head2 license_search( unapproved => 1, unknown => 1, notfound => 0, neutral => 0, repos => [ 'foo', 'bar' ] )

Search for artifacts with specified statuses

=cut

sub license_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'license', %args );
}

=head2 artifact_version_search( g => 'foo', a => 'bar', v => '1.0', repos => [ 'foo', 'bar' ] )

Search for all available artifact versions by GroupId and ArtifactId in local, remote or virtual repositories

=cut

sub artifact_version_search {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'versions', %args );
}

=head2 artifact_latest_version_search_based_on_layout( g => 'foo', a => 'bar', v => '1.0', repos => [ 'foo', 'bar' ] )

Search for the latest artifact version by groupId and artifactId, based on the layout defined in the repository

=cut

sub artifact_latest_version_search_based_on_layout {
    my ( $self, %args ) = @_;
    return $self->_handle_search_props( 'latestVersion', %args );
}

=head2 artifact_latest_version_search_based_on_properties( repo => '_any', path => '/a/b', listFiles => 1 )

Search for artifacts with the latest value in the "version" property

=cut

sub artifact_latest_version_search_based_on_properties {
    my ( $self, %args ) = @_;
    my $repo = delete $args{repo};
    my $path = delete $args{path};

    $repo =~ s{^\/}{}xi;
    $repo =~ s{\/$}{}xi;

    $path =~ s{^\/}{}xi;
    $path =~ s{\/$}{}xi;

    my $url = $self->_api_url() . "/versions/$repo/$path?";
    $url .= $self->_stringify_hash( '&', %args );
    return $self->get($url);
}

=head2 build_artifacts_search( buildNumber => 15, buildName => 'foobar' )

Find all the artifacts related to a specific build

=cut

sub build_artifacts_search {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . "/search/buildArtifacts";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%args )
    );
}

=head2 list_docker_repositories( n => 5, last => 'last_tag_value' )

Lists all Docker repositories hosted in under an Artifactory Docker repository.

=cut

sub list_docker_repositories {
    my ( $self, %args ) = @_;
    my $repository = delete $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/docker/$repository/v2/_catalog";
    $url .= '?' . $self->_stringify_hash( '&', %args ) if (%args);

    return $self->get($url);
}

=head2 list_docker_tags( $image_name, n => 5, last => 'last_tag_value' )

Lists all tags of the specified Artifactory Docker repository.

=cut

sub list_docker_tags {
    my ( $self, $image_name, %args ) = @_;
    my $repository = delete $args{repository} || $self->repository();
    my $url = $self->_api_url() . "/docker/$repository/v2/$image_name/tags/list";
    $url .= '?' . $self->_stringify_hash( '&', %args ) if (%args);

    return $self->get($url);
}

=head1 SECURITY

=cut

=head2 get_users

Get the users list

=cut

sub get_users {
    my $self = shift;
    return $self->_handle_security( undef, 'get', 'users' );
}

=head2 get_user_details( $user )

Get the details of an Artifactory user

=cut

sub get_user_details {
    my ( $self, $user ) = @_;
    return $self->_handle_security( $user, 'get', 'users' );
}

=head2 get_user_encrypted_password

Get the encrypted password of the authenticated requestor

=cut

sub get_user_encrypted_password {
    my $self = shift;
    return $self->_handle_security( undef, 'get', 'encryptedPassword' );
}

=head2 create_or_replace_user( $user, %args )

Creates a new user in Artifactory or replaces an existing user

=cut

sub create_or_replace_user {
    my ( $self, $user, %args ) = @_;
    return $self->_handle_security( $user, 'put', 'users', %args );
}

=head2 update_user( $user, %args )

Updates an exiting user in Artifactory with the provided user details

=cut

sub update_user {
    my ( $self, $user, %args ) = @_;
    return $self->_handle_security( $user, 'post', 'users', %args );
}

=head2 delete_user( $user )

Removes an Artifactory user

=cut

sub delete_user {
    my ( $self, $user ) = @_;
    return $self->_handle_security( $user, 'delete', 'users' );
}

=head2 expire_password_for_a_single_user( $user )

Expires a user's password

=cut

sub expire_password_for_a_single_user {
    my ( $self, $user ) = @_;
    my $url = $self->_api_url() . "/security/users/authorization/expirePassword/$user";
    return $self->post($url);
}

=head2 expire_password_for_multiple_users( $user1, $user2 )

Expires password for a list of users

=cut

sub expire_password_for_multiple_users {
    my ( $self, @users ) = @_;
    my $url = $self->_api_url() . "/security/users/authorization/expirePassword";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( [@users] )
    );
}

=head2 expire_password_for_all_users

Expires password for all users

=cut

sub expire_password_for_all_users {
    my ( $self, @users ) = @_;
    my $url = $self->_api_url() . "/security/users/authorization/expirePasswordForAllUsers";
    return $self->post($url);
}

=head2 unexpire_password_for_a_single_user( $user )

Unexpires a user's password

=cut

sub unexpire_password_for_a_single_user {
    my ( $self, $user ) = @_;
    my $url = $self->_api_url() . "/security/users/authorization/unexpirePassword/$user";
    return $self->post($url);
}

=head2 change_password( user => 'david', oldPassword => 'foo', newPassword => 'bar' )

Changes a user's password

=cut

sub change_password {
    my ( $self, %info ) = @_;
    my $url         = $self->_api_url() . "/security/users/authorization/changePassword";
    my $newpassword = delete $info{newPassword};
    $info{newPassword1} = $newpassword;
    $info{newPassword2} = $newpassword;    # API requires new passwords twice, once for verification

    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%info )
    );
}

=head2 get_password_expiration_policy

Retrieves the password expiration policy

=cut

sub get_password_expiration_policy {
    my $self = shift;
    my $url  = $self->_api_url() . "/security/configuration/passwordExpirationPolicy";
    return $self->get($url);
}

=head2 set_password_expiration_policy

Sets the password expiration policy

=cut

sub set_password_expiration_policy {
    my ( $self, %info ) = @_;
    my $url = $self->_api_url() . "/security/configuration/passwordExpirationPolicy";
    return $self->put(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%info )
    );
}

=head2 configure_user_lock_policy( enabled => 'true|false', loginAttempts => $num )

Configures the user lock policy that locks users out of their account if the number of repeated incorrect login attempts
exceeds the configured maximum allowed.

=cut

sub configure_user_lock_policy {
    my ( $self, %info ) = @_;
    my $url = $self->_api_url() . "/security/userLockPolicy";
    return $self->put(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%info )
    );
}

=head2 retrieve_user_lock_policy

Retrieves the currently configured user lock policy.

=cut

sub retrieve_user_lock_policy {
    my $self = shift;
    my $url  = $self->_api_url() . "/security/userLockPolicy";
    return $self->get($url);
}

=head2 get_locked_out_users

If locking out users is enabled, lists all users that were locked out due to recurrent incorrect login attempts.

=cut

sub get_locked_out_users {
    my $self = shift;
    my $url  = $self->_api_url() . "/security/lockedUsers";
    return $self->get($url);
}

=head2 unlock_locked_out_user

Unlocks a single user that was locked out due to recurrent incorrect login attempts.

=cut

sub unlock_locked_out_user {
    my ( $self, $name ) = @_;
    my $url = $self->_api_url() . "/security/unlockUsers/$name";
    return $self->post($url);
}

=head2 unlock_locked_out_users

Unlocks a list of users that were locked out due to recurrent incorrect login attempts.

=cut

sub unlock_locked_out_users {
    my ( $self, @users ) = @_;
    my $url = $self->_api_url() . "/security/unlockUsers";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \@users )
    );
}

=head2 unlock_all_locked_out_users

Unlocks all users that were locked out due to recurrent incorrect login attempts.

=cut

sub unlock_all_locked_out_users {
    my $self = shift;
    my $url  = $self->_api_url() . "/security/unlockAllUsers";
    return $self->post($url);
}

=head2 create_api_key( apiKey => '3OloposOtVFyCMrT+cXmCAScmVMPrSYXkWIjiyDCXsY=' )

Create an API key for the current user

=cut

sub create_api_key {
    my ( $self, %args ) = @_;
    return $self->_handle_api_key( 'post', %args );
}

=head2 get_api_key

Get the current user's own API key

=cut

sub get_api_key {
    my $self = shift;
    return $self->_handle_api_key('get');
}

=head2 revoke_api_key

Revokes the current user's API key

=cut

sub revoke_api_key {
    my $self = shift;
    return $self->_handle_revoke_api_key('/apiKey/auth');
}

=head2 revoke_user_api_key

Revokes the API key of another user

=cut

sub revoke_user_api_key {
    my ( $self, $user ) = @_;
    return $self->_handle_revoke_api_key("/apiKey/auth/$user");
}

=head2 revoke_all_api_keys

Revokes all API keys currently defined in the system

=cut

sub revoke_all_api_keys {
    my ( $self, %args ) = @_;
    my $deleteall = ( defined $args{deleteAll} ) ? $args{deleteAll} : 1;
    return $self->_handle_revoke_api_key("/apiKey?deleteAll=$deleteall");
}

=head2 get_groups

Get the groups list

=cut

sub get_groups {
    my $self = shift;
    return $self->_handle_security( undef, 'get', 'groups' );
}

=head2 get_group_details( $group )

Get the details of an Artifactory Group

=cut

sub get_group_details {
    my ( $self, $group ) = @_;
    return $self->_handle_security( $group, 'get', 'groups' );
}

=head2 create_or_replace_group( $group, %args )

Creates a new group in Artifactory or replaces an existing group

=cut

sub create_or_replace_group {
    my ( $self, $group, %args ) = @_;
    return $self->_handle_security( $group, 'put', 'groups', %args );
}

=head2 update_group( $group, %args )

Updates an exiting group in Artifactory with the provided group details

=cut

sub update_group {
    my ( $self, $group, %args ) = @_;
    return $self->_handle_security( $group, 'post', 'groups', %args );
}

=head2 delete_group( $group )

Removes an Artifactory group

=cut

sub delete_group {
    my ( $self, $group ) = @_;
    return $self->_handle_security( $group, 'delete', 'groups' );
}

=head2 get_permission_targets

Get the permission targets list

=cut

sub get_permission_targets {
    my $self = shift;
    return $self->_handle_security( undef, 'get', 'permissions' );
}

=head2 get_permission_target_details( $name )

Get the details of an Artifactory Permission Target

=cut

sub get_permission_target_details {
    my ( $self, $name ) = @_;
    return $self->_handle_security( $name, 'get', 'permissions' );
}

=head2 create_or_replace_permission_target( $name, %args )

Creates a new permission target in Artifactory or replaces an existing permission target

=cut

sub create_or_replace_permission_target {
    my ( $self, $name, %args ) = @_;
    return $self->_handle_security( $name, 'put', 'permissions', %args );
}

=head2 delete_permission_target( $name )

Deletes an Artifactory permission target

=cut

sub delete_permission_target {
    my ( $self, $name ) = @_;
    return $self->_handle_security( $name, 'delete', 'permissions' );
}

=head2 effective_item_permissions( $path )

Returns a list of effective permissions for the specified item (file or folder)

=cut

sub effective_item_permissions {
    my ( $self, $arg ) = @_;

    my $path = $self->_merge_repo_and_path($arg);
    my $url  = $self->_api_url() . "/storage/$path?permissions";
    return $self->get($url);
}

=head2 security_configuration

Retrieve the security configuration (security.xml)

=cut

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

    my $url = $self->_api_url() . "/system/security";
    return $self->get($url);
}

=head2 activate_master_key_encryption

Creates a new master key and activates master key encryption

=cut

sub activate_master_key_encryption {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/encrypt";
    return $self->post($url);
}

=head2 deactivate_master_key_encryption

Removes the current master key and deactivates master key encryption

=cut

sub deactivate_master_key_encryption {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/decrypt";
    return $self->post($url);
}

=head2 set_gpg_public_key( key => $string )

Sets the public key that Artifactory provides to Debian clients to verify packages

=cut

sub set_gpg_public_key {
    my ( $self, %args ) = @_;
    my $key = $args{key};
    return $self->_handle_gpg_key( 'public', 'put', content => $key );
}

=head2 get_gpg_public_key

Gets the public key that Artifactory provides to Debian clients to verify packages

=cut

sub get_gpg_public_key {
    my $self = shift;
    return $self->_handle_gpg_key( 'public', 'get' );
}

=head2 set_gpg_private_key( key => $string )

Sets the private key that Artifactory will use to sign Debian packages

=cut

sub set_gpg_private_key {
    my ( $self, %args ) = @_;
    my $key = $args{key};
    return $self->_handle_gpg_key( 'private', 'put', content => $key );
}

=head2 set_gpg_pass_phrase( $passphrase )

Sets the pass phrase required signing Debian packages using the private key

=cut

sub set_gpg_pass_phrase {
    my ( $self, $pass ) = @_;
    return $self->_handle_gpg_key( 'passphrase', 'put', 'X-GPG-PASSPHRASE' => $pass );
}

=head2 create_token( username => 'johnq', scope => 'member-of-groups:readers' )

Creates an access token

=cut

sub create_token {
    my ( $self, %data ) = @_;
    my $url = $self->_api_url() . "/security/token";
    return $self->post( $url, content => \%data );
}

=head2 refresh_token( grant_type => 'refresh_token', refresh_token => 'fgsg53t3g' )

Refresh an access token to extend its validity. If only the access token and the refresh token are provided (and no
other parameters), this pair is used for authentication. If username or any other parameter is provided, then the
request must be authenticated by a token that grants admin permissions.

=cut

sub refresh_token {
    my ( $self, %data ) = @_;
    return $self->create_token(%data);
}

=head2 revoke_token( token => 'fgsg53t3g' )

Revoke an access token

=cut

sub revoke_token {
    my ( $self, %data ) = @_;
    my $url = $self->_api_url() . "/security/token/revoke";
    return $self->post( $url, content => \%data );
}

=head2 get_service_id

Provides the service ID of an Artifactory instance or cluster

=cut

sub get_service_id {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/service_id";
    return $self->get($url);
}

=head2 get_certificates

Returns a list of installed SSL certificates.

=cut

sub get_certificates {
    my $self = shift;
    my $url  = $self->_api_url() . "/system/security/certificates";
    return $self->get($url);
}

=head2 add_certificate( $alias, $file_path )

Adds an SSL certificate.

=cut

sub add_certificate {
    my ( $self, $alias, $file ) = @_;
    my $url  = $self->_api_url() . "/system/security/certificates/$alias";
    my $data = Path::Tiny::path($file)->slurp();
    return $self->post( $url, 'Content-Type' => 'application/text', content => $data );
}

=head2 delete_certificate( $alias )

Deletes an SSL certificate.

=cut

sub delete_certificate {
    my ( $self, $alias ) = @_;
    my $url = $self->_api_url() . "/system/security/certificates/$alias";
    return $self->delete($url);
}

=head1 REPOSITORIES

=cut

=head2 get_repositories( $type )

Returns a list of minimal repository details for all repositories of the specified type

=cut

sub get_repositories {
    my ( $self, $type ) = @_;

    my $url = $self->_api_url() . "/repositories";
    $url .= "?type=$type" if ($type);

    return $self->get($url);
}

=head2 repository_configuration( $name, %args )

Retrieves the current configuration of a repository

=cut

sub repository_configuration {
    my ( $self, $repo, %args ) = @_;

    $repo =~ s{^\/}{}xi;
    $repo =~ s{\/$}{}xi;

    my $url =
      (%args)
      ? $self->_api_url() . "/repositories/$repo?"
      : $self->_api_url() . "/repositories/$repo";
    $url .= $self->_stringify_hash( '&', %args ) if (%args);
    return $self->get($url);
}

=head2 create_or_replace_repository_configuration( $name, \%payload, %args )

Creates a new repository in Artifactory with the provided configuration or replaces the configuration of an existing
repository

=cut

sub create_or_replace_repository_configuration {
    my ( $self, $repo, $payload, %args ) = @_;
    return $self->_handle_repositories( $repo, $payload, 'put', %args );
}

=head2 update_repository_configuration( $name, \%payload )

Updates an exiting repository configuration in Artifactory with the provided configuration elements

=cut

sub update_repository_configuration {
    my ( $self, $repo, $payload ) = @_;
    return $self->_handle_repositories( $repo, $payload, 'post' );
}

=head2 delete_repository( $name )

Removes a repository configuration together with the whole repository content

=cut

sub delete_repository {
    my ( $self, $repo ) = @_;
    return $self->_handle_repositories( $repo, undef, 'delete' );
}

=head2 calculate_yum_repository_metadata( async => 0/1 )

Calculates/recalculates the YUM metdata for this repository, based on the RPM package currently hosted in the repository

=cut

sub calculate_yum_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex( "/yum/$repository", %args );
}

=head2 calculate_nuget_repository_metadata

Recalculates all the NuGet packages for this repository (local/cache/virtual), and re-annotate the NuGet properties for
each NuGet package according to it's internal nuspec file

=cut

sub calculate_nuget_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/nuget/$repository/reindex");
}

=head2 calculate_npm_repository_metadata

Recalculates the npm search index for this repository (local/virtual). Please see the Npm integration documentation for
more details.

=cut

sub calculate_npm_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/npm/$repository/reindex");
}

=head2 calculate_maven_index( repos => [ 'repo1', 'repo2' ], force => 0/1 )

Calculates/caches a Maven index for the specified repositories

=cut

sub calculate_maven_index {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . "/maven?";
    $url .= $self->_stringify_hash( '&', %args );
    return $self->post($url);
}

=head2 calculate_maven_metadata( $path )

Calculates Maven metadata on the specified path (local repositories only)

=cut

sub calculate_maven_metadata {
    my ( $self, $path ) = @_;
    $path = $self->_merge_repo_and_path($path);
    my $url = $self->_api_url() . "/maven/calculateMetadata/$path";
    return $self->post($url);
}

=head2 calculate_debian_repository_metadata( async => 0/1 )

Calculates/recalculates the Packages and Release metadata for this repository,based on the Debian packages in it.
Calculation can be synchronous (the default) or asynchronous.

=cut

sub calculate_debian_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex( "/deb/reindex/$repository", %args );
}

=head2 calculate_cached_remote_debian_repository_coordinates( 'repokey' )

Calculates/recalculates the Debian packages coordinates

=cut

sub calculate_cached_remote_debian_repository_coordinates {
    my ( $self, $repo_key ) = @_;
    my $repository = $repo_key || $self->repository();
    my $url = $self->_api_url() . "/deb/indexCached/$repository";
    return $self->post($url);
}

=head2 calculate_opkg_repository_metadata( async => 0/1, writeProps => 1 )

Calculates/recalculates the Packages and Release metadata for this repository,based on the ipk packages in it (in each
feed location).

=cut

sub calculate_opkg_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex( "/opkg/reindex/$repository", %args );
}

=head2 calculate_bower_index

Recalculates the index for a Bower repository.

=cut

sub calculate_bower_index {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/bower/$repository/reindex");
}

=head2 calculate_helm_chart_index

Calculates Helm chart index on the specified path (local repositories only).

=cut

sub calculate_helm_chart_index {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/helm/$repository/reindex");
}

=head2 calculate_cran_repository_metadata

Calculates/recalculates the Packages and Release metadata for this repository, based on the CRAN packages in it.

=cut

sub calculate_cran_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/cran/reindex/$repository", %args);
}

=head2 calculate_conda_repository_metadata

Calculates/recalculates the Conda packages and release metadata for this repository.

=cut

sub calculate_conda_repository_metadata {
    my ( $self, %args ) = @_;
    my $repository = $args{repository} || $self->repository();
    return $self->_handle_repository_reindex("/conda/reindex/$repository", %args);
}

=head1 SYSTEM & CONFIGURATION

=cut

=head2 system_info

Get general system information

=cut

sub system_info {
    my $self = shift;
    return $self->_handle_system();
}

=head2 verify_connection( endpoint => 'http://server/foobar', username => 'admin', password => 'password' )

Verifies a two-way connection between Artifactory and another product

=cut

sub verify_connection {
    my ( $self, %args ) = @_;
    my $url = $self->_api_url() . "/system/verifyconnection";

    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%args )
    );
}

=head2 system_health_ping

Get a simple status response about the state of Artifactory

=cut

sub system_health_ping {
    my $self = shift;
    return $self->_handle_system('ping');
}

=head2 general_configuration

Get the general configuration (artifactory.config.xml)

=cut

sub general_configuration {
    my $self = shift;
    return $self->_handle_system('configuration');
}

=head2 save_general_configuration( $file )

Save the general configuration (artifactory.config.xml)

=cut

sub save_general_configuration {
    my ( $self, $xml ) = @_;

    my $file = Path::Tiny::path($xml)->slurp( { binmode => ":raw" } );
    my $url = $self->_api_url() . "/system/configuration";
    return $self->post(
        $url,
        'Content-Type' => 'application/xml',
        content        => $file
    );
}

=head2 update_custom_url_base( $url )

Changes the Custom URL base

=cut

sub update_custom_url_base {
    my ( $self, $base ) = @_;
    my $url = $self->_api_url() . '/system/configuration/baseUrl';
    return $self->put(
        $url,
        'Content-Type' => 'text/plain',
        content        => $base
    );
}

=head2 license_information

Retrieve information about the currently installed license

=cut

sub license_information {
    my $self = shift;

    my $url = $self->_api_url() . "/system/license";
    return $self->get($url);
}

=head2 install_license( $licensekey )

Install new license key or change the current one

=cut

sub install_license {
    my ( $self, $key ) = @_;
    my $url = $self->_api_url() . "/system/license";

    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( { licenseKey => $key } )
    );
}

=head2 ha_license_information

Retrieve information about the currently installed licenses in an HA cluster

=cut

sub ha_license_information {
    my $self = shift;

    my $url = $self->_api_url() . "/system/licenses";
    return $self->get($url);
}

=head2 install_ha_cluster_licenses( [ { licenseKey => 'foobar' }, { licenseKey => 'barbaz' } ] )

Install a new license key(s) on an HA cluster

=cut

sub install_ha_cluster_licenses {
    my ( $self, $ref ) = @_;
    my $url = $self->_api_url() . "/system/licenses";

    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode($ref)
    );
}

=head2 delete_ha_cluster_license( 'licenseHash1', 'licenseHash2' )

Deletes a license key from an HA cluster

=cut

sub delete_ha_cluster_license {
    my ( $self, @licenses ) = @_;
    my $url = $self->_api_url() . "/system/licenses?";
    $url .= $self->_handle_non_matrix_props( 'licenseHash', \@licenses );
    return $self->delete( $url, 'Content-Type' => 'application/json' );
}

=head2 version_and_addons_information

Retrieve information about the current Artifactory version, revision, and currently installed Add-ons

=cut

sub version_and_addons_information {
    my $self = shift;

    my $url = $self->_api_url() . "/system/version";
    return $self->get($url);
}

=head2 get_reverse_proxy_configuration

Retrieves the reverse proxy configuration

=cut

sub get_reverse_proxy_configuration {
    my $self = shift;

    my $url = $self->_api_url() . "/system/configuration/webServer";
    return $self->get($url);
}

=head2 update_reverse_proxy_configuration(%data)

Updates the reverse proxy configuration

=cut

sub update_reverse_proxy_configuration {
    my ( $self, %data ) = @_;

    my $url = $self->_api_url() . "/system/configuration/webServer";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%data )
    );
}

=head2 get_reverse_proxy_snippet

Gets the reverse proxy configuration snippet in text format

=cut

sub get_reverse_proxy_snippet {
    my $self = shift;

    my $url = $self->_api_url() . "/system/configuration/reverseProxy/nginx";
    return $self->get($url);
}

=head2 start_sha256_migration_task( "batchThreshold" => 10, etc etc )

Starts the SHA-256 migration process.

=cut

sub start_sha256_migration_task {
    my ( $self, %data ) = @_;

    my $url = $self->_api_url() . "/system/migration/sha2/start";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%data )
    );
}

=head2 stop_sha256_migration_task( "sleepIntervalMillis" => 5000, etc etc )

Stops the SHA-256 migration process

=cut

sub stop_sha256_migration_task {
    my ( $self, %data ) = @_;

    my $url = $self->_api_url() . "/system/migration/sha2/stop";
    return $self->post(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%data )
    );
}

=head1 PLUGINS

=cut

=head2 execute_plugin_code( $execution_name, $params, $async )

Executes a named execution closure found in the executions section of a user plugin

=cut

sub execute_plugin_code {
    my ( $self, $execution_name, $params, $async ) = @_;

    my $url =
      ($params)
      ? $self->_api_url() . "/plugins/execute/$execution_name?params="
      : $self->_api_url() . "/plugins/execute/$execution_name";

    $url = $url . $self->_attach_properties( properties => $params );
    $url .= "&" . $self->_stringify_hash( '&', %{$async} ) if ($async);
    return $self->post($url);
}

=head2 retrieve_all_available_plugin_info

Retrieves all available user plugin information (subject to the permissions of the provided credentials)

=cut

sub retrieve_all_available_plugin_info {
    my $self = shift;
    return $self->_handle_plugins();
}

=head2 retrieve_plugin_info_of_a_certain_type( $type )

Retrieves all available user plugin information (subject to the permissions of the provided credentials) of the
specified type

=cut

sub retrieve_plugin_info_of_a_certain_type {
    my ( $self, $type ) = @_;
    return $self->_handle_plugins($type);
}

=head2 retrieve_build_staging_strategy( strategyName => 'strategy1', buildName => 'build1', %args )

Retrieves a build staging strategy defined by a user plugin

=cut

sub retrieve_build_staging_strategy {
    my ( $self, %args ) = @_;
    my $strategy_name = delete $args{strategyName};
    my $build_name    = delete $args{buildName};

    my $url = $self->_api_url() . "/plugins/build/staging/$strategy_name?buildName=$build_name&params=";
    $url = $url . $self->_attach_properties( properties => \%args );
    return $self->get($url);
}

=head2 execute_build_promotion( promotionName => 'promotion1', buildName => 'build1', buildNumber => 3, %args )

Executes a named promotion closure found in the promotions section of a user plugin

=cut

sub execute_build_promotion {
    my ( $self, %args ) = @_;
    my $promotion_name = delete $args{promotionName};
    my $build_name     = delete $args{buildName};
    my $build_number   = delete $args{buildNumber};

    my $url = $self->_api_url() . "/plugins/build/promote/$promotion_name/$build_name/$build_number?params=";
    $url = $url . $self->_attach_properties( properties => \%args );
    return $self->post($url);
}

=head2 reload_plugins

Reloads user plugins if there are modifications since the last user plugins reload. Works regardless of the automatic
user plugins refresh interval

=cut

sub reload_plugins {
    my $self = shift;
    my $url  = $self->_api_url() . '/plugins/reload';
    return $self->post($url);
}

=head1 IMPORT & EXPORT

=cut

=head2 import_repository_content( path => 'foobar', repo => 'repo', metadata => 1, verbose => 0 )

Import one or more repositories

=cut

sub import_repository_content {
    my ( $self, %args ) = @_;

    my $url = $self->_api_url() . "/import/repositories?";
    $url .= $self->_stringify_hash( '&', %args );
    return $self->post($url);
}

=head2 import_system_settings_example

Returned default Import Settings JSON

=cut

sub import_system_settings_example {
    my $self = shift;
    return $self->_handle_system_settings('import');
}

=head2 full_system_import( importPath => '/import/path', includeMetadata => 'false' etc )

Import full system from a server local Artifactory export directory

=cut

sub full_system_import {
    my ( $self, %args ) = @_;
    return $self->_handle_system_settings( 'import', %args );
}

=head2 export_system_settings_example

Returned default Export Settings JSON

=cut

sub export_system_settings_example {
    my $self = shift;
    return $self->_handle_system_settings('export');
}

=head2 export_system( exportPath => '/export/path', includeMetadata => 'true' etc )

Export full system to a server local directory

=cut

sub export_system {
    my ( $self, %args ) = @_;
    return $self->_handle_system_settings( 'export', %args );
}

=head2 ignore_xray_alert( $path )

Sets an alert to be ignored until next time the repository hosting the artifact about which the alert was issued, is scanned. Note that this endpoint does not
affect artifacts that are blocked because they have not been scanned at all.

=cut

sub ignore_xray_alert {
    my ( $self, $path ) = @_;
    my $url = $self->_api_url() . "/xray/setAlertIgnored?path=$path";
    return $self->post($url);
}

=head2 allow_download_of_blocked_artifacts( 'true'|'false' )

When a repository is configured to block downloads of artifacts, you may override that configuration (and allow download of blocked artifacts). Note that this
setting cannot override the blocking of unscanned artifacts.

=cut

sub allow_download_of_blocked_artifacts {
    my ( $self, $bool ) = @_;
    my $url = $self->_api_url() . "/xray/allowBlockedArtifactsDownload?allow=$bool";
    return $self->post($url);
}

=head2 allow_download_when_xray_is_unavailable( 'true'|'false' )

You may configure Artifactory to block downloads of artifacts when the connected Xray instance is unavailable. This endpoint lets you override that
configuration (and allow download of artifacts).

=cut

sub allow_download_when_xray_is_unavailable {
    my ( $self, $bool ) = @_;
    my $url = $self->_api_url() . "/xray/allowDownloadWhenUnavailable?allow=$bool";
    return $self->post($url);
}

=head2 create_bundle( %hash of data structure )

Create a new support bundle

=cut

sub create_bundle {
    my ( $self, %args ) = @_;
    my $url = $self->_api_url() . '/support/bundles';
    %args = () unless %args;

    return $self->post(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode( \%args )
    );
}

=head2 list_bundles

Lists previously created bundle currently stored in the system

=cut

sub list_bundles {
    my $self = shift;
    my $url  = $self->_api_url() . '/support/bundles';
    return $self->get( $url, "Content-Type" => 'application/json', );
}

=head2 get_bundle_metadata( $name )

Downloads a previously created bundle currently stored in the system

=cut

sub get_bundle_metadata {
    my ( $self, $bundle ) = @_;
    my $url = $self->_api_url() . '/support/bundles/' . $bundle;
    return $self->get( $url, "Content-Type" => 'application/json', );
}

=head2 get_bundle( $name )

Downloads a previously created bundle currently stored in the system

=cut

sub get_bundle {
    my ( $self, $bundle ) = @_;
    my $url = $self->_api_url() . '/support/bundles/' . $bundle . '/archive';
    return $self->get( $url, "Content-Type" => 'application/json', );
}

=head2 delete_bundle( $name )

Deletes a previously created bundle from the system.

=cut

sub delete_bundle {
    my ( $self, $bundle ) = @_;
    my $url = $self->_api_url() . '/support/bundles/' . $bundle;
    return $self->delete( $url, "Content-Type" => 'application/json', );
}

sub _build_ua {
    my $self = shift;
    return LWP::UserAgent->new( agent => 'perl-artifactory-client/' . $VERSION, );
}

sub _build_json {
    my ($self) = @_;
    return JSON::MaybeXS->new( utf8 => 1 );
}

sub _request {
    my ( $self, $method, @args ) = @_;
    return $self->ua->$method(@args);
}

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

    my $url = $self->_api_url() . "/build/$path";
    return $self->get($url);
}

sub _attach_properties {
    my ( $self, %args ) = @_;
    my $properties = $args{properties};
    my $matrix     = $args{matrix};
    my @strings;

    for my $key ( keys %{$properties} ) {
        push @strings, $self->_handle_prop_multivalue( $key, $properties->{$key}, $matrix );
    }

    return join( ";", @strings ) if $matrix;
    return join( "|", @strings );
}

sub _handle_prop_multivalue {
    my ( $self, $key, $values, $matrix ) = @_;

    # need to handle matrix vs non-matrix situations.
    if ($matrix) {
        return $self->_handle_matrix_props( $key, $values );
    }
    return $self->_handle_non_matrix_props( $key, $values );
}

sub _handle_matrix_props {
    my ( $self, $key, $values ) = @_;

    # string looks like key=val;key=val2;key=val3;
    my @strings;
    for my $value ( @{$values} ) {
        $value = '' if ( !defined $value );

        #$value = uri_escape( $value );
        push @strings, "$key=$value";
    }
    return join( ";", @strings );
}

sub _handle_non_matrix_props {
    my ( $self, $key, $values ) = @_;

    # string looks like key=val1,val2,val3|
    my $str = "$key=";
    my @value_holder;
    for my $value ( @{$values} ) {
        $value = '' if ( !defined $value );
        $value = uri_escape($value);
        push @value_holder, $value;
    }
    $str .= join( ",", @value_holder );
    return $str;
}

sub _handle_item {
    my ( $self, %args ) = @_;

    my ( $from, $to, $dry, $suppress_layouts, $fail_fast, $method ) =
      ( $args{from}, $args{to}, $args{dry}, $args{suppress_layouts}, $args{fail_fast}, $args{method} );

    my $url = $self->_api_url() . "/$method$from?to=$to";
    $url .= "&dry=$dry" if ( defined $dry );
    $url .= "&suppressLayouts=$suppress_layouts"
      if ( defined $suppress_layouts );
    $url .= "&failFast=$fail_fast" if ( defined $fail_fast );
    return $self->post($url);
}

sub _handle_repository_replication_configuration {
    my ( $self, $method, $payload ) = @_;
    my $repository = $self->repository();
    my $url        = $self->_api_url() . "/replications/$repository";

    return $self->$method(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode($payload),
    ) if ($payload);

    return $self->$method($url);
}

sub _handle_search {
    my ( $self, $api, %args ) = @_;
    my $name          = $args{name};
    my $repos         = $args{repos};
    my $result_detail = $args{result_detail};

    my $url = $self->_api_url() . "/search/$api?name=$name";

    if ( ref($repos) eq 'ARRAY' ) {
        $url .= "&repos=" . join( ",", @{$repos} );
    }

    my %headers;
    if ( ref($result_detail) eq 'ARRAY' ) {
        $headers{'X-Result-Detail'} = join( ',', @{$result_detail} );
    }

    return $self->get( $url, %headers );
}

sub _handle_search_props {
    my ( $self, $method, %args ) = @_;
    my $result_detail = delete $args{result_detail};

    my $url = $self->_api_url() . "/search/$method?";

    $url .= $self->_stringify_hash( '&', %args );

    my %headers;
    if ( ref($result_detail) eq 'ARRAY' ) {
        $headers{'X-Result-Detail'} = join( ',', @{$result_detail} );
    }

    return $self->get( $url, %headers );
}

sub _stringify_hash {
    my ( $self, $delimiter, %args ) = @_;

    my @strs;
    for my $key ( keys %args ) {
        my $val = $args{$key};

        if ( ref($val) eq 'ARRAY' ) {
            $val = join( ",", @{$val} );
        }
        push @strs, "$key=$val";
    }
    return join( $delimiter, @strs );
}

sub _handle_security {
    my ( $self, $label, $method, $element, %args ) = @_;

    my $url =
      ($label)
      ? $self->_api_url() . "/security/$element/$label"
      : $self->_api_url() . "/security/$element";

    if (%args) {
        return $self->$method(
            $url,
            'Content-Type' => 'application/json',
            content        => $self->_json->encode( \%args )
        );
    }
    return $self->$method($url);
}

sub _handle_repositories {
    my ( $self, $repo, $payload, $method, %args ) = @_;

    $repo =~ s{^\/}{}xi;
    $repo =~ s{\/$}{}xi;

    my $url =
      (%args)
      ? $self->_api_url() . "/repositories/$repo?"
      : $self->_api_url() . "/repositories/$repo";
    $url .= $self->_stringify_hash( '&', %args ) if (%args);

    if ($payload) {
        return $self->$method(
            $url,
            'Content-Type' => 'application/json',
            content        => $self->_json->encode($payload)
        );
    }
    return $self->$method($url);
}

sub _handle_system {
    my ( $self, $arg ) = @_;

    my $url =
      ($arg)
      ? $self->_api_url() . "/system/$arg"
      : $self->_api_url() . "/system";
    return $self->get($url);
}

sub _handle_plugins {
    my ( $self, $type ) = @_;

    my $url =
      ($type)
      ? $self->_api_url() . "/plugins/$type"
      : $self->_api_url() . "/plugins";
    return $self->get($url);
}

sub _handle_system_settings {
    my ( $self, $action, %args ) = @_;

    my $url = $self->_api_url() . "/$action/system";

    if (%args) {
        return $self->post(
            $url,
            'Content-Type' => 'application/json',
            content        => $self->_json->encode( \%args )
        );
    }
    return $self->get($url);
}

sub _handle_gpg_key {
    my ( $self, $type, $method, %args ) = @_;
    my $url = $self->_api_url() . "/gpg/key/$type";
    return $self->$method( $url, %args );
}

sub _handle_repository_reindex {
    my ( $self, $endpoint, %args ) = @_;
    my $url =
      (%args)
      ? $self->_api_url() . $endpoint . "?"
      : $self->_api_url() . $endpoint;
    $url .= $self->_stringify_hash( '&', %args ) if (%args);
    return $self->post($url);
}

sub _handle_multi_push_replication {
    my ( $self, $payload, $method ) = @_;

    my $url = $self->_api_url() . '/replications/multiple';
    return $self->$method(
        $url,
        "Content-Type" => 'application/json',
        Content        => $self->_json->encode($payload)
    );
}

sub _merge_repo_and_path {
    my ( $self, $_path ) = @_;

    $_path = '' if not defined $_path;
    $_path =~ s{^\/}{}xi;

    return join( '/', grep { $_ } $self->repository(), $_path );
}

sub _gather_delete_builds_params {
    my ( $self, $buildnumbers, $artifacts, $deleteall ) = @_;

    my @params;
    if ( ref($buildnumbers) eq 'ARRAY' ) {
        my $str = "buildNumbers=";
        $str .= join( ",", @{$buildnumbers} );
        push @params, $str;
    }
    push @params, "artifacts=$artifacts" if ( defined $artifacts );
    push @params, "deleteAll=$deleteall" if ( defined $deleteall );
    return @params;
}

sub _handle_api_key {
    my ( $self, $method, %args ) = @_;

    my $url = $self->_api_url() . "/apiKey/auth";
    return $self->$method(
        $url,
        'Content-Type' => 'application/json',
        content        => $self->_json->encode( \%args )
    );
}

sub _handle_revoke_api_key {
    my ( $self, $endpoint ) = @_;

    my $resp    = $self->get_api_key();
    my $content = $self->_json->decode( $resp->content );
    my %header;
    $header{'X-Api-Key'} = $content->{apiKey};
    my $url = $self->_api_url() . $endpoint;
    return $self->delete( $url, %header );
}

sub _handle_block_system_replication {
    my ( $self, $ep, %args ) = @_;
    my %merged = (
        push => 'true',
        pull => 'true',
        %args    # overriding defaults
    );
    my $repo = $self->repository();
    my $url = $self->_api_url() . "/system/replications/$ep?" . $self->_stringify_hash( '&', %merged );
    return $self->post($url);
}

__PACKAGE__->meta->make_immutable;

=head1 AUTHOR

Satoshi Yagi, C<< <satoshi.yagi at yahoo.com> >>

=head1 BUGS

Please report any bugs or feature requests to C<bug-artifactory-client at
rt.cpan.org>, or through the web interface at
L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Artifactory-Client>.  I will
be notified, and then you'll automatically be notified of progress on your bug
as I make changes.

=head1 SUPPORT

You can find documentation for this module with the perldoc command.

    perldoc Artifactory::Client

You can also look for information at:

=over 4

=item * RT: CPAN's request tracker (report bugs here)

L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Artifactory-Client>

=item * AnnoCPAN: Annotated CPAN documentation

L<http://annocpan.org/dist/Artifactory-Client>

=item * CPAN Ratings

L<http://cpanratings.perl.org/d/Artifactory-Client>

=item * Search CPAN

L<http://search.cpan.org/dist/Artifactory-Client/>

=back

=head1 ACKNOWLEDGEMENTS

=head1 LICENSE AND COPYRIGHT

Copyright 2014-2015, Yahoo! Inc.

This program is free software; you can redistribute it and/or modify it under
the terms of the the Artistic License (2.0). You may obtain a copy of the full
license at:

L<http://www.perlfoundation.org/artistic_license_2_0>

Any use, modification, and distribution of the Standard or Modified Versions is
governed by this Artistic License. By using, modifying or distributing the
Package, you accept this license. Do not use, modify, or distribute the
Package, if you do not accept this license.

If your Modified Version has been derived from a Modified Version made by
someone other than you, you are nevertheless required to ensure that your
Modified Version complies with the requirements of this license.

This license does not grant you the right to use any trademark, service mark,
tradename, or logo of the Copyright Holder.

This license includes the non-exclusive, worldwide, free-of-charge patent
license to make, have made, use, offer to sell, sell, import and otherwise
transfer the Package with respect to any patent claims licensable by the
Copyright Holder that are necessarily infringed by the Package. If you
institute patent litigation (including a cross-claim or counterclaim) against
any party alleging that the Package constitutes direct or contributory patent
infringement, then this Artistic License to you shall terminate on the date
that such litigation is filed.

Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND
CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED
WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR
NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL LAW.
UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING IN ANY WAY
OUT OF THE USE OF THE PACKAGE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.

=cut

1;


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.