#!/usr/bin/env ruby

require 'optparse'
include FileTest
require 'fileutils'
include FileUtils
require 'set'
$:.unshift('/usr/share/java/java-config')
require 'java-config.rb'

ALT_CMD = '/usr/sbin/alternatives'

CP_DIR = '/etc/java/classpath'
EXT_DIR = '/usr/share/java/ext'
JNI_DIR = '/usr/lib/java/jni'

CONF_ALT_DIR = '/etc/java/alternatives'
CONF_EXT_DIR = '/etc/java/ext'
CONF_JNI_DIR = '/etc/java/jni'

def ensure_privilege
  Process.uid == 0 && Process.gid == 0 or raise SecurityError.new('not a privileged user.')
end

def absolute_path(name, parent)
  /\A\// =~ name ? name : File.join(parent, name)
end

def run_command(command_array)
  $stderr.puts '+%s' % [ command_array.join(' ') ] if $verbose
  fork do
    exec *command_array
  end
end

def install_alternatives(metainfo)
  ensure_privilege
  YAML.load(open(absolute_path(metainfo, CONF_ALT_DIR))).each do |alt|
    command = [ ALT_CMD, '--install', alt.link, alt.name, alt.path, alt.priority.to_s ]
    unless alt.slaves.nil?
      alt.slaves.each do |slave|
	command += [ '--slave', slave.link, slave.name, slave.path ]
      end
    end
    run_command(command)
  end
end

def remove_alternatives(metainfo)
  ensure_privilege
  YAML.load(open(absolute_path(metainfo, CONF_ALT_DIR))).each do |alt|
    command = [ ALT_CMD, '--remove', alt.name, alt.path ]
    run_command(command)
  end 
end

def set_alternatives(metainfo)
  ensure_privilege
  YAML.load(open(absolute_path(metainfo, CONF_ALT_DIR))).each do |alt|
    command = [ ALT_CMD, '--set', alt.name, alt.path ]
    run_command(command)
  end
end

def list_alternatives
  puts Dir["#{CONF_ALT_DIR}/*"].collect {|n| File.basename(n) }.sort
end

def classpath(names)
  info = {}
  missing = Set.new unless $strict
  until names.empty?
    names.each do |n|
      begin
	i = YAML.load(open(absolute_path(n, CP_DIR)))
        i.depends = [] if i.depends.nil? 
        i.depends = [ i.depends ] if i.depends.is_a?(String)
	info[n] = i
      rescue
        if $strict
          $stderr.puts("`#{n}' not found.")
          exit 1
        else
          missing << n 
          $stderr.puts("WARNING: `#{n}' is missing.") if $verbose
        end
      end
    end
    info.values.each {|i| i.depends.reject! {|d| missing.include?(d) }}
    unresolved = info.values.reject {|u| u.depends.all? {|n| info.include?(n) }}
    names = unresolved.collect {|u| u.depends }.flatten
  end
  topology = {}
  info.each {|n,i| topology[n] = i.depends }
  topology.tsort.collect {|name| info[name].classpath }.join(':')
end

def list_classpath
  puts Dir["#{CP_DIR}/*"].collect {|n| File.basename(n) }.sort
end

def remove_extensions(dir)
  remove_links(Dir["#{dir}/*.jar"], EXT_DIR)
end

def remove_native_libraries(dir)
  remove_links(Dir["#{dir}/*.so"], JNI_DIR)
end

def remove_links(files, whence)
  rm_f(files.select {|f| symlink?(f) and /\A#{whence}/o =~ File.readlink(f) }, :verbose => $verbose)
end

def update_extensions
  update_links(Dir["#{CONF_EXT_DIR}/*"], Dir["#{EXT_DIR}/*.jar"], EXT_DIR)
end

def update_native_libraries
  update_links(Dir["#{CONF_JNI_DIR}/*"], Dir["#{JNI_DIR}/*.so"], JNI_DIR)
end

def update_links(dirs, files, whence)
  ensure_privilege
  dirs.each do |dir|
    remove_links(Dir["#{dir}/*"], whence)
    files.each {|f| ln_sf(f, dir, :verbose => $verbose) }
  end
end

$quiet = false
$verbose = false
$strict = false

ARGV.options do |q|
  q.on('-q', '--[no-]quiet', 'run quietly.') do |$quiet|
    $verbose = false
  end
  q.on('-v', '--[no-]verbose', 'run verbosely.') do |$verbose|
    $quiet = false
  end
  q.on('-s', '--[no-]strict-classpath', 'resolve classpath strictly.') {|$strict|}
  q.on('--install-alternatives=METAINFO', 'install alternatives using given METAINFO.') do |metainfo|
    install_alternatives(metainfo)
  end
  q.on('--set-alternatives=METAINFO', 'set alternatives using given METAINFO.') do |metainfo|
    set_alternatives(metainfo)
  end
  q.on('--remove-alternatives=METAINFO', 'uninstall alternatives using given METAINFO.') do |metainfo|
    remove_alternatives(metainfo)
  end
  q.on('--list-alternatives', 'list all metainfo of installed Java alternatives.') do |metainfo|
    list_alternatives
  end
  q.on('--classpath=NAMES', Array, 'print :-concatenated classpath fragment for NAMES.') do |names|
    puts classpath(names)
  end
  q.on('--list-classpath', 'list all classpath names.') do
    list_classpath
  end
  q.on('--remove-extensions=PACKAGE', 'remove symbolic links to extension libraries from extension library from java package PACKAGE.') do |package|
    remove_extensions(File.join(CONF_EXT_DIR, package))
  end
  q.on('--update-extensions', 'update symbolic links to extension libraries.') do
    update_extensions
  end

  q.on('--remove-natives=PACKAGE', 'remove symbolic links to native libraries from native library directory from java package PACKAGE.') do |package|
    remove_extensions(File.join(CONF_JNI_DIR, package))
  end
  q.on('--update-natives', 'update symbolic links to native libraries.') do
    update_extensions
  end
  q.parse!
end

