/*
 * Copyright (c) 1999, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0, which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * This Source Code may also be made available under the following Secondary
 * Licenses when the conditions for such availability set forth in the
 * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
 * version 2 with the GNU Classpath Exception, which is available at
 * https://www.gnu.org/software/classpath/license.html.
 *
 * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
 */

package com.sun.ts.lib.util.sec.misc;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.Enumeration;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;
import java.net.URL;
import java.net.MalformedURLException;
import com.sun.ts.lib.util.sec.net.www.ParseUtil;

/**
 * <p>
 * This class checks dependent extensions a particular jar file may have
 * declared through its manifest attributes.
 * </p>
 * Jar file declared dependent extensions through the extension-list attribute.
 * The extension-list contains a list of keys used to fetch the other attributes
 * describing the required extension. If key is the extension key declared in
 * the extension-list attribute, the following describing attribute can be found
 * in the manifest : key-Extension-Name: (Specification package name)
 * key-Specification-Version: (Specification-Version)
 * key-Implementation-Version: (Implementation-Version)
 * key-Implementation-Vendor-Id: (Imlementation-Vendor-Id)
 * key-Implementation-Version: (Implementation version) key-Implementation-URL:
 * (URL to download the requested extension)
 * <p>
 * This class also maintain versioning consistency of installed extensions
 * dependencies declared in jar file manifest.
 * </p>
 * 
 * @author Jerome Dochez
 */
public class ExtensionDependency {

  /* Callbak interfaces to delegate installation of missing extensions */
  private static Vector providers;

  /**
   * <p>
   * Register an ExtensionInstallationProvider. The provider is responsible for
   * handling the installation (upgrade) of any missing extensions.
   * </p>
   * 
   * @param eip
   *          ExtensionInstallationProvider implementation
   */
  public synchronized static void addExtensionInstallationProvider(
      ExtensionInstallationProvider eip) {
    if (providers == null) {
      providers = new Vector();
    }
    providers.add(eip);
  }

  /**
   * <p>
   * Unregister a previously installed installation provider
   * </p>
   */
  public synchronized static void removeExtensionInstallationProvider(
      ExtensionInstallationProvider eip) {
    providers.remove(eip);
  }

  /**
   * <p>
   * Checks the dependencies of the jar file on installed extension.
   * </p>
   * 
   * @param jarFile
   *          containing the attriutes declaring the dependencies
   */
  public static boolean checkExtensionsDependencies(JarFile jar) {
    if (providers == null) {
      // no need to bother, nobody is registered to install missing
      // extensions
      return true;
    }

    try {
      ExtensionDependency extDep = new ExtensionDependency();
      return extDep.checkExtensions(jar);
    } catch (ExtensionInstallationException e) {
      debug(e.getMessage());
    }
    return false;
  }

  /*
   * Check for all declared required extensions in the jar file manifest.
   */
  protected boolean checkExtensions(JarFile jar)
      throws ExtensionInstallationException {
    Manifest man;
    try {
      man = jar.getManifest();
    } catch (IOException e) {
      return false;
    }

    if (man == null) {
      // The applet does not define a manifest file, so
      // we just assume all dependencies are satisfied.
      return true;
    }

    boolean result = true;
    Attributes attr = man.getMainAttributes();
    if (attr != null) {
      // Let's get the list of declared dependencies
      String value = attr.getValue(Name.EXTENSION_LIST);
      if (value != null) {
        StringTokenizer st = new StringTokenizer(value);
        // Iterate over all declared dependencies
        while (st.hasMoreTokens()) {
          String extensionName = st.nextToken();
          debug("The file " + jar.getName() + " appears to depend on "
              + extensionName);
          // Sanity Check
          String extName = extensionName + "-" + Name.EXTENSION_NAME.toString();
          if (attr.getValue(extName) == null) {
            debug("The jar file " + jar.getName() + " appers to depend on "
                + extensionName + " but does not define the " + extName
                + " attribute in its manifest ");

          } else {
            if (!checkExtension(extensionName, attr)) {
              debug("Failed installing " + extensionName);
              result = false;
            }
          }
        }
      } else {
        debug("No dependencies for " + jar.getName());
      }
    }
    return result;
  }

  /*
   * <p> Check that a particular dependency on an extension is satisfied. </p>
   * 
   * @param extensionName is the key used for the attributes in the manifest
   * 
   * @param attr is the attributes of the manifest file
   *
   * @return true if the dependency is satisfied by the installed extensions
   */
  protected synchronized boolean checkExtension(final String extensionName,
      final Attributes attr) throws ExtensionInstallationException {
    debug("Checking extension " + extensionName);
    if (checkExtensionAgainstInstalled(extensionName, attr))
      return true;

    debug("Extension not currently installed ");
    ExtensionInfo reqInfo = new ExtensionInfo(extensionName, attr);
    return installExtension(reqInfo, null);
  }

