package sbt
package ivyint

import java.lang.reflect.Field
import java.lang.reflect.Method
import org.apache.ivy.util.Credentials
import org.apache.ivy.util.Message
import org.apache.ivy.util.url.IvyAuthenticator
import org.apache.ivy.util.url.CredentialsStore

 * Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when
 * credentials don't line up.
object ErrorMessageAuthenticator {
  private var securityWarningLogged = false
  private def originalAuthenticator: Option[Authenticator] = {
    try {
      val f = classOf[Authenticator].getDeclaredField("theAuthenticator");
    } catch {
      // TODO - Catch more specific errors.
      case t: Throwable =>
        Message.debug("Error occurred while getting the original authenticator: " + t.getMessage)
  private lazy val ivyOriginalField = {
    val field = classOf[IvyAuthenticator].getDeclaredField("original")
  // Attempts to get the original authenticator form the ivy class or returns null.
  private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = {
    // Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run.
    def installIntoIvyImpl(original: Option[Authenticator]): Unit = {
      val newOriginal = new ErrorMessageAuthenticator(original)
      ivyOriginalField.set(ivy, newOriginal)
    try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match {
      case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again.
      case originalOpt => installIntoIvyImpl(originalOpt)
    } catch {
      case t: Throwable => 
        Message.debug("Error occurred will trying to install debug messages into Ivy Authentication" + t.getMessage)
  /** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */
  def install() {
    // Actually installs the error message authenticator.
    def doInstall(original: Option[Authenticator]): Unit =
      try Authenticator.setDefault(new ErrorMessageAuthenticator(original))
      catch {
        case e: SecurityException if !securityWarningLogged =>
          securityWarningLogged = true;
          Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. "
                     + "Helpful debug messages disabled!");
     // We will try to use the original authenticator as backup authenticator.
     // Since there is no getter available, so try to use some reflection to
     // obtain it. If that doesn't work, assume there is no original authenticator
     def doInstallIfIvy(original: Option[Authenticator]): Unit =
       original match {
         case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed
         case Some(ivy: IvyAuthenticator) => installIntoIvy(ivy)
         case original => doInstall(original)
 * An authenticator which just delegates to a previous authenticator and issues *nice* 
 * error messages on failure to find credentials. 
 * Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to 
 * install this one at some point and eventually ivy will capture it and use it.
private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator {

    protected override def getPasswordAuthentication(): PasswordAuthentication = {
        // We're guaranteed to only get here if Ivy's authentication fails
        if (!isProxyAuthentication) {
          val host = getRequestingHost
          // TODO - levenshtein distance "did you mean" message.
          Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].")
          val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty)
          if(!configuredRealms.isEmpty) {
            Message.error(s"  Is one of these realms mispelled for host [${host}]:")
            configuredRealms foreach { realm =>
              Message.error(s"  * ${realm}")
        // TODO - Maybe we should work on a helpful proxy message...
        // TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it.
        //        or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be).
        // Grabs the authentication that would have been provided had we not been installed...
        def originalAuthentication: Option[PasswordAuthentication] = {
          try Option(Authenticator.requestPasswordAuthentication(
          finally Authenticator.setDefault(this)

    /** Returns true if this authentication if for a proxy and not for an HTTP server.
     *  We want to display different error messages, depending.
    private def isProxyAuthentication: Boolean =
       getRequestorType == Authenticator.RequestorType.PROXY