arangamani/jenkins_api_client

View on GitHub
lib/jenkins_api_client/job.rb

Summary

Maintainability
F
2 wks
Test Coverage
#
# Copyright (c) 2012-2013 Kannan Manickam <arangamani.kannan@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#

require 'jenkins_api_client/urihelper'

module JenkinsApi
  class Client
    # This class communicates with the Jenkins "/job" API to obtain details
    # about jobs, creating, deleting, building, and various other operations.
    #
    class Job
      include JenkinsApi::UriHelper

      # Version that jenkins started to include queued build info in build response
      JENKINS_QUEUE_ID_SUPPORT_VERSION = '1.519'

      attr_reader :plugin_collection

      # Initialize the Job object and store the reference to Client object
      #
      # @param client [Client] the client object
      #
      # @return [Job] the job object
      #
      def initialize(client, *plugin_settings)
        @client = client
        @logger = @client.logger
        @plugin_collection = JenkinsApi::Client::PluginSettings::Collection.new(*plugin_settings)
      end

      # Add a plugin to be included in job's xml configureation
      #
      # @param plugin [Jenkins::Api::Client::PluginSettings::Base]
      #
      # @return [JenkinsApi::Client::PluginSettings::Collection] the job object
      def add_plugin(plugin)
        plugin_collection.add(plugin)
      end

      # Remove a plugin to be included in job's xml configureation
      #
      # @param plugin [Jenkins::Api::Client::PluginSettings::Base]
      #
      # @return [JenkinsApi::Client::PluginSettings::Collection] the job object
      def remove_plugin(plugin)
        plugin_collection.remove(plugin)
      end

      # Return a string representation of the object
      #
      def to_s
        "#<JenkinsApi::Client::Job>"
      end

      # Create or Update a job with the name specified and the xml given
      #
      # @param job_name [String] the name of the job
      # @param xml [String] the xml configuration of the job
      #
      # @see #create
      # @see #update
      #
      # @return [String] the HTTP status code from the POST request
      #
      def create_or_update(job_name, xml)
        if exists?(job_name)
          update(job_name, xml)
        else
          create(job_name, xml)
        end
      end

      # Create a job with the name specified and the xml given
      #
      # @param job_name [String] the name of the job
      # @param xml [String] the xml configuration of the job
      #
      # @see #create_or_update
      # @see #update
      #
      # @return [String] the HTTP status code from the POST request
      #
      def create(job_name, xml)
        @logger.info "Creating job '#{job_name}'"
        @client.post_config("/createItem?name=#{form_encode job_name}", xml)
      end

      # Update a job with the name specified and the xml given
      #
      # @param job_name [String] the name of the job
      # @param xml [String] the xml configuration of the job
      #
      # @see #create_or_update
      # @see #create
      #
      # @return [String] the HTTP status code from the POST request
      #
      def update(job_name, xml)
        @logger.info "Updating job '#{job_name}'"
        post_config(job_name, xml)
      end

      # Create or Update a job with params given as a hash instead of the xml
      # This gives some flexibility for creating/updating simple jobs so the
      # user doesn't have to learn about handling xml.
      #
      # @param params [Hash] parameters to create a freestyle project
      #
      # @option params [String] :name
      #   the name of the job
      # @option params [Boolean] :keep_dependencies (false)
      #   whether to keep the dependencies or not
      # @option params [Boolean] :block_build_when_downstream_building (false)
      #   whether to block build when the downstream project is building
      # @option params [Boolean] :block_build_when_upstream_building (false)
      #   whether to block build when the upstream project is building
      # @option params [Boolean] :concurrent_build (false)
      #   whether to allow concurrent execution of builds
      # @option params [String] :scm_provider
      #   the type of source control. Supported providers: git, svn, and cvs
      # @option params [String] :scm_url
      #   the remote url for the selected scm provider
      # @option params [String] :scm_credentials_id
      #   the id of the credentials to use for authenticating with scm. Only for "git"
      # @option params [String] :scm_git_tool
      #   the git executable. Defaults to "Default"; only for "git"
      # @option params [String] :scm_module
      #   the module to download. Only for use with "cvs" scm provider
      # @option params [String] :scm_branch (master)
      #   the branch to use in scm.
      # @option params [String] :scm_tag
      #   the tag to download from scm. Only for use with "cvs" scm provider
      # @option params [Boolean] :scm_use_head_if_tag_not_found
      #   whether to use head if specified tag is not found. Only for "cvs"
      # @option params [String] :timer
      #   the timer for running builds periodically
      # @option params [String] :shell_command
      #   the command to execute in the shell
      # @option params [String] :notification_email
      #   the email for sending notification
      # @option params [String] :skype_targets
      #   the skype targets for sending notifications to. Use * to specify
      #   group chats. Use space to separate multiple targets. Note that this
      #   option requires the "skype" plugin to be installed in jenkins.
      #   Example: testuser *testgroup
      # @option params [String] :skype_strategy (change)
      #   the skype strategy to be used for sending notifications.
      #   Valid values: all, failure, failure_and_fixed, change.
      # @option params [Boolean] :skype_notify_on_build_start (false)
      #   whether to notify skype targets on build start
      # @option params [Boolean] :skype_notify_suspects (false)
      #   whether to notify suspects on skype
      # @option params [Boolean] :skype_notify_culprits (false)
      #   whether to notify culprits on skype
      # @option params [Boolean] :skype_notify_fixers (false)
      #   whether to notify fixers on skype
      # @option params [Boolean] :skype_notify_upstream_committers (false)
      #   whether to notify upstream committers on skype
      # @option params [String] :skype_message (summary_and_scm_changes)
      #   the information to be sent as notification message. Valid:
      #   just_summary, summary_and_scm_changes,
      #   summary_and_build_parameters, summary_scm_changes_and_failed_tests.
      # @option params [String] :child_projects
      #   the projects to add as downstream projects
      # @option params [String] :child_threshold (failure)
      #   the threshold for child projects. Valid options: success, failure,
      #   or unstable.
      #
      # @see #create_freestyle
      # @see #update_freestyle
      #
      # @return [String] the HTTP status code from the POST request
      #
      def create_or_update_freestyle(params)
        if exists?(params[:name])
          update_freestyle(params)
        else
          create_freestyle(params)
        end
      end

      # Create a freestyle project by accepting a Hash of parameters. For the
      # parameter description see #create_of_update_freestyle
      #
      # @param params [Hash] the parameters for creating a job
      #
      # @example Create a Freestype Project
      #   create_freestyle(
      #     :name => "test_freestyle_job",
      #     :keep_dependencies => true,
      #     :concurrent_build => true,
      #     :scm_provider => "git",
      #     :scm_url => "git://github.com./arangamani/jenkins_api_client.git",
      #     :scm_branch => "master",
      #     :shell_command => "bundle install\n rake func_tests"
      #   )
      #
      # @see #create_or_update_freestyle
      # @see #create
      # @see #update_freestyle
      #
      # @return [String] the HTTP status code from the POST request
      #
      def create_freestyle(params)
        xml = build_freestyle_config(params)
        create(params[:name], xml)
      end

      # Update a job with params given as a hash instead of the xml. For the
      # parameter description see #create_or_update_freestyle
      #
      # @param params [Hash] parameters to update a freestyle project
      #
      # @see #create_or_update_freestyle
      # @see #update
      # @see #create_freestyle
      #
      # @return [String] the HTTP status code from the POST request
      #
      def update_freestyle(params)
        xml = build_freestyle_config(params)
        update(params[:name], xml)
      end

      # Builds the XML configuration based on the parameters passed as a Hash
      #
      # @param params [Hash] the parameters for building XML configuration
      #
      # @return [String] the generated XML configuration of the project
      #
      def build_freestyle_config(params)
        # Supported SCM providers
        supported_scm = ["git", "subversion", "cvs"]

        # Set default values for params that are not specified.
        raise ArgumentError, "Job name must be specified" \
          unless params.is_a?(Hash) && params[:name]

        [
          :keep_dependencies,
          :block_build_when_downstream_building,
          :block_build_when_upstream_building,
          :concurrent_build
        ].each do |param|
          params[param] = false if params[param].nil?
        end

        if params[:notification_email]
          if params[:notification_email_for_every_unstable].nil?
            params[:notification_email_for_every_unstable] = false
          end
          if params[:notification_email_send_to_individuals].nil?
            params[:notification_email_send_to_individuals] ||= false
          end
        end

        # SCM configurations and Error handling.
        unless params[:scm_provider].nil?
          unless supported_scm.include?(params[:scm_provider])
            raise "SCM #{params[:scm_provider]} is currently not supported"
          end
          raise "SCM URL must be specified" if params[:scm_url].nil?
          params[:scm_branch] = "master" if params[:scm_branch].nil?
          if params[:scm_use_head_if_tag_not_found].nil?
            params[:scm_use_head_if_tag_not_found] = false
          end
        end

        # Child projects configuration and Error handling
        if params[:child_threshold].nil? && !params[:child_projects].nil?
          params[:child_threshold] = "failure"
        end

        @logger.debug "Creating a freestyle job with params: #{params.inspect}"

        # Build the Job xml file based on the parameters given
        builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
          xml.project do
            xml.actions
            xml.description
            xml.keepDependencies "#{params[:keep_dependencies]}"
            xml.properties
            #buildlogs related stuff
            if params[:discard_old_builds]
              xml.logRotator(:class => 'hudson.tasks.LogRotator') do
                xml.daysToKeep params[:discard_old_builds][:daysToKeep] || -1
                xml.numToKeep params[:discard_old_builds][:numToKeep] || -1
                xml.artifactDaysToKeep params[:discard_old_builds][:artifactDaysToKeep] || -1
                xml.artifactNumToKeep params[:discard_old_builds][:artifactNumToKeep] || -1
              end
            end

            # SCM related stuff
            if params[:scm_provider] == 'subversion'
              # Build subversion related XML portion
              scm_subversion(params, xml)
            elsif params[:scm_provider] == "cvs"
              # Build CVS related XML portion
              scm_cvs(params, xml)
            elsif params[:scm_provider] == "git"
              # Build Git related XML portion
              scm_git(params, xml)
            else
              xml.scm(:class => "hudson.scm.NullSCM")
            end
            # Restrict job to run in a specified node
            if params[:restricted_node]
              xml.assignedNode "#{params[:restricted_node]}"
              xml.canRoam "false"
            else
              xml.canRoam "true"
            end
            xml.disabled "false"
            xml.blockBuildWhenDownstreamBuilding(
              "#{params[:block_build_when_downstream_building]}")
            xml.blockBuildWhenUpstreamBuilding(
              "#{params[:block_build_when_upstream_building]}")
            xml.triggers.vector do
              if params[:timer]
                xml.send("hudson.triggers.TimerTrigger") do
                  xml.spec params[:timer]
                end
              end

              if params[:scm_trigger]
                xml.send("hudson.triggers.SCMTrigger") do
                  xml.spec params[:scm_trigger]
                  xml.ignorePostCommitHooks params.fetch(:ignore_post_commit_hooks) { false }
                end
              end
            end
            xml.concurrentBuild "#{params[:concurrent_build]}"
            # Shell command stuff
            xml.builders do
              if params[:shell_command]
                xml.send("hudson.tasks.Shell") do
                  xml.command "#{params[:shell_command]}"
                end
              end
            end
            # Adding Downstream projects
            xml.publishers do
              # Build portion of XML that adds child projects
              child_projects(params, xml) if params[:child_projects]
              # Build portion of XML that adds email notification
              notification_email(params, xml) if params[:notification_email]
              # Build portion of XML that adds skype notification
              skype_notification(params, xml) if params[:skype_targets]
              artifact_archiver(params[:artifact_archiver], xml)
            end
            xml.buildWrappers
          end
        end

        xml_doc = Nokogiri::XML(builder.to_xml)
        plugin_collection.configure(xml_doc).to_xml
      end


      # Adding email notification to a job
      #
      # @param [Hash] params parameters to add email notification
      #
      # @option params [String] :name Name of the job
      # @option params [String] :notification_email Email address to send
      # @option params [Boolean] :notification_email_for_every_unstable
      #   Send email notification email for every unstable build
      #
      def add_email_notification(params)
        raise "No job name specified" unless params[:name]
        raise "No email address specified" unless params[:notification_email]
        @logger.info "Adding '#{params[:notification_email]}' to be" +
          " notified for '#{params[:name]}'"
        xml = get_config(params[:name])
        n_xml = Nokogiri::XML(xml)
        if n_xml.xpath("//hudson.tasks.Mailer").empty?
          p_xml = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |b_xml|
            notification_email(params, b_xml)
          end
          email_xml = Nokogiri::XML(p_xml.to_xml).xpath(
            "//hudson.tasks.Mailer"
          ).first
          n_xml.xpath("//publishers").first.add_child(email_xml)
          post_config(params[:name], n_xml.to_xml)
        end
      end

      # Adding skype notificaiton to a job
      #
      # @param [Hash] params parameters for adding skype notification
      #  * +:name+ name of the job to add skype notification
      #  * +:skype_targets+ skype targets for sending notifications to. Use *
      #    to specify group chats. Use space to separate multiple targets.
      #    Example: testuser, *testgroup.
      #  * +:skype_strategy+ skype strategy to be used for sending
      #    notifications. Valid values: all, failure, failure_and_fixed,
      #    change. Default: change.
      #  * +:skype_notify_on_build_start+ Default: false
      #  * +:skype_notify_suspects+ Default: false
      #  * +:skype_notify_culprits+ Default: false
      #  * +:skype_notify_fixers+ Default: false
      #  * +:skype_notify_upstream_committers+ Default: false
      #  * +:skype_message+ what should be sent as notification message. Valid:
      #    just_summary, summary_and_scm_changes, summary_and_build_parameters,
      #    summary_scm_changes_and_failed_tests.
      #    Default: summary_and_scm_changes
      #
      def add_skype_notification(params)
        raise "No job name specified" unless params[:name]
        raise "No Skype target specified" unless params[:skype_targets]
        @logger.info "Adding Skype notification for '#{params[:name]}'"
        xml = get_config(params[:name])
        n_xml = Nokogiri::XML(xml)
        if n_xml.xpath("//hudson.plugins.skype.im.transport.SkypePublisher").empty?
          p_xml = Nokogiri::XML::Builder.new(:encoding => "UTF-8") do |b_xml|
            skype_notification(params, b_xml)
          end
          skype_xml = Nokogiri::XML(p_xml.to_xml).xpath(
            "//hudson.plugins.skype.im.transport.SkypePublisher"
          ).first
          n_xml.xpath("//publishers").first.add_child(skype_xml)
          post_config(params[:name], n_xml.to_xml)
        end
      end

      # Configure post-build step to archive artifacts
      #
      # @param artifact_params [Hash] parameters controlling how artifacts are archived
      #
      # @option artifact_params [String] :artifact_files
      #   pattern or names of files to archive
      # @option artifact_params [String] :excludes
      #   pattern or names of files to exclude
      # @option artifact_params [Boolean] :fingerprint (false)
      #   fingerprint the archives
      # @option artifact_params [Boolean] :allow_empty_archive (false)
      #   whether to allow empty archives
      # @option artifact_params [Boolean] :only_if_successful (false)
      #   only archive if successful
      # @option artifact_params [Boolean] :default_excludes (false)
      #   exclude defaults automatically
      #
      # @return [Nokogiri::XML::Builder]
      #
      def artifact_archiver(artifact_params, xml)
        return xml if artifact_params.nil?

        xml.send('hudson.tasks.ArtifactArchiver') do |x|
          x.artifacts artifact_params.fetch(:artifact_files) { '' }
          x.excludes artifact_params.fetch(:excludes) { '' }
          x.fingerprint artifact_params.fetch(:fingerprint) { false }
          x.allowEmptyArchive artifact_params.fetch(:allow_empty_archive) { false }
          x.onlyIfSuccessful artifact_params.fetch(:only_if_successful) { false }
          x.defaultExcludes artifact_params.fetch(:default_excludes) { false }
        end

        xml
      end

      # Rename a job given the old name and new name
      #
      # @param [String] old_job Name of the old job
      # @param [String] new_job Name of the new job.
      #
      def rename(old_job, new_job)
        @logger.info "Renaming job '#{old_job}' to '#{new_job}'"
        @client.api_post_request("/job/#{path_encode old_job}/doRename?newName=#{form_encode new_job}")
      end

      # Delete a job given the name
      #
      # @param job_name [String] the name of the job to delete
      #
      # @return [String] the response from the HTTP POST request
      #
      def delete(job_name)
        @logger.info "Deleting job '#{job_name}'"
        @client.api_post_request("/job/#{path_encode job_name}/doDelete")
      end

      # Deletes all jobs from Jenkins
      #
      # @note This method will remove all jobs from Jenkins. Please use with
      #       caution.
      #
      def delete_all!
        @logger.info "Deleting all jobs from jenkins"
        list_all.each { |job| delete(job) }
      end

      # Wipe out the workspace for a job given the name
      #
      # @param job_name [String] the name of the job to wipe out the workspace
      #
      # @return [String] response from the HTTP POST request
      #
      def wipe_out_workspace(job_name)
        @logger.info "Wiping out the workspace of job '#{job_name}'"
        @client.api_post_request("/job/#{path_encode job_name}/doWipeOutWorkspace")
      end

      # Stops a running build of a job
      # This method will stop the current/most recent build if no build number
      # is specified. The build will be stopped only if it was
      # in 'running' state.
      #
      # @param job_name [String] the name of the job to stop the build
      # @param build_number [Number] the build number to stop
      #
      def stop_build(job_name, build_number = 0)
        build_number = get_current_build_number(job_name) if build_number == 0
        raise "No builds for #{job_name}" unless build_number
        @logger.info "Stopping job '#{job_name}' Build ##{build_number}"
        # Check and see if the build is running
        is_building = @client.api_get_request(
          "/job/#{path_encode job_name}/#{build_number}"
        )["building"]
        if is_building
          @client.api_post_request("/job/#{path_encode job_name}/#{build_number}/stop")
        end
      end
      alias_method :stop, :stop_build
      alias_method :abort, :stop_build

      # Re-create the same job
      # This is a hack to clear any existing builds
      #
      # @param job_name [String] the name of the job to recreate
      #
      # @return [String] the response from the HTTP POST request
      #
      def recreate(job_name)
        @logger.info "Recreating job '#{job_name}'"
        job_xml = get_config(job_name)
        delete(job_name)
        create(job_name, job_xml)
      end

      # Copy a job
      #
      # @param from_job_name [String] the name of the job to copy from
      # @param to_job_name [String] the name of the job to copy to
      #
      # @return [String] the response from the HTTP POST request
      #
      def copy(from_job_name, to_job_name=nil)
        to_job_name = "copy_of_#{from_job_name}" if to_job_name.nil?
        @logger.info "Copying job '#{from_job_name}' to '#{to_job_name}'"
        @client.api_post_request(
          "/createItem?name=#{path_encode to_job_name}&mode=copy&from=#{path_encode from_job_name}"
        )
      end

      # Get progressive console output from Jenkins server for a job
      #
      # @param [String] job_name Name of the Jenkins job
      # @param [Number] build_num Specific build number to obtain the
      #   console output from. Default is the recent build
      # @param [Number] start start offset to get only a portion of the text
      # @param [String] mode Mode of text output. 'text' or 'html'
      #
      # @return [Hash] response
      #   * +output+ console output of the job
      #   * +size+ size of the text. This can be used as 'start' for the
      #     next call to get progressive output
      #   * +more+ more data available for the job. 'true' if available
      #     and nil otherwise
      #
      def get_console_output(job_name, build_num = 0, start = 0, mode = 'text')
        build_num = get_current_build_number(job_name) if build_num == 0
        if build_num == 0
          puts "No builds for this job '#{job_name}' yet."
          return nil
        end
        if mode == 'text'
          mode = 'Text'
        elsif mode == 'html'
          mode = 'Html'
        else
          raise "Mode should either be 'text' or 'html'. You gave: #{mode}"
        end
        get_msg = "/job/#{path_encode job_name}/#{build_num}/logText/progressive#{mode}?"
        get_msg << "start=#{start}"
        raw_response = true
        api_response = @client.api_get_request(get_msg, nil, nil, raw_response)
        #puts "Response: #{api_response.header['x-more-data']}"
        response = {}
        response['output'] = api_response.body
        response['size'] = api_response.header['x-text-size']
        response['more'] = api_response.header['x-more-data']

        response
      end

      # List all jobs on the Jenkins CI server
      #
      # @return [Array<String>] the names of all jobs in jenkins
      #
      def list_all
        response_json = @client.api_get_request("", "tree=jobs[name]")["jobs"]
        response_json.map { |job| job["name"] }.sort
      end

      # Checks if the given job exists in Jenkins
      #
      # @param job_name [String] the name of the job to check
      #
      # @return [Boolean] whether the job exists in jenkins or not
      #
      def exists?(job_name)
        list(job_name).include?(job_name)
      end

      # List all Jobs matching the given status
      # You can optionally pass in jobs list to filter the status from
      #
      # @param status [String] the job status to filter
      # @param jobs [Array<String>] if specified this array will be used for
      #   filtering by the status otherwise the filtering will be done using
      #   all jobs available in jenkins
      #
      # @return [Array<String>] filtered jobs
      #
      def list_by_status(status, jobs = [])
        jobs = list_all if jobs.empty?
        @logger.info "Obtaining jobs matching status '#{status}'"
        json_response = @client.api_get_request("", "tree=jobs[name,color]")
        filtered_jobs = []
        json_response["jobs"].each do |job|
          if color_to_status(job["color"]) == status &&
             jobs.include?(job["name"])
            filtered_jobs << job["name"]
          end
        end
        filtered_jobs
      end

      # List all jobs that match the given regex
      #
      # @param filter [String] a regular expression or a string to filter jobs
      # @param ignorecase [Boolean] whether to ignore case or not
      #
      # @return [Array<String>] jobs matching the given pattern
      #
      def list(filter, ignorecase = true)
        @logger.info "Obtaining jobs matching filter '#{filter}'"
        response_json = @client.api_get_request("")
        jobs = []
        response_json["jobs"].each do |job|
          if ignorecase
            jobs << job["name"] if job["name"] =~ /#{filter}/i
          else
            jobs << job["name"] if job["name"] =~ /#{filter}/
          end
        end
        jobs
      end

      # List all jobs on the Jenkins CI server along with their details
      #
      # @return [Array<Hash>] the details of all jobs in jenkins
      #
      def list_all_with_details
        @logger.info "Obtaining the details of all jobs"
        response_json = @client.api_get_request("")
        response_json["jobs"]
      end

      # List details of a specific job
      #
      # @param job_name [String] the name of the job to obtain the details from
      #
      # @return [Hash] the details of the specified job
      #
      def list_details(job_name)
        @logger.info "Obtaining the details of '#{job_name}'"
        @client.api_get_request("/job/#{path_encode job_name}")
      end

      # List upstream projects of a specific job
      #
      # @param job_name [String] the name of the job to obtain upstream
      #  projects for
      #
      def get_upstream_projects(job_name)
        @logger.info "Obtaining the upstream projects of '#{job_name}'"
        response_json = @client.api_get_request("/job/#{path_encode job_name}")
        response_json["upstreamProjects"]
      end

      # List downstream projects of a specific job
      #
      # @param job_name [String] the name of the job to obtain downstream
      #   projects for
      #
      def get_downstream_projects(job_name)
        @logger.info "Obtaining the down stream projects of '#{job_name}'"
        response_json = @client.api_get_request("/job/#{path_encode job_name}")
        response_json["downstreamProjects"]
      end

      # Obtain build details of a specific job
      #
      # @param [String] job_name
      #
      def get_builds(job_name, options = {})
        @logger.info "Obtaining the build details of '#{job_name}'"
        url = "/job/#{path_encode job_name}"

        tree = options[:tree] || nil
        response_json = @client.api_get_request url, tree_string(tree)
        response_json["builds"]
      end

      # This method maps the color to status of a job
      #
      # @param [String] color color given by the API for a job
      #
      # @return [String] status status of the given job matching the color
      #
      def color_to_status(color)
        case color
        when "blue"
          "success"
        when "red"
          "failure"
        when "yellow"
          "unstable"
        when /anime/
          "running"
        # In the recent version of Jenkins (> 1.517), jobs that are not built
        # yet have a color of "notbuilt" instead of "grey". Include that to the
        # not_run condition so it is backward compatible.
        when "grey", "notbuilt"
          "not_run"
        when "aborted"
          "aborted"
        when "disabled"
          "disabled"
        else
          "invalid"
        end
      end

      # Determine if the build is queued
      #
      # @param [String] job_name
      #
      # @return [Integer] build number if queued, or [Boolean] false if not queued
      #
      def queued?(job_name)
        queue_result = @client.api_get_request("/job/#{path_encode job_name}")['inQueue']
        if queue_result
          return @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber']
        else
          return queue_result
        end
      end

      # Obtain the current build status of the job
      # By default Jenkins returns the color of the job status icon
      # This function translates the color into a meaningful status
      #
      # @param [String] job_name
      #
      # @return [String] status current status of the given job
      #
      def get_current_build_status(job_name)
        @logger.info "Obtaining the current build status of '#{job_name}'"
        response_json = @client.api_get_request("/job/#{path_encode job_name}")
        color_to_status(response_json["color"])
      end
      alias_method :status, :get_current_build_status

      # Obtain the current build number of the given job
      # This function returns nil if there were no builds for the given job.
      #
      # @param [String] job_name
      #
      # @return [Integer] current build number of the given job
      #
      def get_current_build_number(job_name)
        @logger.info "Obtaining the current build number of '#{job_name}'"
        @client.api_get_request("/job/#{path_encode job_name}")['nextBuildNumber'].to_i - 1
      end
      alias_method :build_number, :get_current_build_number

      # Build a Jenkins job, optionally waiting for build to start and
      # returning the build number.
      # Adds support for new/old Jenkins servers where build_queue id may
      # not be available. Also adds support for periodic callbacks, and
      # optional cancellation of queued_job if not started within allowable
      # time window (if build_queue option available)
      #
      #   Notes:
      #     'opts' may be a 'true' or 'false' value to maintain
      #       compatibility with old method signature, where true indicates
      #     'return_build_number'. In this case, true is translated to:
      #       { 'build_start_timeout' => @client_timeout }
      #       which simulates earlier behavior.
      #
      #   progress_proc
      #     Optional proc that is called periodically while waiting for
      #     build to start.
      #     Initial call (with poll_count == 0) indicates build has been
      #     requested, and that polling is starting.
      #     Final call will indicate one of build_started or cancelled.
      #     params:
      #       max_wait [Integer] Same as opts['build_start_timeout']
      #       current_wait [Integer]
      #       poll_count [Integer] How many times has queue been polled
      #
      #   completion_proc
      #     Optional proc that is called <just before> the 'build' method
      #     exits.
      #     params:
      #       build_number [Integer]  Present if build started or nil
      #       build_cancelled [Boolean]  True if build timed out and was
      #         successfully removed from build-queue
      #
      # @param [String] job_name the name of the job
      # @param [Hash]   params   the parameters for parameterized build
      # @param [Hash]   opts     options for this method
      #  * +build_start_timeout+ [Integer] How long to wait for queued
      #    build to start before giving up. Default: 0/nil
      #  * +cancel_on_build_start_timeout+ [Boolean] Should an attempt be
      #    made to cancel the queued build if it hasn't started within
      #    'build_start_timeout' seconds? This only works on newer versions
      #    of Jenkins where JobQueue is exposed in build post response.
      #    Default: false
      #  * +poll_interval+ [Integer] How often should we check with CI
      #    Server while waiting for start. Default: 2 (seconds)
      #  * +progress_proc+ [Proc] A proc that will receive progress notitications. Default: nil
      #  * +completion_proc+ [Proc] A proc that is called <just before>
      #    this method (build) exits.  Default: nil
      #
      # @return [Integer] build number, or nil if not started (IF TIMEOUT SPECIFIED)
      # @return [String] HTTP response code (per prev. behavior) (NO TIMEOUT SPECIFIED)
      #
      def build(job_name, params={}, opts = {})
        if opts.nil? || opts.is_a?(FalseClass)
          opts = {}
        elsif opts.is_a?(TrueClass)
          opts = { 'build_start_timeout' => @client_timeout }
        end

        opts['job_name'] = job_name

        msg = "Building job '#{job_name}'"
        msg << " with parameters: #{params.inspect}" unless params.empty?
        @logger.info msg

        if (opts['build_start_timeout'] || 0) > 0
          # Best-guess build-id
          # This is only used if we go the old-way below... but we can use this number to detect if multiple
          # builds were queued
          current_build_id = get_current_build_number(job_name)
          expected_build_id = current_build_id > 0 ? current_build_id + 1 : 1
        end

        if (params.nil? or params.empty?)
          response = @client.api_post_request("/job/#{path_encode job_name}/build",
            {},
            true)
        else
          response = @client.api_post_request("/job/#{path_encode job_name}/buildWithParameters",
            params,
            true)
        end

        if (opts['build_start_timeout'] || 0) > 0
          if @client.compare_versions(@client.get_jenkins_version, JENKINS_QUEUE_ID_SUPPORT_VERSION) >= 0
            return get_build_id_from_queue(response, expected_build_id, opts)
          else
            return get_build_id_the_old_way(expected_build_id, opts)
          end
        else
          return response.code
        end
      end

      def get_build_id_from_queue(response, expected_build_id, opts)
        # If we get this far the API hasn't detected an error response (it would raise Exception)
        # So no need to check response code
        # Obtain the queue ID from the location
        # header and wait till the build is moved to one of the executors and a
        # build number is assigned
        build_start_timeout = opts['build_start_timeout']
        poll_interval = opts['poll_interval'] || 2
        poll_interval = 1 if poll_interval < 1
        progress_proc = opts['progress_proc']
        completion_proc = opts['completion_proc']
        job_name = opts['job_name']

        if response["location"]
          task_id_match = response["location"].match(/\/item\/(\d*)\//)
          task_id = task_id_match.nil? ? nil : task_id_match[1]
          unless task_id.nil?
            @logger.info "Job queued for #{job_name}, will wait up to #{build_start_timeout} seconds for build to start..."

            # Let progress proc know we've queued the build
            progress_proc.call(build_start_timeout, 0, 0) if progress_proc

            # Wait for the build to start
            begin
              start = Time.now.to_i
              Timeout::timeout(build_start_timeout) do
                started = false
                attempts = 0

                while !started
                  # Don't really care about the response... if we get thru here, then it must have worked.
                  # Jenkins will return 404's until the job starts
                  queue_item = @client.queue.get_item_by_id(task_id)

                  if queue_item['executable'].nil?
                    # Job not started yet
                    attempts += 1

                    progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc
                    # Every 5 attempts (~10 seconds)
                    @logger.info "Still waiting..." if attempts % 5 == 0

                    sleep poll_interval
                  else
                    build_number = queue_item['executable']['number']
                    completion_proc.call(build_number, false) if completion_proc

                    return build_number
                  end
                end
              end
            rescue Timeout::Error
              # Well, we waited - and the job never started building
              # Attempt to kill off queued job (if flag set)
              if opts['cancel_on_build_start_timeout']
                @logger.info "Job for '#{job_name}' did not start in a timely manner, attempting to cancel pending build..."

                begin
                  @client.api_post_request("/queue/cancelItem?id=#{task_id}")
                  @logger.info "Job cancelled"
                  completion_proc.call(nil, true) if completion_proc
                rescue JenkinsApi::Exceptions::ApiException => e
                  completion_proc.call(nil, false) if completion_proc
                  @logger.warn "Error while attempting to cancel pending job for '#{job_name}'. #{e.class} #{e}"
                  raise
                end
              else
                @logger.info "Jenkins build for '#{job_name}' failed to start in a timely manner"
                completion_proc.call(nil, false) if completion_proc
              end

              # Old version used to throw timeout error, so we should let that go thru now
              raise
            rescue JenkinsApi::Exceptions::ApiException => e
              # Jenkins Api threw an error at us
              completion_proc.call(nil, false) if completion_proc
              @logger.warn "Problem while waiting for '#{job_name}' build to start.  #{e.class} #{e}"
              raise
            end
          else
            @logger.warn "Jenkins did not return a queue_id for '#{job_name}' build (location: #{response['location']})"
            return get_build_id_the_old_way(expected_build_id, opts)
          end
        else
          @logger.warn "Jenkins did not return a location header for '#{job_name}' build"
          return get_build_id_the_old_way(expected_build_id, opts)
        end
      end
      private :get_build_id_from_queue

      def get_build_id_the_old_way(expected_build_id, opts)
        # Try to wait until the build starts so we can mimic queue
        # Wait for the build to start
        build_start_timeout = opts['build_start_timeout']
        poll_interval = opts['poll_interval'] || 2
        poll_interval = 1 if poll_interval < 1
        progress_proc = opts['progress_proc']
        completion_proc = opts['completion_proc']
        job_name = opts['job_name']

        @logger.info "Build requested for '#{job_name}', will wait up to #{build_start_timeout} seconds for build to start..."

        # Let progress proc know we've queued the build
        progress_proc.call(build_start_timeout, 0, 0) if progress_proc

        begin
          start = Time.now.to_i
          Timeout::timeout(build_start_timeout) do
            attempts = 0

            while true
              attempts += 1

              # Don't really care about the response... if we get thru here, then it must have worked.
              # Jenkins will return 404's until the job starts
              begin
                get_build_details(job_name, expected_build_id)
                completion_proc.call(expected_build_id, false) if completion_proc

                return expected_build_id
              rescue JenkinsApi::Exceptions::NotFound => e
                progress_proc.call(build_start_timeout, (Time.now.to_i - start), attempts) if progress_proc

                # Every 5 attempts (~10 seconds)
                @logger.info "Still waiting..." if attempts % 5 == 0

                sleep poll_interval
              end
            end
          end
        rescue Timeout::Error
          # Well, we waited - and the job never started building
          # Now we need to raise an exception so that the build can be officially failed
          completion_proc.call(nil, false) if completion_proc
          @logger.info "Jenkins '#{job_name}' build failed to start in a timely manner"

          # Old version used to propagate timeout error
          raise
        rescue JenkinsApi::Exceptions::ApiException => e
          completion_proc.call(nil, false) if completion_proc
          # Jenkins Api threw an error at us
          @logger.warn "Problem while waiting for '#{job_name}' build ##{expected_build_id} to start.  #{e.class} #{e}"
          raise
        end
      end
      private :get_build_id_the_old_way

      # Programatically schedule SCM polling for the specified job
      #
      # @param job_name [String] the name of the job
      #
      # @return [String] the response code from the HTTP post request
      #
      def poll(job_name)
        @logger.info "Polling SCM changes for job '#{job_name}'"
        @client.api_post_request("/job/#{job_name}/polling")
      end

      # Enable a job given the name of the job
      #
      # @param [String] job_name
      #
      def enable(job_name)
        @logger.info "Enabling job '#{job_name}'"
        @client.api_post_request("/job/#{path_encode job_name}/enable")
      end

      # Disable a job given the name of the job
      #
      # @param [String] job_name
      #
      def disable(job_name)
        @logger.info "Disabling job '#{job_name}'"
        @client.api_post_request("/job/#{path_encode job_name}/disable")
      end

      # Obtain the configuration stored in config.xml of a specific job
      #
      # @param [String] job_name
      #
      # @return [String] XML Config.xml of the job
      #
      def get_config(job_name)
        @logger.info "Obtaining the config.xml of '#{job_name}'"
        @client.get_config("/job/#{path_encode job_name}")
      end

      # Post the configuration of a job given the job name and the config.xml
      #
      # @param [String] job_name
      # @param [String] xml
      #
      # @return [String] response_code return code from HTTP POST
      #
      def post_config(job_name, xml)
        @logger.info "Posting the config.xml of '#{job_name}'"
        @client.post_config("/job/#{path_encode job_name}/config.xml", xml)
      end

      # Obtain the test results for a specific build of a job
      #
      # @param [String] job_name
      # @param [Number] build_num
      #
      def get_test_results(job_name, build_num)
        build_num = get_current_build_number(job_name) if build_num == 0
        @logger.info "Obtaining the test results of '#{job_name}'" +
          " Build ##{build_num}"
        @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/testReport")
      rescue Exceptions::NotFound
        # Not found is acceptable, as not all builds will have test results
        # and this is what jenkins throws at us in that case
        nil
      end

      # Obtain the plugin results for a specific build of a job
      #
      # @param [String] job_name
      # @param [Number] build_num
      # @param [String] plugin_name
      #
      def get_plugin_results(job_name, build_num, plugin_name)
        build_num = get_current_build_number(job_name) if build_num == 0
        @logger.info "Obtaining the '#{plugin_name}' plugin results of '#{job_name}'" +
          " Build ##{build_num}"
        @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/#{plugin_name}Result")
      rescue Exceptions::NotFound
        # Not found is acceptable, as not all builds will have plugin results
        # and this is what jenkins throws at us in that case
        nil
      end

      # Obtain detailed build info for a job
      #
      # @param [String] job_name
      # @param [Number] build_num
      #
      def get_build_details(job_name, build_num)
        build_num = get_current_build_number(job_name) if build_num == 0
        @logger.info "Obtaining the build details of '#{job_name}'" +
          " Build ##{build_num}"

        @client.api_get_request("/job/#{path_encode job_name}/#{build_num}/")
      end

      # Change the description of a specific job
      #
      # @param [String] job_name
      # @param [String] description
      #
      # @return [String] response_code return code from HTTP POST
      #
      def change_description(job_name, description)
        @logger.info "Changing the description of '#{job_name}' to '#{description}'"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        desc = n_xml.xpath("//description").first
        desc.content = "#{description}"
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Block the build of the job when downstream is building
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def block_build_when_downstream_building(job_name)
        @logger.info "Blocking builds of '#{job_name}' when downstream" +
          " projects are building"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        node = n_xml.xpath("//blockBuildWhenDownstreamBuilding").first
        if node.content == "false"
          node.content = "true"
          xml_modified = n_xml.to_xml
          post_config(job_name, xml_modified)
        end
      end

      # Unblock the build of the job when downstream is building
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def unblock_build_when_downstream_building(job_name)
        @logger.info "Unblocking builds of '#{job_name}' when downstream" +
          " projects are building"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        node = n_xml.xpath("//blockBuildWhenDownstreamBuilding").first
        if node.content == "true"
          node.content = "false"
          xml_modified = n_xml.to_xml
          post_config(job_name, xml_modified)
        end
      end

      # Block the build of the job when upstream is building
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def block_build_when_upstream_building(job_name)
        @logger.info "Blocking builds of '#{job_name}' when upstream" +
          " projects are building"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        node = n_xml.xpath("//blockBuildWhenUpstreamBuilding").first
        if node.content == "false"
          node.content = "true"
          xml_modified = n_xml.to_xml
          post_config(job_name, xml_modified)
        end
      end

      # Unblock the build of the job when upstream is building
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def unblock_build_when_upstream_building(job_name)
        @logger.info "Unblocking builds of '#{job_name}' when upstream" +
          " projects are building"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        node = n_xml.xpath("//blockBuildWhenUpstreamBuilding").first
        if node.content == "true"
          node.content = "false"
          xml_modified = n_xml.to_xml
          post_config(job_name, xml_modified)
        end
      end

      # Allow or disable concurrent build execution
      #
      # @param [String] job_name
      # @param [Bool] option true or false
      #
      # @return [String] response_code return code from HTTP POST
      #
      def execute_concurrent_builds(job_name, option)
        @logger.info "Setting the concurrent build execution option of" +
          " '#{job_name}' to #{option}"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        node = n_xml.xpath("//concurrentBuild").first
        if node.content != "#{option}"
          node.content = option == true ? "true" : "false"
          xml_modified = n_xml.to_xml
          post_config(job_name, xml_modified)
        end
      end

      # Obtain the build parameters of a job. It returns an array of hashes with
      # details of job params.
      #
      # @param [String] job_name
      #
      # @return [Array] params_array Array of parameters for the given job
      #
      def get_build_params(job_name)
        @logger.info "Obtaining the build params of '#{job_name}'"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        params = n_xml.xpath("//parameterDefinitions").first
        params_array = []
        if params
          params.children.each do |param|
            param_hash = {}
            case param.name
            when "hudson.model.StringParameterDefinition",
                 "hudson.model.BooleanParameterDefinition",
                 "hudson.model.TextParameterDefinition",
                 "hudson.model.PasswordParameterDefinition"
              param_hash[:type] = 'string' if param.name =~ /string/i
              param_hash[:type] = 'boolean' if param.name =~ /boolean/i
              param_hash[:type] = 'text' if param.name =~ /text/i
              param_hash[:type] = 'password' if param.name =~ /password/i
              param.children.each do |value|
                param_hash[:name] = value.content if value.name == "name"
                if value.name == "description"
                  param_hash[:description] = value.content
                end
                if value.name == "defaultValue"
                  param_hash[:default] = value.content
                end
              end
            when "hudson.model.RunParameterDefinition"
              param_hash[:type] = 'run'
              param.children.each do |value|
                param_hash[:name] = value.content if value.name == "name"
                if value.name == "description"
                  param_hash[:description] = value.content
                end
                if value.name == "projectName"
                  param_hash[:project] = value.content
                end
              end
            when "hudson.model.FileParameterDefinition"
              param_hash[:type] = 'file'
              param.children.each do |value|
                param_hash[:name] = value.content if value.name == "name"
                if value.name == "description"
                  param_hash[:description] = value.content
                end
              end
            when "hudson.scm.listtagsparameter.ListSubversionTagsParameterDefinition"
              param_hash[:type] = 'list_tags'
              param.children.each do |value|
                if value.name == "name"
                  param_hash[:name] = value.content
                end
                if value.name == "description"
                  param_hash[:description] = value.content
                end
                if value.name == "tagsDir"
                  param_hash[:tags_dir] = value.content
                end
                if value.name == "tagsFilter"
                  param_hash[:tags_filter] = value.content
                end
                if value.name == "reverseByDate"
                  param_hash[:reverse_by_date] = value.content
                end
                if value.name == "reverseByName"
                  param_hash[:reverse_by_name] = value.content
                end
                if value.name == "defaultValue"
                  param_hash[:default] = value.content
                end
                param_hash[:max_tags] = value.content if value.name == "maxTags"
                param_hash[:uuid] = value.content if value.name == "uuid"
              end
            when "hudson.model.ChoiceParameterDefinition"
              param_hash[:type] = 'choice'
              param.children.each do |value|
                param_hash[:name] = value.content if value.name == "name"
                param_hash[:description] = value.content \
                  if value.name == "description"
                choices = []
                if value.name == "choices"
                  value.children.each do |value_child|
                    if value_child.name == "a"
                      value_child.children.each do |choice_child|
                        choices << choice_child.content.strip \
                          unless choice_child.content.strip.empty?
                      end
                    end
                  end
                end
                param_hash[:choices] = choices unless choices.empty?
              end
            end
            params_array << param_hash unless param_hash.empty?
          end
        end
        params_array
      end

      # Add downstream projects to a specific job given the job name,
      # projects to be added as downstream projects, and the threshold
      #
      # @param [String] job_name
      # @param [String] downstream_projects
      # @param [String] threshold - failure, success, or unstable
      # @param [Boolean] overwrite - true or false
      #
      # @return [String] response_code return code from HTTP POST
      #
      def add_downstream_projects(job_name,
                                  downstream_projects,
                                  threshold, overwrite = false)
        @logger.info "Adding #{downstream_projects.inspect} as downstream" +
          " projects for '#{job_name}' with the threshold of '#{threshold}'" +
          " and overwrite option of '#{overwrite}'"
        name, ord, col = get_threshold_params(threshold)
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        child_projects_node = n_xml.xpath("//childProjects").first
        if child_projects_node
          if overwrite
            child_projects_node.content = "#{downstream_projects}"
          else
            to_replace = child_projects_node.content +
              ", #{downstream_projects}"
            child_projects_node.content = to_replace
          end
        else
          publisher_node = n_xml.xpath("//publishers").first
          build_trigger_node = publisher_node.add_child(
            "<hudson.tasks.BuildTrigger/>"
          )
          child_project_node = build_trigger_node.first.add_child(
            "<childProjects>#{downstream_projects}</childProjects>"
          )
          threshold_node = child_project_node.first.add_next_sibling(
            "<threshold/>"
          )
          threshold_node.first.add_child(
            "<name>#{name}</name><ordinal>#{ord}</ordinal><color>#{col}</color>"
          )
        end
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Remove all downstream projects of a specific job
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def remove_downstream_projects(job_name)
        @logger.info "Removing the downstream projects of '#{job_name}'"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        n_xml.search("//hudson.tasks.BuildTrigger").each do |node|
          child_project_trigger = false
          node.search("//childProjects").each do |child_node|
            child_project_trigger = true
            child_node.search("//threshold").each do |threshold_node|
              threshold_node.children.each do |threshold_value_node|
                threshold_value_node.content = nil
                threshold_value_node.remove
              end
              threshold_node.content = nil
              threshold_node.remove
            end
            child_node.content = nil
            child_node.remove
          end
          node.content = nil
          node.remove
        end
        publisher_node = n_xml.search("//publishers").first
        publisher_node.content = nil if publisher_node.children.empty?
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Add upstream projects to a specific job given the job name,
      # projects to be added as upstream projects, and the threshold
      #
      # @param [String] job_name
      # @param [String] upstream_projects - separated with comma
      # @param [String] threshold - failure, success, or unstable
      # @param [Boolean] overwrite - true or false
      #
      # @return [String] response_code return code from HTTP POST
      #
      def add_upstream_projects(job_name,
                                upstream_projects,
                                threshold, overwrite = false)
        @logger.info "Adding #{upstream_projects.inspect} as upstream" +
                         " projects for '#{job_name}' with the threshold of '#{threshold}'" +
                         " and overwrite option of '#{overwrite}'"
        name, ord, col = get_threshold_params(threshold)
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        upstream_projects_node = n_xml.xpath("//upstreamProjects").first
        if upstream_projects_node
          if overwrite
            upstream_projects_node.content = "#{upstream_projects}"
          else
            to_replace = upstream_projects_node.content +
                ", #{upstream_projects}"
            upstream_projects_node.content = to_replace
          end
        else
          triggers_node = n_xml.xpath("//triggers").first
          reverse_build_trigger_node = triggers_node.add_child(
              "<jenkins.triggers.ReverseBuildTrigger/>"
          )
          reverse_build_trigger_node.first.add_child(
              "<spec/>"
          )
          reverse_build_trigger_node.first.add_child(
              "<upstreamProjects>#{upstream_projects}</upstreamProjects>"
          )
          threshold_node = reverse_build_trigger_node.first.add_child(
              "<threshold/>"
          )
          threshold_node.first.add_child(
              "<name>#{name}</name><ordinal>#{ord}</ordinal><color>#{col}</color>"
          )
        end
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Remove all upstream projects of a specific job
      #
      # @param [String] job_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def remove_upstream_projects(job_name)
        @logger.info "Removing the upstream projects of '#{job_name}'"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        n_xml.search("//jenkins.triggers.ReverseBuildTrigger").remove
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Resctrict the given job to a specific node
      #
      # @param [String] job_name
      # @param [String] node_name
      #
      # @return [String] response_code return code from HTTP POST
      #
      def restrict_to_node(job_name, node_name)
        @logger.info "Restricting '#{job_name}' to '#{node_name}' node"
        xml = get_config(job_name)
        n_xml = Nokogiri::XML(xml)
        if (node = n_xml.xpath("//assignedNode").first)
          node.content = node_name
        else
          project = n_xml.xpath("//scm").first
          project.add_next_sibling("<assignedNode>#{node_name}</assignedNode>")
          roam_node = n_xml.xpath("//canRoam").first
          roam_node.content = "false"
        end
        xml_modified = n_xml.to_xml
        post_config(job_name, xml_modified)
      end

      # Unchain any existing chain between given job names
      #
      # @param [Array] job_names Array of job names to be unchained
      #
      def unchain(job_names)
        @logger.info "Unchaining jobs: #{job_names.inspect}"
        job_names.each { |job| remove_downstream_projects(job) }
      end

      # Chain the jobs given based on specified criteria
      #
      # @param [Array] job_names Array of job names to be chained
      # @param [String] threshold threshold for running the next job
      # @param [Array] criteria criteria which should be applied for
      #                picking the jobs for the chain
      # @param [Integer] parallel Number of jobs that should be considered
      #                  for parallel run
      #
      # @return [Array] job_names Names of jobs that are in the top of the
      #                 chain
      def chain(job_names, threshold, criteria, parallel = 1)
        raise "Parallel jobs should be at least 1" if parallel < 1
        unchain(job_names)

        @logger.info "Chaining jobs: #{job_names.inspect}" +
          " with threshold of '#{threshold}' and criteria as '#{criteria}'" +
          " with #{parallel} number of parallel jobs"
        filtered_job_names = []
        if criteria.include?("all") || criteria.empty?
          filtered_job_names = job_names
        else
          job_names.each do |job|
            filtered_job_names << job if criteria.include?(
              @client.job.get_current_build_status(job)
            )
          end
        end

        filtered_job_names.each_with_index do |job_name, index|
          break if index >= (filtered_job_names.length - parallel)
          @client.job.add_downstream_projects(
            job_name, filtered_job_names[index + parallel], threshold, true
          )
        end
        if parallel > filtered_job_names.length
          parallel = filtered_job_names.length
        end
        filtered_job_names[0..parallel-1]
      end

      # Get a list of promoted builds for given job
      #
      # @param  [String] job_name
      # @return [Hash]   Hash map of promitions and the promoted builds. Promotions that didn't took place yet
      #                  return nil
      def get_promotions(job_name)
        result = {}

        @logger.info "Obtaining the promotions of '#{job_name}'"
        response_json = @client.api_get_request("/job/#{job_name}/promotion")

        response_json["processes"].each do |promotion|
          @logger.info "Getting promotion details of '#{promotion['name']}'"

          if promotion['color'] == 'notbuilt'
            result[promotion['name']] = nil
          else
            promo_json = @client.api_get_request("/job/#{job_name}/promotion/latest/#{promotion['name']}")
            result[promotion['name']] = promo_json['target']['number']
          end
        end

        result
      end


      # Create a new promotion process
      #
      # This must be called before set/get promote config can be used on a process
      #
      # Must be called after updating the job's config
      # @param  [String] job_name
      # @param  [String] process The process name
      # @return [String] Process config
      def init_promote_process(job_name, process, config)
        @logger.info "Creating new process #{process} for job #{job_name}"
        @client.post_config("/job/#{job_name}/promotion/createProcess?name=#{process}", config)
      end


      # Get a job's promotion config
      #
      # @param  [String] job_name
      # @param  [String] process The process name
      # @return [String] Promote config
      def get_promote_config(job_name, process)
        @logger.info "Getting promote config for job '#{job_name}' process '#{process}'"
        @client.get_config("/job/#{job_name}/promotion/process/#{process}/config.xml")
      end

      # Set a job's promotion config
      #
      # @param  [String] job_name
      # @param  [String] process The process name
      # @param  [String] Job config
      # @return nil
      def set_promote_config(job_name, process, config)
        @logger.info "Setting promote config for job '#{job_name}' process '#{process}' to #{config}"
        @client.post_config("/job/#{job_name}/promotion/process/#{process}/config.xml", config)
      end

      # Delete a job's promotion config
      #
      # @param [String] job_name
      # @param [String] process The process name
      # @return nil
      def delete_promote_config(job_name, process)
        @logger.info "Deleting promote config for job '#{job_name}' process '#{process}'"
        @client.post_config("/job/#{job_name}/promotion/process/#{process}/doDelete")
      end

      #A Method to find artifacts path from the Current Build
      #
      # @param [String] job_name
      # @param [Integer] build_number
      #   defaults to latest build
      #
      def find_artifact(job_name, build_number = 0)
        find_artifacts(job_name, build_number).first
      end

      #A Method to check artifact exists path from the Current Build
      #
      # @param [String] job_name
      # @param [Integer] build_number
      #   defaults to latest build
      #
      def artifact_exists?(job_name, build_number = 0)
        begin
          artifact_path(job_name: job_name, build_number: build_number)

          return true
        rescue Exception => e
          return false
        end
      end

      # Find the artifacts for build_number of job_name, defaulting to current job
      #
      # @param [String] job_name
      # @param [Integer] build_number Optional build number
      # @return [String, Hash] JSON response from Jenkins
      #
      def find_artifacts(job_name, build_number = nil)
        response_json       = get_build_details(job_name, build_number)
        artifact_path(build_details: response_json).map do |p|
          path_encode("#{response_json['url']}artifact/#{p['relativePath']}")
        end
      end

      # Find the artifacts for the current job
      #
      # @param [String] job_name
      # @return [String, Hash] JSON response from Jenkins
      #
      def find_latest_artifacts(job_name)
        find_artifacts(job_name)
      end

      private

      # Obtains the threshold params used by jenkins in the XML file
      # given the threshold
      #
      # @param [String] threshold success, failure, or unstable
      #
      # @return [String] status readable status matching the color
      #
      def get_threshold_params(threshold)
        case threshold
        when 'success'
          name = 'SUCCESS'
          ordinal = 0
          color = 'BLUE'
        when 'unstable'
          name = 'UNSTABLE'
          ordinal = 1
          color = 'YELLOW'
        when 'failure'
          name = 'FAILURE'
          ordinal = 2
          color = 'RED'
        end
        return name, ordinal, color
      end

      # This private method builds portion of XML that adds subversion SCM
      # to a Job
      #
      # @param [Hash] params parameters to be used for building XML
      # @param [XML] xml Nokogiri XML object
      #
      def scm_subversion(params, xml)
        xml.scm(:class => "hudson.scm.SubversionSCM",
               :plugin => "subversion@1.39") {
         xml.locations {
           xml.send("hudson.scm.SubversionSCM_-ModuleLocation") {
             xml.remote "#{params[:scm_url]}"
             xml.local "."
           }
         }
         xml.excludedRegions
         xml.includedRegions
         xml.excludedUsers
         xml.excludedRevprop
         xml.excludedCommitMessages
         xml.workspaceUpdater(:class =>
                              "hudson.scm.subversion.UpdateUpdater")
        }
      end

      # This private method builds portion of XML that adds CVS SCM to a Job
      #
      # @param [Hash] params parameters to be used for building XML
      # @param [XML] xml Nokogiri XML object
      #
      def scm_cvs(params, xml)
        xml.scm(:class => "hudson.scm.CVSSCM",
                :plugin => "cvs@1.6") {
          xml.cvsroot "#{params[:scm_url]}"
          xml.module "#{params[:scm_module]}"
          if params[:scm_branch]
            xml.branch "#{params[:scm_branch]}"
          else
            xml.branch "#{params[:scm_tag]}"
          end
          xml.canUseUpdate true
          xml.useHeadIfNotFound(
            "#{params[:scm_use_head_if_tag_not_found]}")
          xml.flatten true
          if params[:scm_tag]
            xml.isTag true
          else
            xml.isTag false
          end
          xml.excludedRegions
        }
      end

      # This private method adds portion of XML that adds Git SCM to a Job
      #
      # @param [Hash] params parameters to be used for building XML
      # @param [XML] xml Nokogiri XML object
      #
      def scm_git(params, xml)
        xml.scm(:class => "hudson.plugins.git.GitSCM") {
          xml.configVersion "2"
          xml.userRemoteConfigs {
            xml.send("hudson.plugins.git.UserRemoteConfig") {
              xml.name
              xml.refspec
              xml.credentialsId "#{params[:scm_credentials_id]}"
              xml.url "#{params[:scm_url]}"
            }
          }
          xml.branches {
            xml.send("hudson.plugins.git.BranchSpec") {
              xml.name "#{params[:scm_branch]}"
            }
          }
          xml.disableSubmodules "false"
          xml.recursiveSubmodules "false"
          xml.doGenerateSubmoduleConfigurations "false"
          xml.authorOrCommitter "false"
          xml.clean "false"
          xml.wipeOutWorkspace "false"
          xml.pruneBranches "false"
          xml.remotePoll "false"
          xml.ignoreNotifyCommit "false"
          xml.useShallowClone "false"
          xml.buildChooser(:class =>
                           "hudson.plugins.git.util.DefaultBuildChooser")
          xml.gitTool params.fetch(:scm_git_tool) { "Default" }
          xml.submoduleCfg(:class => "list")
          xml.relativeTargetDir
          xml.reference
          xml.excludedRegions
          xml.excludedUsers
          xml.gitConfigName
          xml.gitConfigEmail
          xml.skipTag "false"
          xml.includedRegions
          xml.scmName
        }
      end

      # Method for creating portion of xml that builds Skype notification
      # Use this option only when you have the Skype plugin installed and
      # everything is set up properly
      #
      # @param [Hash] params Parameters for adding skype notificaiton. For the
      # options in this params Hash refer to create_freestyle
      # @param [XML] xml Main xml to attach the skype portion.
      #
      def skype_notification(params, xml)
        params[:skype_strategy] = case params[:skype_strategy]
        when "all"
          "ALL"
        when "failure"
          "ANY_FAILURE"
        when "failure_and_fixed"
          "FAILURE_AND_FIXED"
        when "change"
          "STATECHANGE_ONLY"
        else
          "STATECHANGE_ONLY"
        end

        params[:skype_notify_on_build_start] = false if params[:skype_notify_on_build_start].nil?
        params[:skype_notify_suspects] = false if params[:skype_notify_suspects].nil?
        params[:skype_notify_culprits] = false if params[:skype_notify_culprits].nil?
        params[:skype_notify_fixers] = false if params[:skype_notify_fixers].nil?
        params[:skype_notify_upstream_committers] = false if params[:skype_notify_upstream_committers].nil?

        targets = params[:skype_targets].split(/\s+/)
        xml.send("hudson.plugins.skype.im.transport.SkypePublisher") {
          xml.targets {
            targets.each { |target|
              if target =~ /^\*/
                # Group Chat
                xml.send("hudson.plugins.im.GroupChatIMMessageTarget") {
                  # Skipe the first * character
                  xml.value target[1..-1]
                  xml.notificationOnly false
                }
              else
                # Individual message
                xml.send("hudson.plugins.im.DefaultIMMessageTarget") {
                  xml.value target
                }
              end
            }
          }
          xml.strategy "#{params[:skype_strategy]}"
          xml.notifyOnBuildStart params[:skype_notify_on_build_start]
          xml.notifySuspects params[:skype_notify_suspects]
          xml.notifyCulprits params[:skype_notify_culprits]
          xml.notifyFixers params[:skype_notify_fixers]
          xml.notifyUpstreamCommitters params[:skype_notify_upstream_committers]
          notification_class = case params[:skype_message]
          when "just_summary"
            "hudson.plugins.im.build_notify.SummaryOnlyBuildToChatNotifier"
          when "summary_and_scm_changes"
            "hudson.plugins.im.build_notify.DefaultBuildToChatNotifier"
          when "summary_and_build_parameters"
            "hudson.plugins.im.build_notify.BuildParametersBuildToChatNotifier"
          when "summary_scm_changes_and_failed_tests"
            "hudson.plugins.im.build_notify.PrintFailingTestsBuildToChatNotifier"
          else
            "hudson.plugins.im.build_notify.DefaultBuildToChatNotifier"
          end
          xml.buildToChatNotifier(:class => notification_class)
          xml.matrixMultiplier "ONLY_CONFIGURATIONS"
        }
      end

      # This private method builds portion of XML that adds notification email
      # to a Job.
      #
      # @param [Hash] params parameters to be used for building XML
      # @param [XML] xml Nokogiri XML object
      #
      def notification_email(params, xml)
        if params[:notification_email]
          xml.send("hudson.tasks.Mailer") {
            xml.recipients "#{params[:notification_email]}"
            xml.dontNotifyEveryUnstableBuild(
              "#{params[:notification_email_for_every_unstable]}")
            xml.sendToIndividuals(
              "#{params[:notification_email_send_to_individuals]}")
          }
        end
      end

      # This private method builds portion of XML that adds child projects
      # to a Job.
      #
      # @param [Hash] params parameters to be used for building XML
      # @param [XML] xml Nokogiri XML object
      #
      def child_projects(params, xml)
        xml.send("hudson.tasks.BuildTrigger") {
          xml.childProjects "#{params[:child_projects]}"
          threshold = params[:child_threshold]
          name, ordinal, color = get_threshold_params(threshold)
          xml.threshold {
            xml.name "#{name}"
            xml.ordinal "#{ordinal}"
            xml.color "#{color}"
          }
        }
      end

      def tree_string tree_value
        return nil unless tree_value
        "tree=#{tree_value}"
      end

      # This private method gets the artifact path or throws an exception
      #
      # @param [Hash] job_name, build_number or build_details object
      #
      def artifact_path(params)
        job_name      = params[:job_name]
        build_number  = params[:build_number] || 0
        build_details = params[:build_details]

        build_details = get_build_details(job_name, build_number) if build_details.nil?
        artifacts     = build_details['artifacts']
        artifact_paths = []

        if artifacts && artifacts.any?
          artifact_paths = artifacts.find_all{ |a| a.key?('relativePath') }
        end

        if artifact_paths.empty?
          raise "No artifacts found."
        end
        artifact_paths
      end
    end
  end
end