/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2008-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2012-2015 ForgeRock AS.
 */
package org.opends.server.admin;



import static org.opends.messages.AdminMessages.*;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.util.StaticUtils.*;
import static org.opends.server.util.ServerConstants.EOL;

import java.io.ByteArrayOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

import org.forgerock.i18n.LocalizableMessage;
import org.opends.server.admin.std.meta.RootCfgDefn;
import org.opends.server.core.DirectoryServer;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.opends.server.types.InitializationException;


/**
 * Manages the class loader which should be used for loading configuration definition classes and associated extensions.
 * <p>
 * For extensions which define their own extended configuration definitions, the class loader will make sure
 * that the configuration definition classes are loaded and initialized.
 * <p>
 * Initially the class loader provider is disabled, and calls to the {@link #getClassLoader()} will return
 * the system default class loader.
 * <p>
 * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
 */
public final class ClassLoaderProvider {
  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();

  /**
   * Private URLClassLoader implementation.
   * This is only required so that we can provide access to the addURL method.
   */
  private static final class MyURLClassLoader extends URLClassLoader {

    /** Create a class loader with the default parent class loader. */
    public MyURLClassLoader() {
      super(new URL[0]);
    }



    /**
     * Create a class loader with the provided parent class loader.
     *
     * @param parent
     *          The parent class loader.
     */
    public MyURLClassLoader(ClassLoader parent) {
      super(new URL[0], parent);
    }



    /**
     * Add a Jar file to this class loader.
     *
     * @param jarFile
     *          The name of the Jar file.
     * @throws MalformedURLException
     *           If a protocol handler for the URL could not be found, or if some other error occurred
     *           while constructing the URL.
     * @throws SecurityException
     *           If a required system property value cannot be accessed.
     */
    public void addJarFile(File jarFile) throws SecurityException, MalformedURLException {
      addURL(jarFile.toURI().toURL());
    }

  }

  /** The name of the manifest file listing the core configuration definition classes. */
  private static final String CORE_MANIFEST = "core.manifest";

  /** The name of the manifest file listing a extension's configuration definition classes. */
  private static final String EXTENSION_MANIFEST = "extension.manifest";

  /** The name of the lib directory. */
  private static final String LIB_DIR = "lib";

  /** The name of the extensions directory. */
  private static final String EXTENSIONS_DIR = "extensions";

  /** The singleton instance. */
  private static final ClassLoaderProvider INSTANCE = new ClassLoaderProvider();

  /** Attribute name in jar's MANIFEST corresponding to the revision number. */
  private static final String REVISION_NUMBER = "Revision-Number";

  /** The attribute names for build information is name, version and revision number. */
  private static final String[] BUILD_INFORMATION_ATTRIBUTE_NAMES =
                 new String[]{Attributes.Name.EXTENSION_NAME.toString(),
                              Attributes.Name.IMPLEMENTATION_VERSION.toString(),
                              REVISION_NUMBER};


  /**
   * Get the single application wide class loader provider instance.
   *
   * @return Returns the single application wide class loader provider instance.
   */
  public static ClassLoaderProvider getInstance() {
    return INSTANCE;
  }

  /** Set of registered Jar files. */
  private Set<File> jarFiles = new HashSet<>();

  /**
   * Underlying class loader used to load classes and resources (null if disabled).<br>
   * We contain a reference to the URLClassLoader rather than sub-class it so that it is possible to replace the
   * loader at run-time. For example, when removing or replacing extension Jar files (the URLClassLoader
   * only supports adding new URLs, not removal).
   */
  private MyURLClassLoader loader;



  /** Private constructor. */
  private ClassLoaderProvider() {
    // No implementation required.
  }



  /**
   * Disable this class loader provider and removed any registered extensions.
   *
   * @throws IllegalStateException
   *           If this class loader provider is already disabled.
   */
  public synchronized void disable()
      throws IllegalStateException {
    if (loader == null) {
      throw new IllegalStateException(
          "Class loader provider already disabled.");
    }
    loader = null;
    jarFiles = new HashSet<>();
  }



  /**
   * Enable this class loader provider using the application's class loader as the parent class loader.
   *
   * @throws InitializationException
   *           If the class loader provider could not initialize successfully.
   * @throws IllegalStateException
   *           If this class loader provider is already enabled.
   */
  public synchronized void enable()
      throws InitializationException, IllegalStateException {
    enable(RootCfgDefn.class.getClassLoader());
  }



