# Encoding: utf-8
# IBM WebSphere Application Server Liberty Buildpack
# Copyright IBM Corp. 2013, 2019
#
# Licensed 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 'English'
require 'fileutils'
require 'liberty_buildpack/container'
require 'liberty_buildpack/container/common_paths'
require 'liberty_buildpack/container/container_utils'
require 'liberty_buildpack/container/feature_manager'
require 'liberty_buildpack/container/install_components'
require 'liberty_buildpack/container/optional_components'
require 'liberty_buildpack/container/services_manager'
require 'liberty_buildpack/container/web_xml_ext'
require 'liberty_buildpack/diagnostics/logger_factory'
require 'liberty_buildpack/repository/component_index'
require 'liberty_buildpack/repository/configured_item'
require 'liberty_buildpack/util'
require 'liberty_buildpack/util/cache/application_cache'
require 'liberty_buildpack/util/format_duration'
require 'liberty_buildpack/util/properties'
require 'liberty_buildpack/util/license_management'
require 'liberty_buildpack/util/location_resolver'
require 'liberty_buildpack/util/heroku'
require 'liberty_buildpack/util/xml_utils'
require 'open-uri'

module LibertyBuildpack::Container
  # Encapsulates the detect, compile, and release functionality for Liberty applications.
  class Liberty

    include LibertyBuildpack::Util

    # Creates an instance, passing in an arbitrary collection of options.
    #
    # @param [Hash] context the context that is provided to the instance
    # @option context [String] :app_dir the directory that the application exists in
    # @option context [Hash] :environment the environment variables available to the application
    # @option context [String] :java_home the directory that acts as +JAVA_HOME+
    # @option context [Array<String>] :java_opts an array that Java options can be added to
    # @option context [String] :lib_directory the directory that additional libraries are placed in
    # @option context [CommonPaths] :common_paths the set of paths common across components that components should reference
    # @option context [Hash] :vcap_application the information about the deployed application provided by the Cloud Controller
    # @option context [Hash] :vcap_services the bound services to the application provided by the Cloud Controller
    # @option context [Hash] :license_ids the licenses accepted by the user
    # @option context [Hash] :configuration the properties provided by the user
    def initialize(context)
      @logger = LibertyBuildpack::Diagnostics::LoggerFactory.get_logger
      @app_dir = context[:app_dir]
      @java_home = context[:java_home]
      @java_opts = context[:java_opts]
      @lib_directory = context[:lib_directory]
      @common_paths = context[:common_paths] || CommonPaths.new
      @configuration = context[:configuration]
      @vcap_services = Heroku.heroku? ? Heroku.new.generate_vcap_services(ENV) : context[:vcap_services]
      @vcap_application = context[:vcap_application]
      @license_id = context[:license_ids]['IBM_LIBERTY_LICENSE']
      @environment = context[:environment]
      unpack_apps
    end

    # Detects whether this application is a Liberty application.
    #
    # @return [String] returns +liberty-<version>+ if and only if the application has a server.xml, otherwise
    #                  returns +nil+
    def detect
      liberty_version = Liberty.find_liberty_item(@app_dir, @configuration)[0]
      if liberty_version || use_liberty_springboot?
        # set the relative path from '.liberty/usr/servers/defaultserver'
        @common_paths.relative_location = File.join(LIBERTY_HOME, USR_PATH, SERVERS_PATH, DEFAULT_SERVER)
        [liberty_type, liberty_id(liberty_version)]
      end
    end

    # Downloads and unpacks a Liberty instance
    #
    # @return [void]
    def compile
      Liberty.validate(@app_dir)
      @liberty_components_and_uris, @liberty_license = Liberty.find_liberty_files(@app_dir, @configuration)
      unless LibertyBuildpack::Util.check_license(@liberty_license, @license_id)
        print "\nYou have not accepted the IBM Liberty License.\n\nVisit the following uri:\n#{@liberty_license}\n\nExtract the license number (D/N:) and place it inside your manifest file as a ENV property e.g. \nENV: \n  IBM_LIBERTY_LICENSE: {License Number}.\n"
        raise
      end
      download_and_install_liberty
      link_application
      update_server_xml
      make_server_script_runnable
      download_and_install_features
      # Need to do minify here to have server_xml updated and applications and libs linked.
      minify_liberty if minify?
      overlay_java
      set_liberty_system_properties
      set_jdk_memory_configuration
      populate_class_cache if populate_class_cache_staging?
    end

    # Creates the command to run the Liberty application.
    #
    # @return [String] the command to run the application.
    def release
      jvm_options

      server_dir = ' wlp/usr/servers/' << server_name << '/'
      write_server_name
      runtime_vars_file =  server_dir + 'runtime-vars.xml'
      create_vars_string = File.join(LIBERTY_HOME, 'create_vars.rb') << runtime_vars_file << ' &&'
      create_jdk_memory_string = ContainerUtils.space(File.join(LIBERTY_HOME, 'calculate_memory.rb') << ' &&')
      skip_maxpermsize_string = ContainerUtils.space('WLP_SKIP_MAXPERMSIZE=true')
      java_home_string = ContainerUtils.space("JAVA_HOME=\"$PWD/#{@java_home}\"")
      wlp_user_dir_string = ContainerUtils.space('WLP_USER_DIR="$PWD/wlp/usr"')
      server_script_string = ContainerUtils.space("exec #{File.join(LIBERTY_HOME, 'bin', 'server')}")
      path_string = ContainerUtils.space('PATH=$PWD/.ruby/bin:$PATH') << ' &&'

      if File.exist?('/tmp/ruby')
        start_command = "#{path_string}#{create_vars_string}#{create_jdk_memory_string}#{skip_maxpermsize_string}#{java_home_string}#{wlp_user_dir_string}#{server_script_string} run #{server_name}"
      else
        start_command = "#{create_vars_string}#{create_jdk_memory_string}#{skip_maxpermsize_string}#{java_home_string}#{wlp_user_dir_string}#{server_script_string} run #{server_name}"
      end
      move_app

      start_command
    end

    private

    def unpack_apps
      server_xml = Liberty.server_xml(@app_dir)
      if Liberty.web_inf(@app_dir)
        # nothing to do
      elsif Liberty.meta_inf(@app_dir)
        wars = Dir.glob(File.expand_path(File.join(@app_dir, '*.war')))
        Liberty.expand_apps(wars)
      elsif server_xml
        server_path = File.dirname(server_xml)
        ears = Dir.glob("#{server_path}/**/*.ear")
        Liberty.expand_apps(ears)
        wars = Dir.glob("#{server_path}/**/*.war")
        Liberty.expand_apps(wars)
      end
    end

    def move_app
      if Liberty.liberty_directory(@app_dir)
        # Nothing to move
      elsif Liberty.server_directory(@app_dir)
        dest = File.join(@app_dir, '.wlp', USR_PATH, SERVERS_PATH, DEFAULT_SERVER)
        move_and_relink @app_dir, dest
        move_staging_logs dest, @app_dir
        FileUtils.mv File.join(@app_dir, '.wlp'), File.join(@app_dir, WLP_PATH)
        move_user_features
      else
        dest = File.join(@app_dir, '.wlp', USR_PATH, SERVERS_PATH)
        FileUtils.mkdir_p(dest)
        FileUtils.mv default_server_path, dest
        dest = File.join(dest, DEFAULT_SERVER, 'apps', myapp_name)
        FileUtils.rm_rf(dest)
        move_and_relink @app_dir, dest
        move_staging_logs dest, @app_dir
        FileUtils.mv File.join(@app_dir, '.wlp'), File.join(@app_dir, WLP_PATH)
        move_user_features
      end
    end

    def move_and_relink(src, dest)
      # Remember symlink targets
      links = Dir[File.join(src, '**', '*')].select { |file| File.symlink?(file) }
      linkmap = Hash[links.map { |link| [Pathname(link).relative_path_from(Pathname(src)), File.realpath(link)] }]
      FileUtils.mkdir_p(dest)
      FileUtils.mv Dir[File.join(src, '*')], dest
      # Relink symlinks to the targets
      linkmap.each do |link, target|
        file = File.join(dest, link)
        FileUtils.ln_sf(Pathname(target).relative_path_from(Pathname(File.dirname(file))), file)
      end
    end

    def move_staging_logs(src, dest)
      FileUtils.mkdir_p(File.join(dest, 'logs'))
      FileUtils.mv File.join(src, 'logs', 'staging_task.log'), File.join(dest, 'logs')
    end

    def move_user_features
      extension_dir = File.join(usr_dir, 'extension')
      FileUtils.mv(extension_dir, File.join(@app_dir, WLP_PATH, USR_PATH)) if Dir.exist?(extension_dir)
    end

    def jvm_options
      # disable 2-phase (XA) transactions via a -D option, as they are unsupported in
      # Liberty in the cloud (log files need a persistent location, and someone to
      # recover them).
      @java_opts << '-Dcom.ibm.tx.jta.disable2PC=true'

      # add existing options from the jvm.options file, if there is one, to the current
      # options, without duplicating options.
      jvm_options_src = File.join(current_server_dir, JVM_OPTIONS)
      if File.exist?(jvm_options_src)
        File.open(jvm_options_src, 'r') do |file|
          file.each_line do |line|
            line.chomp!
            @java_opts << line unless @java_opts.include?(line)
          end
        end
      end

      # re-write the file with all the options.
      FileUtils.mkdir_p File.dirname(jvm_options_src)
      jvm_options_file = File.new(jvm_options_src, 'w')
      jvm_options_file.puts(@java_opts)
      jvm_options_file.close

      # link from server runtime to the options file, if the options file isn't
      # already in the runtime defaultServer directory (if a server.xml was
      # pushed, jvm options will be in the same location, and both are linked
      # to from the runtime).
      if File.exist?(default_server_path) && !File.exist?(File.join(default_server_path, JVM_OPTIONS))
        default_server_pathname = Pathname.new(default_server_path)
        FileUtils.ln_sf(Pathname.new(jvm_options_src).relative_path_from(default_server_pathname), default_server_path)
      end
    end

    def minify?
      (@environment['minify'].nil? ? (@configuration['minify'] != false) : (@environment['minify'] != 'false')) && java_present?
    end

    def minify_liberty
      print '-----> Minifying Liberty ... '
      minify_start_time = Time.now
      Dir.mktmpdir do |root|
        minified_zip = File.join(root, 'minified.zip')
        minify_script_string = "JAVA_HOME=\"#{@app_dir}/#{@java_home}\" JVM_ARGS="" #{File.join(liberty_home, 'bin', 'server')} package #{server_name} --include=minify --archive=#{minified_zip} --os=-z/OS"
        # Make it quiet unless there're errors (redirect only stdout)
        minify_script_string << ContainerUtils.space('1>/dev/null')
        system(minify_script_string)
        # Update with minified version only if the generated file exists and not empty.
        if File.size? minified_zip
          ContainerUtils.unzip(minified_zip, root)
          if File.exist? icap_extension
            extensions_dir = File.join(root, WLP_PATH, 'etc', 'extensions')
            system("mkdir -p #{extensions_dir}", '&&', "cp #{icap_extension} #{extensions_dir}")
          end
          system("rm -rf #{liberty_home}/lib && mv #{root}/wlp/lib #{liberty_home}/lib")
          system("rm -rf #{root}/wlp")
          # Re-create sym-links for application and libraries.
          make_server_script_runnable
          print "(#{(Time.now - minify_start_time).duration}).\n\n"
        else
          print 'failed, will continue using unminified Liberty.\n\n'
        end
      end
    end

    def java_present?
      !@java_home.nil? && File.directory?(File.join(@app_dir, @java_home))
    end

    def set_liberty_system_properties
      resources_dir = File.expand_path(RESOURCES, File.dirname(__FILE__))
      create_vars_destination = File.join(liberty_home, 'create_vars.rb')
      FileUtils.cp(File.join(resources_dir, 'create_vars.rb'), create_vars_destination)
      File.chmod(0o755, create_vars_destination)
    end

    def set_jdk_memory_configuration
      memory_resources_dir = File.expand_path(MEMORY_RESOURCES, File.dirname(__FILE__))

      Dir.glob(File.join(memory_resources_dir, '/*')).select { |f| !File.directory?(f) }.each { |f| FileUtils.cp(f, File.join(liberty_home, File.basename(f))) }

      File.chmod(0o755, File.join(liberty_home, 'calculate_memory.rb'))
      File.chmod(0o755, File.join(liberty_home, 'memory_size.rb'))
      File.chmod(0o755, File.join(liberty_home, 'memory_bucket.rb'))
      File.chmod(0o755, File.join(liberty_home, 'memory_limit.rb'))
      File.chmod(0o755, File.join(liberty_home, 'memory_range.rb'))
      File.chmod(0o755, File.join(liberty_home, 'openjdk_memory_heuristic_factory.rb'))
      File.chmod(0o755, File.join(liberty_home, 'stack_memory_bucket.rb'))
      File.chmod(0o755, File.join(liberty_home, 'weight_balancing_memory_heuristic.rb'))
    end

    KEY_HTTP_PORT = 'port'.freeze

    RESOURCES = File.join('..', '..', '..', 'resources', 'liberty').freeze

    MEMORY_RESOURCES = File.join('..', '..', '..', 'resources', 'memory').freeze

    KEY_SUPPORT = 'support'.freeze

    LIBERTY_HOME = '.liberty'.freeze

    DEFAULT_SERVER = 'defaultServer'.freeze

    WLP_PATH = 'wlp'.freeze

    USR_PATH = 'usr'.freeze

    SERVERS_PATH = 'servers'.freeze

    SERVER_XML_GLOB = 'wlp/usr/servers/*/server.xml'.freeze

    SERVER_XML = 'server.xml'.freeze

    JVM_OPTIONS = 'jvm.options'.freeze

    WEB_INF = 'WEB-INF'.freeze

    META_INF = 'META-INF'.freeze

    BOOT_INF = 'BOOT-INF'.freeze

    MEMORY_CONFIG_FOLDER = '.memory_config/'.freeze

    SERVER_NAME_FILE = 'server_name_information'.freeze

    def update_server_xml
      server_xml = Liberty.server_xml(@app_dir)
      if server_xml
        update_provided_server_xml(server_xml)
      elsif Liberty.web_inf(@app_dir) || Liberty.meta_inf(@app_dir) || Liberty.boot_inf(@app_dir)
        check_default_features
        create_default_server_xml
      else
        raise 'Neither a server.xml nor WEB-INF directory nor a ear was found.'
      end
    end

    def check_default_features
      unless features_set?
        features = config_features(@configuration) || []
        puts format('-----> Warning: Liberty feature set is not specified. Using the default feature set: %s. For the best results, explicitly set the features via the JBP_CONFIG_LIBERTY environment variable or deploy the application as a server directory or packaged server with a custom server.xml file.', features)
      end
    end

    def features_set?
      conf_env = @environment['JBP_CONFIG_LIBERTY']
      unless conf_env.nil?
        begin
          value = YAML.load(conf_env)
          if !config_features(value).nil?
            return true
          elsif value.is_a?(Array)
            value.each do |item|
              return true unless config_features(item).nil?
            end
          end
        rescue SyntaxError
          return false
        end
      end
      false
    end

    def config_features(config)
      config['app_archive']['features'] if config.is_a?(Hash) && !config['app_archive'].nil?
    end

    def update_provided_server_xml(server_xml)
      # Preserve the original configuration before we start modifying it
      FileUtils.cp server_xml.to_s, "#{server_xml}.org"

      server_xml_doc = XmlUtils.read_xml_file(server_xml)

      # Perform inlining of includes prior to adding include for runtime-vars.xml
      # as the file may not exist yet.
      inline_includes(server_xml_doc, File.dirname(server_xml), LocationResolver.new(@app_dir, liberty_home, server_name))

      # add common settings
      update_server_xml_common(server_xml_doc, false)

      XmlUtils.write_formatted_xml_file(server_xml_doc, server_xml)
    end

    def create_default_server_xml
      server_xml_doc = REXML::Document.new('<server></server>')
      server_xml_doc.context[:attribute_quote] = :quote

      default_config = @configuration['app_archive']

      # create featureManager with configured features
      feature_manager = REXML::Element.new('featureManager', server_xml_doc.root)
      unless default_config.nil?
        features = default_config['features'] || []
        features.each do |feature|
          feature_element = REXML::Element.new('feature', feature_manager)
          feature_element.text = feature
        end
      end

      # create application
      application = REXML::Element.new('application', server_xml_doc.root)
      application.attributes['name'] = 'myapp'
      application.attributes['location'] = myapp_name
      application.attributes['type'] = myapp_type
      application.attributes['context-root'] = get_context_root || '/'

      # configure CDI 1.2 implicit bean archive scanning
      cdi = REXML::Element.new('cdi12', server_xml_doc.root)
      scan = default_config.nil? || default_config['implicit_cdi'].nil? ? false : default_config['implicit_cdi']
      cdi.add_attribute('enableImplicitBeanArchives', scan)

      # add common settings
      update_server_xml_common(server_xml_doc, true)

      filename = File.join(default_server_path, SERVER_XML)
      XmlUtils.write_formatted_xml_file(server_xml_doc, filename)
    end

    def update_server_xml_common(server_xml_doc, create)
      update_http_endpoint(server_xml_doc)
      update_web_container(server_xml_doc)
      # add runtime-vars.xml include to server.xml.
      add_runtime_vars(server_xml_doc)
      # Liberty logs must go into cf logs directory so cf logs command displays them.
      update_logs_dir(server_xml_doc)
      # Disable default Liberty Welcome page to avoid returning 200 response before app is ready.
      disable_welcome_page(server_xml_doc)
      # Disable application monitoring
      disable_application_monitoring(server_xml_doc)
      # Enable configuration (server.xml) monitoring
      update_config_monitoring_polling_interval(server_xml_doc)

      # Check if appstate ICAP feature can be used
      check_appstate_feature(server_xml_doc) if appstate_enabled?

      # Use native liberty springboot feature if enabled
      add_springboot(server_xml_doc) if use_liberty_springboot?

      # update config for services
      @services_manager.update_configuration(server_xml_doc, create, current_server_dir)
    end

    def use_liberty_springboot?
      spring_version = @environment['LIBERTY_NATIVE_SPRINGBOOT']
      return true unless spring_version.nil?
      false
    end

    def populate_class_cache_staging?
      populate_class_cache_value = @environment['LIBERTY_SKIP_POPULATE_CLASSCACHE']
      return false unless populate_class_cache_value.nil?
      true
    end

    def add_springboot(server_xml_doc)
      feature_managers = REXML::XPath.match(server_xml_doc, '/server/featureManager')
      if feature_managers.empty?
        feature_manager = REXML::Element.new('featureManager', server_xml_doc.root)
      else
        feature_manager = feature_managers[0]
      end
      spring_version = @environment['LIBERTY_NATIVE_SPRINGBOOT']
      springboot_feature = REXML::Element.new('feature', feature_manager)
      springboot_feature.text = format('springBoot-%s', spring_version)
      springboot_feature.text
    end

    def get_context_root
      ibm_web_xml = WebXmlExt.read(File.join(@app_dir, WEB_INF, 'ibm-web-ext.xml'))
      ibm_web_xml.get_context_root unless ibm_web_xml.nil?
    end

    def update_http_endpoint(server_xml_doc)
      endpoints = REXML::XPath.match(server_xml_doc, '/server/httpEndpoint')
      if endpoints.empty?
        endpoint = REXML::Element.new('httpEndpoint', server_xml_doc.root)
        endpoint.add_attribute('id', 'defaultHttpEndpoint')
      else
        endpoint = endpoints[0]
        endpoints.drop(1).each { |element| element.parent.delete_element(element) }
      end
      if appstate_enabled? && appstate_apps(server_xml_doc).size > 0
        endpoint.add_attribute('host', '127.0.0.1')
      else
        endpoint.add_attribute('host', '*')
      end
      endpoint.add_attribute('httpPort', "${#{KEY_HTTP_PORT}}")

      compression = REXML::XPath.match(server_xml_doc, '/server/compression')
      if compression.empty?
        endpoint.add_element('compression') if endpoint.elements['compression'].nil?
      end
      endpoint.delete_attribute('httpsPort')
    end

    def update_web_container(server_xml_doc)
      webcontainers = REXML::XPath.match(server_xml_doc, '/server/webContainer')
      if webcontainers.empty?
        webcontainer = REXML::Element.new('webContainer', server_xml_doc.root)
      else
        webcontainer = webcontainers[0]
      end
      webcontainer.add_attribute('trustHostHeaderPort', 'true')
      webcontainer.add_attribute('extractHostHeaderPort', 'true')
    end

    def add_runtime_vars(server_xml_doc)
      include_file = REXML::Element.new('include', server_xml_doc.root)
      include_file.add_attribute('location', 'runtime-vars.xml')
    end

    def update_logs_dir(server_xml_doc)
      elements = REXML::XPath.match(server_xml_doc, '/server/logging')
      if elements.empty?
        logging = REXML::Element.new('logging', server_xml_doc.root)
      else
        logging = elements.last
      end
      logging.add_attribute('logDirectory', '${application.log.dir}')
      logging.add_attribute('consoleLogLevel', 'INFO') if logging.attribute('consoleLogLevel').nil?
    end

    def update_config_monitoring_polling_interval(server_xml_doc)
      configs = REXML::XPath.match(server_xml_doc, '/server/config')
      if configs.empty?
        config = REXML::Element.new('config', server_xml_doc.root)
        config.add_attribute('updateTrigger', 'polled')
        config.add_attribute('monitorInterval', '60000ms')
      end
    end

    def disable_application_monitoring(server_xml_doc)
      application_monitors = REXML::XPath.match(server_xml_doc, '/server/applicationMonitor')
      if application_monitors.empty?
        application_monitor = REXML::Element.new('applicationMonitor', server_xml_doc.root)
        dropins_dir = File.join(current_server_dir, 'dropins')
        dropins_dir_populated = Dir.exist?(dropins_dir) && Dir.entries(dropins_dir).size > 2
        application_monitor.add_attribute('dropinsEnabled', dropins_dir_populated ? 'true' : 'false')
        application_monitor.add_attribute('updateTrigger', 'mbean')
      end
    end

    def disable_welcome_page(server_xml_doc)
      dispatchers = REXML::XPath.match(server_xml_doc, '/server/httpDispatcher')
      if dispatchers.empty?
        dispatcher = REXML::Element.new('httpDispatcher', server_xml_doc.root)
      else
        dispatcher = dispatchers[0]
        dispatchers.drop(1).each { |element| element.parent.delete_element(element) }
      end
      dispatcher.add_attribute('enableWelcomePage', 'false')
      dispatcher.add_attribute('trustedSensitiveHeaderOrigin', '*')
    end

    def appstate_enabled?
      config_enabled = @configuration['app_state'].nil? || @configuration['app_state']
      feature_present = File.file?(icap_extension)
      config_enabled && feature_present
    end

    def appstate_apps(server_xml_doc)
      apps = REXML::XPath.match(server_xml_doc, '/server/application | /server/webApplication | /server/enterpriseApplication')
      app_names = []
      apps.each do |app|
        app_names << app.attributes['name'] unless app.attributes['name'].nil?
      end
      app_names
    end

    def check_appstate_feature(server_xml_doc)
      apps = appstate_apps(server_xml_doc)
      if apps.size > 0
        # Add appstate-2.0 feature
        feature_managers = REXML::XPath.match(server_xml_doc, '/server/featureManager')
        if feature_managers.empty?
          feature_manager = REXML::Element.new('featureManager', server_xml_doc.root)
        else
          feature_manager = feature_managers[0]
        end
        appstate_feature = REXML::Element.new('feature', feature_manager)
        appstate_feature.text = 'appstate-2.0'

        # Set the apps to be monitored.
        appstate = REXML::Element.new('appstate2', server_xml_doc.root)
        appstate.add_attribute('appName', apps.join(', '))
      end
    end

    def make_server_script_runnable
      %w(server featureManager productInfo installUtility).each do |name|
        script = File.join(liberty_home, 'bin', name)
        File.chmod(0o755, script) if File.exist?(script)
      end
    end

    def server_name
      if Liberty.liberty_directory @app_dir
        candidates = Dir[File.join(@app_dir, WLP_PATH, USR_PATH, SERVERS_PATH, '*')]
        raise "Incorrect number of servers to deploy (expecting exactly one): #{candidates}" if candidates.size != 1
        File.basename(candidates[0])
      elsif Liberty.server_directory(@app_dir) || Liberty.web_inf(@app_dir) || Liberty.meta_inf(@app_dir) || Liberty.boot_inf(@app_dir)
        DEFAULT_SERVER
      else
        raise 'Could not find either a WEB-INF directory or a server.xml.'
      end
    end

    def server_name_file
      File.join(@app_dir, MEMORY_CONFIG_FOLDER, SERVER_NAME_FILE)
    end

    def write_server_name
      FileUtils.mkdir_p File.join(@app_dir, MEMORY_CONFIG_FOLDER)
      File.open(server_name_file, 'w') { |file| file.write(server_name) }
    end

    # Liberty download component names, as used in the component_index.yml file
    # pointed to by the index.yml file The index.yml file is
    # pointed to by the buildpack liberty.yml file.
    COMPONENT_LIBERTY_CORE   = 'liberty_core'.freeze
    COMPONENT_LIBERTY_EXT    = 'liberty_ext'.freeze

    def download_and_install_liberty
      # create a temporary directory where the downloaded files will be extracted to.
      Dir.mktmpdir do |root|
        FileUtils.rm_rf(liberty_home)
        FileUtils.mkdir_p(liberty_home)

        # download and extract the server to a temporary location.
        uri = @liberty_components_and_uris[COMPONENT_LIBERTY_CORE]
        raise 'No Liberty download defined in buildpack.' if uri.nil?
        download_and_unpack_archive(uri, root)

        # read opt-out of service bindings information from env (manifest.yml), and initialise
        # services manager, which will be used to list dependencies for any bound services.
        context = { app_dir: @app_dir, java_home: @java_home }
        @services_manager = ServicesManager.new(@vcap_services, runtime_vars_dir(root), @environment['services_autoconfig_excludes'], context)

        # if the liberty feature manager and repository are not being used to install server
        # features, download the required files from the various configured locations. If the
        # repository is being used it will install features later (after server.xml is updated).
        unless FeatureManager.enabled?(@configuration)
          # download and extract the extended server files to the same location, if required.
          if @services_manager.requires_liberty_extensions? || configured_feature_requires_component?(COMPONENT_LIBERTY_EXT)
            download_and_unpack_archive(uri, root) if (uri = @liberty_components_and_uris.delete(COMPONENT_LIBERTY_EXT))
          end
          # services may provide zips or esas or both. Query services to see what's needed
          # and download and install.
          install_list = InstallComponents.new
          @services_manager.get_required_esas(@liberty_components_and_uris, install_list)
          download_and_unpack_archives(install_list.zips, root)
          download_and_install_esas(install_list.esas, root)
        end

        # move the server to it's proper location.
        system "mv #{root}/wlp/* #{liberty_home}/"

        # configure icap extension, if required.
        system "sed -i -e 's|productInstall=wlp/|productInstall=#{LIBERTY_HOME}/|' #{icap_extension}" if File.file? icap_extension

        # install any services client jars required.
        @services_manager.install_client_jars(@liberty_components_and_uris, current_server_dir)
      end
    end

    def populate_class_cache
      print '-----> Populating class cache ... '
      populating_start_time = Time.now
      system "export PATH=$PATH:#{liberty_home}/../.java/jre/bin:#{liberty_home}/../.java/bin && #{liberty_home}/bin/server start #{server_name} >null"
      system "export PATH=$PATH:#{liberty_home}/../.java/jre/bin:#{liberty_home}/../.java/bin && #{liberty_home}/bin/server stop #{server_name} >null"
      system "export PATH=$PATH:#{liberty_home}/../.java/jre/bin:#{liberty_home}/../.java/bin && #{liberty_home}/bin/server start #{server_name} >null"
      system "export PATH=$PATH:#{liberty_home}/../.java/jre/bin:#{liberty_home}/../.java/bin && #{liberty_home}/bin/server stop #{server_name} >null"
      puts "(#{(Time.now - populating_start_time).duration})"
    end

    # is the given liberty component required ? It may be non-optional, in which
    # case it is required, or it may be optional, in which case it is required
    # if one of the features it supplies is requested in a server.xml.
    def configured_feature_requires_component?(component)
      list_configured_features_from_component(component).length > 0
    end

    def list_configured_features_from_component(component)
      feature_names = OptionalComponents.feature_names(component)
      if !feature_names
        # no such component
        []
      elsif (server_xml = Liberty.server_xml(@app_dir))
        feature_manager = FeatureManager.new(@app_dir, @java_home, @configuration)
        feature_names & feature_manager.get_features(server_xml)
      else
        # no server.xml supplied, so check default features.

        default_config = @configuration['app_archive']
        default_features = default_config.nil? ? [] : default_config['features'] || []

        # return an intersection of two arrays
        default_features & feature_names
      end
    end

    # download and install any features configured in server.xml that are not already
    # present in the liberty server by invoking featureManager with the list of all
    # features, and letting it download and install the missing features from the
    # liberty repository.
    def download_and_install_features
      server_xml = File.join(current_server_dir, SERVER_XML)
      feature_manager = FeatureManager.new(@app_dir, @java_home, @configuration)
      feature_manager.download_and_install_features(server_xml, liberty_home)
    end

    def download_and_unpack_archives(zips, root)
      zips.each do |entry|
        # each entry is an array of two entries, uri and optional directory string
        uri = entry[0]
        dir = entry[1]
        if dir.nil?
          dir = root
        else
          dir = File.join(@app_dir, dir) unless Pathname.new(dir).absolute?
          FileUtils.mkdir_p(dir)
        end
        download_and_unpack_archive(uri, dir)
      end
    end

    # This method unpacks an archive file. Supported archive types are .zip, .jar, tar.gz and tgz.
    # WARNING: Do not use this method to download archive files that should not be unzipped, such as client driver jars.
    # For each downloaded file, there is a corresponding cached, etag, last_modified, and lock extension.
    def download_and_unpack_archive(uri, root)
      # all file types filtered here should be handled inside block.
      if uri.end_with?('.tgz', '.tar.gz', '.zip', 'jar')
        if uri.include? '://'
          print "-----> Downloading from #{uri} ... "
        else
          filename = File.basename(uri)
          print "-----> Retrieving #{filename} ... "
        end
        download_start_time = Time.now
        LibertyBuildpack::Util::Cache::ApplicationCache.new.get(uri) do |file|
          puts "(#{(Time.now - download_start_time).duration})"
          install_archive(file, uri, root)
        end
      else
        # shouldn't happen, expect index.yml or component_index.yml to always
        # name files that can be handled here.
        puts "Unknown file type, not downloaded, at #{uri}"
      end
    end

    def install_archive(file, uri, root)
      print '         Installing archive ... '
      install_start_time = Time.now
      if uri.end_with?('.zip', 'jar')
        ContainerUtils.unzip(file.path, root)
        puts "(#{(Time.now - install_start_time).duration})"
      elsif uri.end_with?('tar.gz', '.tgz')
        system "tar -zxf #{file.path} -C #{root} 2>&1"
        puts "(#{(Time.now - install_start_time).duration})"
      else
        # shouldn't really happen
        puts "Unknown file type, not installed, at #{uri}."
      end
    end

    def download_and_install_esas(esas, root)
      esas.each do |esa|
        # each esa is an array of two entries, uri and options string
        uri = esa[0]
        options = esa[1]
        if uri.include? '://'
          print "-----> Downloading from #{uri} ... "
        else
          filename = File.basename(uri)
          print "-----> Retrieving #{filename} ... "
        end
        download_start_time = Time.now
        # for each downloaded file, there is a corresponding cached, etag, last_modified, and lock extension
        LibertyBuildpack::Util::Cache::ApplicationCache.new.get(uri) do |file|
          puts "(#{(Time.now - download_start_time).duration})."
          install_esa(file, options, root)
        end
      end
    end

    def install_esa(file, options, root)
      print '         Installing feature ... '
      install_start_time = Time.now
      # setup the command and options
      cmd = File.join(root, WLP_PATH, 'bin', 'featureManager')
      script_string = "JAVA_HOME=\"#{@app_dir}/#{@java_home}\" JVM_ARGS="" #{cmd} install #{file.path} #{options} 2>&1"
      output = `#{script_string}`
      if $CHILD_STATUS.to_i != 0
        puts 'FAILED'
        puts output.to_s
      else
        puts "(#{(Time.now - install_start_time).duration})."
      end
    end

    def inline_includes(server_xml_doc, server_xml_dir, location_resolver)
      REXML::XPath.each(server_xml_doc, '/server/include') do |element|
        location = element.attributes['location']
        optional = element.attributes['optional']
        if location.start_with? 'http:'
          raise 'Configuration files accessible via HTTP are not supported.'
        else
          location = location_resolver.absolute_path(location, server_xml_dir)
          if File.exist? location
            included_xml_doc = XmlUtils.read_xml_file(location)
            inline_includes included_xml_doc, File.dirname(location), location_resolver
            included_xml_doc.root.elements.each { |nested| server_xml_doc.root.insert_after element, nested }
            server_xml_doc.root.delete_element element
          elsif optional !~ /^true$/i
            raise "Configuration file could not be located: #{location}"
          end
        end
      end
    end

    # Liberty, features and driver jars are downloaded as a number of separate archives and .esa files.
    #
    # Return a map of component name to uri string.
    def self.find_liberty_files(app_dir, configuration)
      config_uri, license = Liberty.find_liberty_item(app_dir, configuration).drop(1)
      # Back to the future. Temporary hack to handle all-in-one liberty core for open source buildpack while the repository is being restructured.
      if config_uri.end_with?('.jar', '.zip')
        components_and_uris = { COMPONENT_LIBERTY_CORE => config_uri }
      else
        components_and_uris = LibertyBuildpack::Repository::ComponentIndex.new(config_uri).components
      end
      raise "Failed to locate a repository containing a component_index and installable components using uri #{config_uri}." if components_and_uris.nil?
      [components_and_uris, license]
    end

    # Reads the contents of the index file in the Liberty Repository's root to return the matching version, artifact uri, and license
    # of the item that matches the specified version criteria in the buildpack's config file.
    #
    # Returns the version, artifact uri, and license of the requested item in the index file
    def self.find_liberty_item(app_dir, configuration)
      if server_xml(app_dir) || web_inf(app_dir) || meta_inf(app_dir) || boot_inf(app_dir)
        version, entry = LibertyBuildpack::Repository::ConfiguredItem.find_item(configuration) do |candidate_version|
          raise "Malformed Liberty version #{candidate_version}: too many version components" if candidate_version[4]
        end
        if entry.is_a?(Hash)
          type = runtime_type(configuration)
          raise "Runtime type not supported: #{type}" if entry[type].nil?
          return version, entry[type], entry['license']
        else
          return version, entry, nil
        end
      else
        return nil, nil, nil
      end
    rescue => e
      raise RuntimeError, "Liberty container error: #{e.message}", e.backtrace
    end

    def self.runtime_type(configuration)
      type = configuration['type']
      if type.nil? || type.casecmp('webProfile6') == 0
        'uri'
      else
        type
      end
    end

    def liberty_type
      if Liberty.web_inf(@app_dir)
        'WAR'
      elsif Liberty.meta_inf(@app_dir)
        'EAR'
      elsif Liberty.liberty_directory(@app_dir)
        'SVR-PKG'
      elsif Liberty.server_directory(@app_dir)
        'SVR-DIR'
      else
        'unknown'
      end
    end

    def liberty_id(version)
      "liberty-#{version}"
    end

    def link_application
      if Liberty.liberty_directory(@app_dir)
        # Server package. We will delete the .liberty/usr directory and link in the wlp/usr directory from the server package as the usr directory. Copy user esas from
        # .liberty/usr over to wlp/usr before the delete.
        copy_user_features
        FileUtils.rm_rf(usr_dir)
        FileUtils.mkdir_p(liberty_home)
        FileUtils.ln_sf(Pathname.new(File.join(@app_dir, WLP_PATH, USR_PATH)).relative_path_from(Pathname.new(liberty_home)), liberty_home)
      elsif Liberty.server_directory(@app_dir)
        FileUtils.rm_rf(default_server_path)
        FileUtils.mkdir_p(default_server_path)
        default_server_pathname = Pathname.new(default_server_path)
        Pathname.glob(File.join(@app_dir, '*')) do |file|
          FileUtils.ln_sf(file.relative_path_from(default_server_pathname), default_server_path)
        end
      else
        FileUtils.rm_rf(myapp_dir)
        FileUtils.mkdir_p(myapp_dir)
        myapp_pathname = Pathname.new(myapp_dir)
        Pathname.glob(File.join(@app_dir, '*')) do |file|
          FileUtils.ln_sf(file.relative_path_from(myapp_pathname), myapp_dir)
        end
      end
    end

    def copy_user_features
      return unless Dir.exist?(features_dir)
      FileUtils.mkdir_p(File.join(@app_dir, WLP_PATH, USR_PATH, 'extension', 'lib', 'features'))
      output = `cp #{features_dir}/*.mf #{@app_dir}/wlp/usr/extension/lib/features`
      @logger.warn("copy_user_features copy manifests returned #{output}") if $CHILD_STATUS.to_i != 0
      output = `cp #{extension_lib_dir}/*.jar #{@app_dir}/wlp/usr/extension/lib`
      @logger.warn("copy_user_features copy jars returned #{output}") if $CHILD_STATUS.to_i != 0
    end

    def overlay_java
      server_xml_path =  Liberty.liberty_directory(@app_dir)
      if server_xml_path # server package (zip) push
        path_start = File.dirname(server_xml_path)
        ContainerUtils.overlay_java(path_start, @app_dir)
      else # WAR or server directory push
        ContainerUtils.overlay_java(@app_dir, @app_dir)
      end
    end

    def myapp_type
      if use_liberty_springboot?
        'spring'
      else
        Liberty.web_inf(@app_dir) ? 'war' : 'ear'
      end
    end

    def myapp_name
      "myapp.#{myapp_type}"
    end

    def myapp_dir
      File.join(apps_dir, myapp_name)
    end

    def apps_dir
      File.join(default_server_path, 'apps')
    end

    def servers_directory
      File.join(usr_dir, SERVERS_PATH)
    end

    def shared_dir
      File.join(usr_dir, 'shared')
    end

    def shared_resources_dir
      File.join(shared_dir, 'resources')
    end

    def extension_lib_dir
      File.join(usr_dir, 'extension', 'lib')
    end

    def features_dir
      File.join(extension_lib_dir, 'features')
    end

    def default_server_path
      File.join(servers_directory, DEFAULT_SERVER)
    end

    def usr_dir
      File.join(liberty_home, USR_PATH)
    end

    def liberty_home
      File.join(@app_dir, LIBERTY_HOME)
    end

    def icap_extension
      File.join(liberty_home, 'etc', 'extensions', 'icap.properties')
    end

    def self.web_inf(app_dir)
      web_inf = File.join(app_dir, WEB_INF)
      File.directory?(File.join(app_dir, WEB_INF)) && !main_class?(app_dir) ? web_inf : nil
    end

    def self.main_class?(app_dir)
      manifest_file = File.join(app_dir, 'META-INF/MANIFEST.MF')
      if File.exist?(manifest_file)
        props = LibertyBuildpack::Util::Properties.new(manifest_file)
        main_class = props['Main-Class']
        !main_class.nil?
      else
        false
      end
    end

    def self.boot_inf(app_dir)
      boot_inf = File.join(app_dir, BOOT_INF)
      File.directory?(File.join(app_dir, BOOT_INF)) ? boot_inf : nil
    end

    def self.meta_inf(app_dir)
      # return nil if META-INF directory doesn't exist. This mimics behavior of previous implementation.
      meta_inf = File.join(app_dir, META_INF)
      return nil if File.directory?(meta_inf) == false
      # To mimic the behavior of the previous (flawed) implementatation, from here on out we only return nil if we can determine it's a jar
      manifest_file = File.join(app_dir, META_INF, 'MANIFEST.MF')
      return meta_inf if File.exist?(manifest_file) == false
      props = LibertyBuildpack::Util::Properties.new(manifest_file)
      main_class = props['Main-Class']
      main_class.nil? ? meta_inf : nil
    end

    def self.server_directory(server_dir)
      server_xml = File.join(server_dir, SERVER_XML)
      File.file? server_xml ? server_xml : nil
    end

    def self.liberty_directory(app_dir)
      candidates = Dir[File.join(app_dir, SERVER_XML_GLOB)]
      if candidates.size > 1
        raise "Incorrect number of servers to deploy (expecting exactly one): #{candidates}"
      end
      candidates.any? ? candidates[0] : nil
    end

    def self.validate(app_dir)
      bin = File.join(app_dir, WLP_PATH, 'bin')
      dir = File.exist? bin
      if dir
        print "\nThe pushed packaged server contains runtime binaries. Use the command 'server package --include=usr' to package the server without the runtime binaries.\n"
        raise "The pushed packaged server contains runtime binaries. Use the command 'server package --include=usr' to package the server without the runtime binaries."
      end
      if Liberty.server_directory(app_dir) && (Liberty.web_inf(app_dir) || Liberty.meta_inf(app_dir) || Liberty.boot_inf(app_dir))
        print "\nWAR and EAR files cannot contain a server.xml file in the root directory.\n"
        raise 'WAR and EAR files cannot contain a server.xml file in the root directory.'
      end
    end

    def self.server_xml(app_dir)
      deep_candidates = Dir[File.join(app_dir, SERVER_XML_GLOB)]
      shallow_candidates = Dir[File.join(app_dir, SERVER_XML)]
      candidates = deep_candidates.concat shallow_candidates
      if candidates.size > 1
        raise "Incorrect number of servers to deploy (expecting exactly one): #{candidates}"
      end
      candidates.any? ? candidates[0] : nil
    end

    def current_server_dir
      if Liberty.liberty_directory @app_dir
        # packaged server use case. Push a server zip.
        File.join(@app_dir, WLP_PATH, USR_PATH, SERVERS_PATH, server_name)
      elsif Liberty.server_directory @app_dir
        # unpackaged server.xml use case. Push from a server directory
        @app_dir
      else
        # push web app use case.
        default_server_path
      end
    end

    def runtime_vars_dir(root)
      if Liberty.liberty_directory @app_dir
        # packaged server use case. create runtime-vars in the server directory.
        File.join(@app_dir, WLP_PATH, USR_PATH, SERVERS_PATH, server_name)
      elsif Liberty.server_directory @app_dir
        # unpackaged server.xml use case, push directory. create runtime-vars in the @app_dir
        @app_dir
      else
        # push web app use case. create runtime-vars in the temp staging area, will get copied at end of staging
        File.join(root, WLP_PATH, USR_PATH, SERVERS_PATH, DEFAULT_SERVER)
      end
    end

    def self.expand_apps(apps)
      apps.each do |app|
        next unless File.file? app
        temp_directory = "#{app}.tmp"
        ContainerUtils.unzip(app, temp_directory)
        File.delete(app)
        File.rename(temp_directory, app)
      end
    end

  end

end