  /*
   * <p> Check if a particular extension is part of the currently installed
   * extensions. </p>
   * 
   * @param extensionName is the key for the attributes in the manifest
   * 
   * @param attr is the attributes of the manifest
   *
   * @return true if the requested extension is already installed
   */
  boolean checkExtensionAgainstInstalled(String extensionName, Attributes attr)
      throws ExtensionInstallationException {
    File fExtension = checkExtensionExists(extensionName);

    if (fExtension != null) {
      // Extension already installed, just check against this one
      try {
        if (checkExtensionAgainst(extensionName, attr, fExtension))
          return true;
      } catch (FileNotFoundException e) {
        debugException(e);
      } catch (IOException e) {
        debugException(e);
      }
      return false;

    } else {
      // Not sure if extension is already installed, so check all the
      // installed extension jar files to see if we get a match

      File[] installedExts;

      try {
        // Get the list of installed extension jar files so we can
        // compare the installed versus the requested extension
        installedExts = getInstalledExtensions();
      } catch (IOException e) {
        debugException(e);
        return false;
      }

      for (int i = 0; i < installedExts.length; i++) {
        try {
          if (checkExtensionAgainst(extensionName, attr, installedExts[i]))
            return true;
        } catch (FileNotFoundException e) {
          debugException(e);
        } catch (IOException e) {
          debugException(e);
          // let's continue with the next installed extension
        }
      }
    }
    return false;
  }

  /*
   * <p> Check if the requested extension described by the attributes in the
   * manifest under the key extensionName is compatible with the jar file. </p>
   *
   * @param extensionName key in the attibute list
   * 
   * @param attr manifest file attributes
   * 
   * @param file installed extension jar file to compare the requested extension
   * against.
   */
  protected boolean checkExtensionAgainst(String extensionName, Attributes attr,
      final File file) throws IOException, FileNotFoundException,
      ExtensionInstallationException {

    debug("Checking extension " + extensionName + " against " + file.getName());

    // Load the jar file ...
    Manifest man;
    try {
      man = (Manifest) AccessController
          .doPrivileged(new PrivilegedExceptionAction() {
            public Object run() throws IOException, FileNotFoundException {
              if (!file.exists())
                throw new FileNotFoundException(file.getName());
              JarFile jarFile = new JarFile(file);
              return jarFile.getManifest();
            }
          });
    } catch (PrivilegedActionException e) {
      if (e.getException() instanceof FileNotFoundException)
        throw (FileNotFoundException) e.getException();
      throw (IOException) e.getException();
    }

    // Construct the extension information object
    ExtensionInfo reqInfo = new ExtensionInfo(extensionName, attr);
    debug("Requested Extension : " + reqInfo);

    int isCompatible = ExtensionInfo.INCOMPATIBLE;
    ExtensionInfo instInfo = null;

    if (man != null) {
      Attributes instAttr = man.getMainAttributes();
      if (instAttr != null) {
        instInfo = new ExtensionInfo(null, instAttr);
        debug("Extension Installed " + instInfo);
        isCompatible = instInfo.isCompatibleWith(reqInfo);
        switch (isCompatible) {
        case ExtensionInfo.COMPATIBLE:
          debug("Extensions are compatible");
          return true;

        case ExtensionInfo.INCOMPATIBLE:
          debug("Extensions are incompatible");
          return false;

        default:
          // everything else
          debug("Extensions require an upgrade or vendor switch");
          return installExtension(reqInfo, instInfo);

        }
      }
    }
    return false;
  }

  /*
   * <p> An required extension is missing, if an ExtensionInstallationProvider
   * is registered, delegate the installation of that particular extension to
   * it. </p>
   *
   * @param reqInfo Missing extension information
   * 
   * @param instInfo Older installed version information
   *
   * @return true if the installation is successful
   */
  protected boolean installExtension(ExtensionInfo reqInfo,
      ExtensionInfo instInfo) throws ExtensionInstallationException {

    Vector currentProviders;
    synchronized (providers) {
      currentProviders = (Vector) providers.clone();
    }
    for (Enumeration e = currentProviders.elements(); e.hasMoreElements();) {
      ExtensionInstallationProvider eip = (ExtensionInstallationProvider) e
          .nextElement();

      if (eip != null) {
        // delegate the installation to the provider
        if (eip.installExtension(reqInfo, instInfo)) {
          debug(reqInfo.name + " installation successful");
          Launcher.ExtClassLoader cl = (Launcher.ExtClassLoader) Launcher
              .getLauncher().getClassLoader().getParent();
          addNewExtensionsToClassLoader(cl);
          return true;
        }
      }
    }
    // We have tried all of our providers, noone could install this
    // extension, we just return failure at this point
    debug(reqInfo.name + " installation failed");
    return false;
  }