  /**
   * Enable this class loader provider using the provided parent class loader.
   *
   * @param parent
   *          The parent class loader.
   * @throws InitializationException
   *           If the class loader provider could not initialize successfully.
   * @throws IllegalStateException
   *           If this class loader provider is already enabled.
   */
  public synchronized void enable(ClassLoader parent)
      throws InitializationException, IllegalStateException {
    if (loader != null) {
      throw new IllegalStateException("Class loader provider already enabled.");
    }

    if (parent != null) {
      loader = new MyURLClassLoader(parent);
    } else {
      loader = new MyURLClassLoader();
    }

    // Forcefully load all configuration definition classes in OpenDJ.jar.
    initializeCoreComponents();

    // Put extensions jars into the class loader and load all configuration definition classes in that they contain.
    // First load the extension from the install directory, then from the instance directory.
    File installExtensionsPath  = buildExtensionPath(DirectoryServer.getServerRoot());
    File instanceExtensionsPath = buildExtensionPath(DirectoryServer.getInstanceRoot());

    initializeAllExtensions(installExtensionsPath);

    if (! installExtensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
      initializeAllExtensions(instanceExtensionsPath);
    }
  }

  private File buildExtensionPath(String directory)  {
    File libDir = new File(directory, LIB_DIR);
    try {
      return new File(libDir, EXTENSIONS_DIR).getCanonicalFile();
    } catch (Exception e) {
      return new File(libDir, EXTENSIONS_DIR);
    }
  }


  /**
   * Gets the class loader which should be used for loading classes and resources. When this class loader provider
   * is disabled, the system default class loader will be returned by default.
   * <p>
   * Applications <b>MUST NOT</b> maintain persistent references to the class loader as it can change at run-time.
   *
   * @return Returns the class loader which should be used for loading classes and resources.
   */
  public synchronized ClassLoader getClassLoader() {
    if (loader != null) {
      return loader;
    } else {
      return ClassLoader.getSystemClassLoader();
    }
  }



  /**
   * Indicates whether this class loader provider is enabled.
   *
   * @return Returns <code>true</code> if this class loader provider is enabled.
   */
  public synchronized boolean isEnabled() {
    return loader != null;
  }



  /**
   * Add the named extensions to this class loader.
   *
   * @param extensions
   *          A List of the names of the extensions to be loaded.
   * @throws InitializationException
   *           If one of the extensions could not be loaded and initialized.
   */
  private synchronized void addExtension(List<File> extensions)
      throws InitializationException {
    // First add the Jar files to the class loader.
    List<JarFile> jars = new LinkedList<>();
    for (File extension : extensions) {
      if (jarFiles.contains(extension)) {
        // Skip this file as it is already loaded.
        continue;
      }

      // Attempt to load it.
      jars.add(loadJarFile(extension));

      // Register the Jar file with the class loader.
      try {
        loader.addJarFile(extension);
      } catch (Exception e) {
        logger.traceException(e);

        LocalizableMessage message = ERR_ADMIN_CANNOT_OPEN_JAR_FILE
            .get(extension.getName(), extension.getParent(), stackTraceToSingleLineString(e));
        throw new InitializationException(message);
      }
      jarFiles.add(extension);
    }

    // Now forcefully load the configuration definition classes.
    for (JarFile jar : jars) {
      initializeExtension(jar);
    }
  }



  /**
   * Prints out all information about extensions.
   *
   * @return a String instance representing all information about extensions;
   *         <code>null</code> if there is no information available.
   */
  public String printExtensionInformation() {
    File extensionsPath = buildExtensionPath(DirectoryServer.getServerRoot());

    List<File> extensions = new ArrayList<>();
    if (extensionsPath.exists() && extensionsPath.isDirectory()) {
      extensions.addAll(listFiles(extensionsPath));
    }

    File instanceExtensionsPath = buildExtensionPath(DirectoryServer.getInstanceRoot());
    if (!extensionsPath.getAbsolutePath().equals(instanceExtensionsPath.getAbsolutePath())) {
      extensions.addAll(listFiles(instanceExtensionsPath));
    }

    if ( extensions.isEmpty() ) {
      return null;
    }

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    PrintStream ps = new PrintStream(baos);
    // prints:
    // --
    //            Name                 Build number         Revision number
    ps.printf("--%s           %-20s %-20s %-20s%s",
              EOL,
              "Name",
              "Build number",
              "Revision number",
              EOL);

    for(File extension : extensions) {
      printExtensionDetails(ps, extension);
    }

    return baos.toString();
  }

