// Copyright (C) 2014 The Android Open Source Project
//
// 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.

package com.google.gerrit.server.plugins;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.gerrit.extensions.annotations.Export;
import com.google.gerrit.server.plugins.Plugin.ApiType;
import com.google.inject.Module;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.util.Map;
import java.util.Set;
import java.util.jar.Manifest;

import static com.google.common.base.Preconditions.checkState;

/**
 * Base plugin scanner for a set of pre-loaded classes.
 *
 * Utility base class for simplifying the development of Server plugin scanner
 * based on a set of externally pre-loaded classes.
 *
 * Extending this class you can implement very easily a PluginContentScanner
 * from a set of pre-loaded Java Classes and an API Type.
 * The convention used by this class is:
 * - there is at most one Guice module per Gerrit module type (SysModule, HttpModule, SshModule)
 * - plugin is set to be restartable in Gerrit Plugin MANIFEST
 * - only Export and Listen annotated classes can be self-discovered
 */
public abstract class AbstractPreloadedPluginScanner implements
    PluginContentScanner {
  protected final String pluginName;
  protected final String pluginVersion;
  protected final Set<Class<?>> preloadedClasses;
  protected final ApiType apiType;

  private Class<?> sshModuleClass;
  private Class<?> httpModuleClass;
  private Class<?> sysModuleClass;

  public AbstractPreloadedPluginScanner(String pluginName, String pluginVersion,
      Set<Class<?>> preloadedClasses, Plugin.ApiType apiType) {
    this.pluginName = pluginName;
    this.pluginVersion = pluginVersion;
    this.preloadedClasses = preloadedClasses;
    this.apiType = apiType;
  }

  @Override
  public Manifest getManifest() throws IOException {
    scanGuiceModules(preloadedClasses);
    StringBuilder manifestString =
        new StringBuilder("PluginName: " + pluginName + "\n"
            + "Implementation-Version: " + pluginVersion + "\n"
            + "Gerrit-ReloadMode: restart\n"
            + "Gerrit-ApiType: " + apiType + "\n");
    appendIfNotNull(manifestString, "Gerrit-Module: ", sshModuleClass);
    appendIfNotNull(manifestString, "Gerrit-HttpModule: ", httpModuleClass);
    appendIfNotNull(manifestString, "Gerrit-SshModule: ", sysModuleClass);
    return new Manifest(new ByteArrayInputStream(manifestString.toString()
        .getBytes()));
  }

  @Override
  public Map<Class<? extends Annotation>, Iterable<ExtensionMetaData>> scan(
      String pluginName, Iterable<Class<? extends Annotation>> annotations)
      throws InvalidPluginException {
    ImmutableMap.Builder<Class<? extends Annotation>, Iterable<ExtensionMetaData>> result =
        ImmutableMap.builder();

    for (Class<? extends Annotation> annotation : annotations) {
      Set<ExtensionMetaData> classMetaDataSet = Sets.newHashSet();
      result.put(annotation, classMetaDataSet);

      for (Class<?> clazz : preloadedClasses) {
        if (!Modifier.isAbstract(clazz.getModifiers())
            && clazz.getAnnotation(annotation) != null) {
          classMetaDataSet.add(new ExtensionMetaData(clazz.getName(),
              getExportAnnotationValue(clazz, annotation)));
        }
      }
    }
    return result.build();
  }

  private void appendIfNotNull(StringBuilder string, String header,
      Class<?> guiceModuleClass) {
    if (guiceModuleClass != null) {
      string.append(header);
      string.append(guiceModuleClass.getName());
      string.append("\n");
    }
  }

  private void scanGuiceModules(Set<Class<?>> classes) throws IOException {
    try {
      Class<?> sysModuleBaseClass = Module.class;
      Class<?> httpModuleBaseClass =
          Class.forName("com.google.inject.servlet.ServletModule");
      Class<?> sshModuleBaseClass =
          Class.forName("com.google.gerrit.sshd.CommandModule");

      for (Class<?> clazz : classes) {

        if (sshModuleBaseClass.isAssignableFrom(clazz)) {
          sshModuleClass =
              getUniqueGuiceModule(sshModuleBaseClass, sshModuleClass, clazz);
        } else if (httpModuleBaseClass.isAssignableFrom(clazz)) {
          httpModuleClass =
              getUniqueGuiceModule(httpModuleBaseClass, httpModuleClass, clazz);
        } else if (sysModuleBaseClass.isAssignableFrom(clazz)) {
          sysModuleClass =
              getUniqueGuiceModule(sysModuleBaseClass, sysModuleClass, clazz);
        }
      }
    } catch (ClassNotFoundException e) {
      throw new IOException(
          "Cannot find base Gerrit classes for Guice Plugin Modules", e);
    }
  }

  private Class<?> getUniqueGuiceModule(Class<?> guiceModuleBaseClass,
      Class<?> existingGuiceModuleName, Class<?> newGuiceModuleClass) {
    checkState(existingGuiceModuleName == null,
        "Multiple %s implementations: %s, %s", guiceModuleBaseClass,
        existingGuiceModuleName, newGuiceModuleClass);
    return newGuiceModuleClass;
  }

  private String getExportAnnotationValue(Class<?> scriptClass,
      Class<? extends Annotation> annotation) {
    return annotation == Export.class ? scriptClass.getAnnotation(Export.class)
        .value() : "";
  }
}

@@ -2828,16 +2828,19 @@
 %22Gerrit-
+Ssh
 Module: 
@@ -2972,35 +2972,32 @@
 String, %22Gerrit-
-Ssh
 Module: %22, sysMo
@@ -4569,47 +4569,201 @@
 %22);%0A
-%0A      for (Class%3C?%3E clazz : classes) %7B
+      sshModuleClass = null;%0A      httpModuleClass = null;%0A      sysModuleClass = null;%0A%0A      for (Class%3C?%3E clazz : classes) %7B%0A        if (clazz.isLocalClass()) %7B%0A          continue;%0A        %7D
 %0A%0A  

Fix Guice module auto-discover for plugin providers