# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

require 'uri'
require 'net/http'
require 'thread'

module Buildr

  # Provides a collection of tasks and methods for using Jetty, specifically as a server
  # for testing your application.
  #
  # Build files should always start Jetty by invoking the #use task, typically as
  # a prerequisite. This task will start Jetty once during the build, and shut it down
  # when the build completes.
  #
  # If you want to keep Jetty running across builds, and look at error messages, you can
  # start Jetty in a separate console with:
  #   buildr jetty:start
  # To stop this instance of Jetty, simply kill the process (Ctrl-C) or run:
  #   buildr jetty:stop
  #
  # If you start Jetty separately from the build, the #use task will connect to that
  # existing server. Since you are using Jetty across several builds, you will want to
  # cleanup any mess created by each build. You can use the #setup and #teardown tasks,
  # which are called when Jetty is first used in the build, and when the build ends.
  class Jetty

    # Which version of Jetty we're using by default (change with options.jetty.version).
    VERSION = '9.4.6.v20170531'
    SLF4J_VERSION = '1.7.25'

    # Libraries used by Jetty.
    REQUIRES = [ "org.eclipse.jetty:jetty-server:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-webapp:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-http:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-util:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-io:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-servlet:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-security:jar:#{VERSION}",
      "org.eclipse.jetty:jetty-xml:jar:#{VERSION}",
      "org.slf4j:slf4j-api:jar:#{SLF4J_VERSION}",
      "org.slf4j:slf4j-simple:jar:#{SLF4J_VERSION}",
      "org.slf4j:jcl-over-slf4j:jar:#{SLF4J_VERSION}",
      'javax.servlet:javax.servlet-api:jar:3.1.0' ]

    Java.classpath <<  REQUIRES
    Java.classpath << File.dirname(__FILE__)

    # Default URL for Jetty (change with options.jetty.url).
    URL = "http://localhost:8080"

    class << self

      # :call-seq:
      #   instance() => Jetty
      #
      # Returns an instance of Jetty.
      def instance()
        @instance ||= Jetty.new("jetty", URL)
      end

    end

    def initialize(name, url) #:nodoc:
      @url = url
      namespace name do
        @setup = task("setup")
        @teardown = task("teardown")
        @use = task("use") { fire }
      end
    end

    # The URL for the Jetty server. Leave as is if you want to use the default server
    # (http://localhost:8080).
    attr_accessor :url

    # :call-seq:
    #    start(pipe?)
    #
    # Starts Jetty. This method does not return, it keeps the thread running until
    # Jetty is stopped. If you want to run Jetty parallel with other tasks in the build,
    # invoke the #use task instead.
    def start(sync = nil)
      begin
        puts "classpath #{Java.classpath.inspect}"
        port = URI.parse(url).port
        puts "Starting Jetty at http://localhost:#{port}" if verbose
        Java.load
        jetty = Java.org.apache.buildr.JettyWrapper.new(port)
        sync << "Started" if sync
        sleep # Forever
      rescue Interrupt # Stopped from console
      rescue Exception=>error
        puts "#{error.class}: #{error.message}"
      end
      exit! # No at_exit
    end

    # :call-seq:
    #    stop()
    #
    # Stops Jetty. Stops a server running in a separate process.
    def stop()
      uri = URI.parse(url)
      begin
        Net::HTTP.start(uri.host, uri.port) do |http|
          http.request_post "/buildr/stop", ""
        end
      rescue Errno::ECONNREFUSED
        # Expected if Jetty server not running.
      rescue EOFError
        # We get EOFError because Jetty is brutally killed.
      end
      puts "Jetty server stopped"
    end

    # :call-seq:
    #   running?() => boolean
    #
    # Returns true if it finds a running Jetty server that supports the Buildr
    # requests for deploying, stopping, etc.
    def running?()
      uri = URI.parse(url)
      begin
        Net::HTTP.start(uri.host, uri.port) do |http|
          response = http.request_get("/buildr/")
          response.is_a?(Net::HTTPSuccess) && response.body =~ /Alive/
        end
      rescue Errno::ECONNREFUSED, Errno::EBADF
        false
      end
    end

    # :call-seq:
    #   deploy(url, webapp) => path
    #
    # Deploy a WAR in the specified URL.
    def deploy(url, webapp)
      use.invoke
      uri = URI.parse(url)
      Net::HTTP.start(uri.host, uri.port) do |http|
        response = http.request_post("/buildr/deploy", "webapp=#{webapp}&path=#{uri.path}")
        if Net::HTTPOK === response && response.body =~ /Deployed/
          path = response.body.split[1]
          puts "Deployed #{webapp}, context path #{uri.path}" if trace?
          path
        else
          fail "Deployment failed: #{response}"
        end
      end
    end

    # :call-seq:
    #   undeploy(url) => boolean
    #
    # Undeploys a WAR from the specified URL.
    def undeploy(url)
      use.invoke
      uri = URI.parse(url)
      Net::HTTP.start(uri.host, uri.port) do |http|
        response = http.request_post("/buildr/undeploy", "path=#{uri.path}")
        if Net::HTTPOK === response && response.body =~ /Undeployed/
          true
        else
          fail "Deployment failed: #{response}"
        end
      end
    end

    # :call-seq:
    #   setup(*prereqs) => task
    #   setup(*prereqs) { |task| .. } => task
    #
    # This task executes when Jetty is first used in the build. You can use it to
    # deploy artifacts into Jetty.
    def setup(*prereqs, &block)
      @setup.enhance prereqs, &block
    end

    # :call-seq:
    #   teardown(*prereqs) => task
    #   teardown(*prereqs) { |task| .. } => task
    #
    # This task executes when the build is done. You can use it to undeploy artifacts
    # previously deployed into Jetty.
    def teardown(*prereqs, &block)
      @teardown.enhance prereqs, &block
    end

    # :call-seq:
    #   use(*prereqs) => task
    #   use(*prereqs) { |task| .. } => task
    #
    # If you intend to use Jetty, invoke this task. It will start a new instance of
    # Jetty and close it when the build is done. However, if you already have a server
    # running in the background (e.g. jetty:start), it will use that server and will
    # not close it down.
    def use(*prereqs, &block)
      @use.enhance prereqs, &block
    end

  protected

    # If you want to start Jetty inside the build, call this method instead of #start.
    # It will spawn a separate process that will run Jetty, and will stop Jetty when
    # the build ends. However, if you already started Jetty from the console (with
    # take jetty:start), it will use the existing instance without shutting it down.
    def fire()
      unless running?
        sync = Queue.new
        Thread.new { start sync }
        # Wait for Jetty to fire up before doing anything else.
        sync.pop == "Started" or fail "Jetty not started"
        puts "Jetty started" if verbose
        at_exit { stop }
      end
      @setup.invoke
      at_exit { @teardown.invoke }
    end

  end

  namespace "jetty" do
    desc "Start an instance of Jetty running in the background"
    task("start") { Jetty.instance.start }
    desc "Stop an instance of Jetty running in the background"
    task("stop") { Jetty.instance.stop }
  end

  # :call-seq:
  #   jetty() => Jetty
  #
  # Returns a Jetty object. You can use this to discover the Jetty#use task,
  # configure the Jetty#setup and Jetty#teardown tasks, deploy and undeploy to Jetty.
  def jetty()
    @jetty ||= Jetty.instance
  end

end