  private List<File> listFiles(File path){
    if (path.exists() && path.isDirectory()) {
      return Arrays.asList(path.listFiles(new FileFilter() {
        public boolean accept(File pathname) {
          // only files with names ending with ".jar"
          return pathname.isFile() && pathname.getName().endsWith(".jar");
        }
      }));
    }
    return Collections.emptyList();
  }

  private void printExtensionDetails(PrintStream ps, File extension) {
    // retrieve MANIFEST entry and display name, build number and revision number
    try {
      JarFile jarFile = new JarFile(extension);
      JarEntry entry = jarFile.getJarEntry("admin/" + EXTENSION_MANIFEST);
      if (entry == null) {
        return;
      }

      String[] information = getBuildInformation(jarFile);

      ps.append("Extension: ");
      boolean addBlank = false;
      for(String name : information) {
        if ( addBlank ) {
          ps.append(" ");
        } else {
          addBlank = true;
        }
        ps.printf("%-20s", name);
      }
      ps.append(EOL);
    } catch(Exception e) {
      // ignore extra information for this extension
    }
  }


  /**
   * Returns a String array with the following information :
   * <br>index 0: the name of the extension.
   * <br>index 1: the build number of the extension.
   * <br>index 2: the revision number of the extension.
   *
   * @param extension the jar file of the extension
   * @return a String array containing the name, the build number and the revision number
   *         of the extension given in argument
   * @throws java.io.IOException thrown if the jar file has been closed.
   */
  private String[] getBuildInformation(JarFile extension)
      throws IOException {
    String[] result = new String[3];

    // retrieve MANIFEST entry and display name, version and revision
    Manifest manifest = extension.getManifest();

    if ( manifest != null ) {
      Attributes attributes = manifest.getMainAttributes();

      int index = 0;
      for(String name : BUILD_INFORMATION_ATTRIBUTE_NAMES) {
        String value = attributes.getValue(name);
        if ( value == null ) {
          value = "<unknown>";
        }
        result[index++] = value;
      }
    }

    return result;
  }



  /**
   * Put extensions jars into the class loader and load all configuration definition classes in that they contain.
   * @param extensionsPath Indicates where extensions are located.
   *
   * @throws InitializationException
   *           If the extensions folder could not be accessed or if a extension jar file could not be accessed or
   *           if one of the configuration definition classes could not be initialized.
   */
  private void initializeAllExtensions(File extensionsPath)
      throws InitializationException {

    try {
      if (!extensionsPath.exists()) {
        // The extensions directory does not exist. This is not a critical problem.
        logger.warn(WARN_ADMIN_NO_EXTENSIONS_DIR, extensionsPath);
        return;
      }

      if (!extensionsPath.isDirectory()) {
        // The extensions directory is not a directory. This is more critical.
        throw new InitializationException(ERR_ADMIN_EXTENSIONS_DIR_NOT_DIRECTORY.get(extensionsPath));
      }

      // Add and initialize the extensions.
      addExtension(listFiles(extensionsPath));
    } catch (InitializationException e) {
      logger.traceException(e);
      throw e;
    } catch (Exception e) {
      logger.traceException(e);

      LocalizableMessage message = ERR_ADMIN_EXTENSIONS_CANNOT_LIST_FILES.get(
          extensionsPath, stackTraceToSingleLineString(e));
      throw new InitializationException(message, e);
    }
  }



  /**
   * Make sure all core configuration definitions are loaded.
   *
   * @throws InitializationException
   *           If the core manifest file could not be read or if one of the configuration definition
   *           classes could not be initialized.
   */
  private void initializeCoreComponents()
      throws InitializationException {
    InputStream is = RootCfgDefn.class.getResourceAsStream("/admin/" + CORE_MANIFEST);

    if (is == null) {
      LocalizableMessage message = ERR_ADMIN_CANNOT_FIND_CORE_MANIFEST.get(CORE_MANIFEST);
      throw new InitializationException(message);
    }

    try {
      loadDefinitionClasses(is);
    } catch (InitializationException e) {
      logger.traceException(e);

      LocalizableMessage message = ERR_CLASS_LOADER_CANNOT_LOAD_CORE.get(CORE_MANIFEST,
          stackTraceToSingleLineString(e));
      throw new InitializationException(message);
    }
  }