  /**
   * <p>
   * Checks if the extension, that is specified in the extension-list in the
   * applet jar manifest, is already installed (i.e. exists in the extension
   * directory).
   * </p>
   *
   * @param extensionName
   *          extension name in the extension-list
   *
   * @return the extension if it exists in the extension directory
   */
  private File checkExtensionExists(String extensionName) {
    // Function added to fix bug 4504166
    final String extName = extensionName;
    final String[] fileExt = { ".jar", ".zip" };

    return (File) AccessController.doPrivileged(new PrivilegedAction() {
      public Object run() {
        try {
          File fExtension;
          File[] dirs = getExtDirs();

          // Search the extension directories for the extension that is
          // specified
          // in the attribute extension-list in the applet jar manifest
          for (int i = 0; i < dirs.length; i++) {
            for (int j = 0; j < fileExt.length; j++) {
              if (extName.toLowerCase().endsWith(fileExt[j])) {
                fExtension = new File(dirs[i], extName);
              } else {
                fExtension = new File(dirs[i], extName + fileExt[j]);
              }
              debug("checkExtensionExists:fileName " + fExtension.getName());
              if (fExtension.exists()) {
                return fExtension;
              }
            }
          }
          return null;

        } catch (Exception e) {
          debugException(e);
          return null;
        }
      }
    });
  }

  /**
   * <p>
   * 
   * @return the java.ext.dirs property as a list of directory
   *         </p>
   */
  private static File[] getExtDirs() {
    String s = java.security.AccessController.doPrivileged(
        new com.sun.ts.lib.util.sec.security.action.GetPropertyAction(
            "java.ext.dirs"));

    File[] dirs;
    if (s != null) {
      StringTokenizer st = new StringTokenizer(s, File.pathSeparator);
      int count = st.countTokens();
      debug("getExtDirs count " + count);
      dirs = new File[count];
      for (int i = 0; i < count; i++) {
        dirs[i] = new File(st.nextToken());
        debug("getExtDirs dirs[" + i + "] " + dirs[i]);
      }
    } else {
      dirs = new File[0];
      debug("getExtDirs dirs " + dirs);
    }
    debug("getExtDirs dirs.length " + dirs.length);
    return dirs;
  }

  /*
   * <p> Scan the directories and return all files installed in those </p>
   * 
   * @param dirs list of directories to scan
   *
   * @return the list of files installed in all the directories
   */
  private static File[] getExtFiles(File[] dirs) throws IOException {
    Vector urls = new Vector();
    for (int i = 0; i < dirs.length; i++) {
      String[] files = dirs[i].list(new JarFilter());
      if (files != null) {
        debug("getExtFiles files.length " + files.length);
        for (int j = 0; j < files.length; j++) {
          File f = new File(dirs[i], files[j]);
          urls.add(f);
          debug("getExtFiles f[" + j + "] " + f);
        }
      }
    }
    File[] ua = new File[urls.size()];
    urls.copyInto(ua);
    debug("getExtFiles ua.length " + ua.length);
    return ua;
  }

  /*
   * <p>
   * 
   * @return the list of installed extensions jar files </p>
   */
  private File[] getInstalledExtensions() throws IOException {
    return (File[]) AccessController.doPrivileged(new PrivilegedAction() {
      public Object run() {
        try {
          return getExtFiles(getExtDirs());
        } catch (IOException e) {
          debug("Cannot get list of installed extensions");
          debugException(e);
          return new URL[0];
        }
      }
    });
  }

  /*
   * <p> Add the newly installed jar file to the extension class loader. </p>
   *
   * @param cl the current installed extension class loader
   *
   * @return true if successful
   */
  private Boolean addNewExtensionsToClassLoader(Launcher.ExtClassLoader cl) {
    try {
      File[] installedExts = getInstalledExtensions();
      for (int i = 0; i < installedExts.length; i++) {
        final File instFile = installedExts[i];
        URL instURL = AccessController
            .doPrivileged(new PrivilegedAction<URL>() {
              public URL run() {
                try {
                  return ParseUtil.fileToEncodedURL(instFile);
                } catch (MalformedURLException e) {
                  debugException(e);
                  return null;
                }
              }
            });
        if (instURL != null) {
          URL[] urls = cl.getURLs();
          boolean found = false;
          for (int j = 0; j < urls.length; j++) {
            debug("URL[" + j + "] is " + urls[j] + " looking for " + instURL);
            if (urls[j].toString()
                .compareToIgnoreCase(instURL.toString()) == 0) {
              found = true;
              debug("Found !");
            }
          }
          if (!found) {
            debug("Not Found ! adding to the classloader " + instURL);
            cl.addExtURL(instURL);
          }
        }
      }
    } catch (MalformedURLException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
      // let's continue with the next installed extension
    }
    return Boolean.TRUE;
  }

  // True to display all debug and trace messages
  static final boolean DEBUG = false;

  private static void debug(String s) {
    if (DEBUG) {
      System.err.println(s);
    }
  }

  private void debugException(Throwable e) {
    if (DEBUG) {
      e.printStackTrace();
    }
  }

}