  /**
   * Make sure all the configuration definition classes in a extension are loaded.
   *
   * @param jarFile
   *          The extension's Jar file.
   * @throws InitializationException
   *           If the extension jar file could not be accessed or if one of the configuration definition classes
   *           could not be initialized.
   */
  private void initializeExtension(JarFile jarFile)
      throws InitializationException {
    JarEntry entry = jarFile.getJarEntry("admin/" + EXTENSION_MANIFEST);
    if (entry != null) {
      InputStream is;
      try {
        is = jarFile.getInputStream(entry);
      } catch (Exception e) {
        logger.traceException(e);

        LocalizableMessage message = ERR_ADMIN_CANNOT_READ_EXTENSION_MANIFEST.get(EXTENSION_MANIFEST, jarFile.getName(),
            stackTraceToSingleLineString(e));
        throw new InitializationException(message);
      }

      try {
        loadDefinitionClasses(is);
      } catch (InitializationException e) {
        logger.traceException(e);

        LocalizableMessage message = ERR_CLASS_LOADER_CANNOT_LOAD_EXTENSION.get(jarFile.getName(), EXTENSION_MANIFEST,
            stackTraceToSingleLineString(e));
        throw new InitializationException(message);
      }
      logExtensionsBuildInformation(jarFile);
    }
  }



  private void logExtensionsBuildInformation(JarFile jarFile)
  {
    try {
      String[] information = getBuildInformation(jarFile);
      LocalizedLogger extensionsLogger = LocalizedLogger.getLocalizedLogger("org.opends.server.extensions");
      extensionsLogger.info(NOTE_LOG_EXTENSION_INFORMATION, jarFile.getName(), information[1], information[2]);
    } catch(Exception e) {
      // Do not log information for that extension
    }
  }



  /**
   * Forcefully load configuration definition classes named in a manifest file.
   *
   * @param is
   *          The manifest file input stream.
   * @throws InitializationException
   *           If the definition classes could not be loaded and initialized.
   */
  private void loadDefinitionClasses(InputStream is)
      throws InitializationException {
    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
    List<AbstractManagedObjectDefinition<?, ?>> definitions = new LinkedList<>();
    while (true) {
      String className;
      try {
        className = reader.readLine();
      } catch (IOException e) {
        throw new InitializationException(
            ERR_CLASS_LOADER_CANNOT_READ_MANIFEST_FILE.get(e.getMessage()), e);
      }

      // Break out when the end of the manifest is reached.
      if (className == null) {
        break;
      }

      // Skip blank lines.
      className = className.trim();
      if (className.length() == 0) {
        continue;
      }

      // Skip lines beginning with #.
      if (className.startsWith("#")) {
        continue;
      }

      logger.trace("Loading class " + className);

      // Load the class and get an instance of it if it is a definition.
      Class<?> theClass;
      try {
        theClass = Class.forName(className, true, loader);
      } catch (Exception e) {
        throw new InitializationException(ERR_CLASS_LOADER_CANNOT_LOAD_CLASS.get(className, e.getMessage()), e);
      }
      if (AbstractManagedObjectDefinition.class.isAssignableFrom(theClass)) {
        // We need to instantiate it using its getInstance() static method.
        Method method;
        try {
          method = theClass.getMethod("getInstance");
        } catch (Exception e) {
          throw new InitializationException(
              ERR_CLASS_LOADER_CANNOT_FIND_GET_INSTANCE_METHOD.get(className, e.getMessage()), e);
        }

        // Get the definition instance.
        AbstractManagedObjectDefinition<?, ?> d;
        try {
          d = (AbstractManagedObjectDefinition<?, ?>) method.invoke(null);
        } catch (Exception e) {
          throw new InitializationException(
              ERR_CLASS_LOADER_CANNOT_INVOKE_GET_INSTANCE_METHOD.get(className, e.getMessage()), e);
        }
        definitions.add(d);
      }
    }

    // Initialize any definitions that were loaded.
    for (AbstractManagedObjectDefinition<?, ?> d : definitions) {
      try {
        d.initialize();
      } catch (Exception e) {
        throw new InitializationException(
            ERR_CLASS_LOADER_CANNOT_INITIALIZE_DEFN.get(d.getName(), d.getClass().getName(), e.getMessage()), e);
      }
    }
  }



  /**
   * Load the named Jar file.
   *
   * @param jar
   *          The name of the Jar file to load.
   * @return Returns the loaded Jar file.
   * @throws InitializationException
   *           If the Jar file could not be loaded.
   */
  private JarFile loadJarFile(File jar)
      throws InitializationException {
    JarFile jarFile;

    try {
      // Load the extension jar file.
      jarFile = new JarFile(jar);
    } catch (Exception e) {
      logger.traceException(e);

      LocalizableMessage message = ERR_ADMIN_CANNOT_OPEN_JAR_FILE.get(
          jar.getName(), jar.getParent(), stackTraceToSingleLineString(e));
      throw new InitializationException(message);
    }
    return jarFile;
  }

}
