浏览代码

Add version to plugins

Plugin developpers can now add a version number to their es-plugin.properties file:

```properties
plugin=org.elasticsearch.test.integration.nodesinfo.TestPlugin
version=0.0.7-SNAPSHOT
```

Also, for site plugins, it's recommended to add a `es-plugin.properties` file in root site directory with `description` and `version` properties:

```properties
description=This is a description for a dummy test site plugin.
version=0.0.7-BOND-SITE
```

When running Nodes Info API, you will get information on versions:

```sh
$ curl 'http://localhost:9200/_nodes?plugin=true&pretty'
```

```javascript
{
  "ok" : true,
  "cluster_name" : "test-cluster-MacBook-Air-de-David.local",
  "nodes" : {
    "RHMsToxiRcCXwHiS6mEaFw" : {
      "name" : "node2",
      "transport_address" : "inet[/192.168.0.15:9301]",
      "hostname" : "MacBook-Air-de-David.local",
      "version" : "0.90.0.Beta2-SNAPSHOT",
      "http_address" : "inet[/192.168.0.15:9201]",
      "plugins" : [ {
        "name" : "dummy",
        "version" : "0.0.7-BOND-SITE",
        "description" : "This is a description for a dummy test site plugin.",
        "url" : "/_plugin/dummy/",
        "site" : true,
        "jvm" : false
      } ]
    },
    "IKiUOo-LSCq1Km1GUhBwPg" : {
      "name" : "node3",
      "transport_address" : "inet[/192.168.0.15:9302]",
      "hostname" : "MacBook-Air-de-David.local",
      "version" : "0.90.0.Beta2-SNAPSHOT",
      "http_address" : "inet[/192.168.0.15:9202]",
      "plugins" : [ {
        "name" : "test-plugin",
        "version" : "0.0.7-SNAPSHOT",
        "description" : "test-plugin description",
        "site" : false,
        "jvm" : true
      } ]
    },
    "H64dcSF2R_GNWh6XRCYZJA" : {
      "name" : "node1",
      "transport_address" : "inet[/192.168.0.15:9300]",
      "hostname" : "MacBook-Air-de-David.local",
      "version" : "0.90.0.Beta2-SNAPSHOT",
      "http_address" : "inet[/192.168.0.15:9200]",
      "plugins" : [ ]
    },
    "mGEZcYl8Tye0Rm5AACBhPA" : {
      "name" : "node4",
      "transport_address" : "inet[/192.168.0.15:9303]",
      "hostname" : "MacBook-Air-de-David.local",
      "version" : "0.90.0.Beta2-SNAPSHOT",
      "http_address" : "inet[/192.168.0.15:9203]",
      "plugins" : [ {
        "name" : "test-plugin",
        "version" : "0.0.7-SNAPSHOT",
        "description" : "test-plugin description",
        "site" : false,
        "jvm" : true
      }, {
        "name" : "test-no-version-plugin",
        "version" : "NA",
        "description" : "test-no-version-plugin description",
        "site" : false,
        "jvm" : true
      }, {
        "name" : "dummy",
        "version" : "NA",
        "description" : "No description found for dummy.",
        "url" : "/_plugin/dummy/",
        "site" : true,
        "jvm" : false
      } ]
    }
  }
}
```

Relative to #2668.
Closes #2784.
David Pilato 12 年之前
父节点
当前提交
abf9a86678

+ 49 - 10
src/main/java/org/elasticsearch/action/admin/cluster/node/info/PluginInfo.java

@@ -18,6 +18,8 @@
  */
 package org.elasticsearch.action.admin.cluster.node.info;
 
+import org.elasticsearch.Version;
+import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.io.stream.Streamable;
@@ -29,18 +31,23 @@ import java.io.IOException;
 import java.io.Serializable;
 
 public class PluginInfo implements Streamable, Serializable, ToXContent {
+    public static final String DESCRIPTION_NOT_AVAILABLE = "No description found.";
+    public static final String VERSION_NOT_AVAILABLE = "NA";
+
     static final class Fields {
         static final XContentBuilderString NAME = new XContentBuilderString("name");
         static final XContentBuilderString DESCRIPTION = new XContentBuilderString("description");
         static final XContentBuilderString URL = new XContentBuilderString("url");
         static final XContentBuilderString JVM = new XContentBuilderString("jvm");
         static final XContentBuilderString SITE = new XContentBuilderString("site");
+        static final XContentBuilderString VERSION = new XContentBuilderString("version");
     }
 
     private String name;
     private String description;
     private boolean site;
     private boolean jvm;
+    private String version;
 
     public PluginInfo() {
     }
@@ -52,12 +59,18 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
      * @param description Its description
      * @param site        true if it's a site plugin
      * @param jvm         true if it's a jvm plugin
+     * @param version     Version number is applicable (NA otherwise)
      */
-    public PluginInfo(String name, String description, boolean site, boolean jvm) {
+    public PluginInfo(String name, String description, boolean site, boolean jvm, String version) {
         this.name = name;
         this.description = description;
         this.site = site;
         this.jvm = jvm;
+        if (Strings.hasText(version)) {
+            this.version = version;
+        } else {
+            this.version = VERSION_NOT_AVAILABLE;
+        }
     }
 
     /**
@@ -91,7 +104,7 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
     /**
      * We compute the URL for sites: "/_plugin/" + name + "/"
      *
-     * @return
+     * @return relative URL for site plugin
      */
     public String getUrl() {
         if (site) {
@@ -101,6 +114,13 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
         }
     }
 
+    /**
+     * @return Version number for the plugin
+     */
+    public String getVersion() {
+        return version;
+    }
+
     public static PluginInfo readPluginInfo(StreamInput in) throws IOException {
         PluginInfo info = new PluginInfo();
         info.readFrom(in);
@@ -113,6 +133,11 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
         this.description = in.readString();
         this.site = in.readBoolean();
         this.jvm = in.readBoolean();
+        if (in.getVersion().onOrAfter(Version.V_1_0_0_RC2)) {
+            this.version = in.readString();
+        } else {
+            this.version = VERSION_NOT_AVAILABLE;
+        }
     }
 
     @Override
@@ -121,12 +146,16 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
         out.writeString(description);
         out.writeBoolean(site);
         out.writeBoolean(jvm);
+        if (out.getVersion().onOrAfter(Version.V_1_0_0_RC2)) {
+            out.writeString(version);
+        }
     }
 
     @Override
     public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
         builder.startObject();
         builder.field(Fields.NAME, name);
+        builder.field(Fields.VERSION, version);
         builder.field(Fields.DESCRIPTION, description);
         if (site) {
             builder.field(Fields.URL, getUrl());
@@ -140,16 +169,15 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
 
-        PluginInfo p = (PluginInfo) o;
+        PluginInfo that = (PluginInfo) o;
 
-        return name.equals(p.getName());
+        if (!name.equals(that.name)) return false;
+        if (version != null ? !version.equals(that.version) : that.version != null) return false;
+
+        return true;
     }
 
     @Override
@@ -157,4 +185,15 @@ public class PluginInfo implements Streamable, Serializable, ToXContent {
         return name.hashCode();
     }
 
+    @Override
+    public String toString() {
+        final StringBuffer sb = new StringBuffer("PluginInfo{");
+        sb.append("name='").append(name).append('\'');
+        sb.append(", description='").append(description).append('\'');
+        sb.append(", site=").append(site);
+        sb.append(", jvm=").append(jvm);
+        sb.append(", version='").append(version).append('\'');
+        sb.append('}');
+        return sb.toString();
+    }
 }

+ 0 - 54
src/main/java/org/elasticsearch/plugins/PluginsHelper.java

@@ -1,54 +0,0 @@
-/*
- * Licensed to Elasticsearch under one or more contributor
- * license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright
- * ownership. Elasticsearch licenses this file to you 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 org.elasticsearch.plugins;
-
-import com.google.common.collect.Sets;
-import org.elasticsearch.env.Environment;
-
-import java.io.File;
-import java.util.Set;
-
-/**
- * Helper class for plugins
- */
-public class PluginsHelper {
-
-    /**
-     * Build a list of existing site plugins for a given environment
-     * @param environment We look into Environment#pluginsFile()
-     * @return A Set of existing site plugins
-     */
-    public static Set<String> sitePlugins(Environment environment) {
-        File pluginsFile = environment.pluginsFile();
-        Set<String> sitePlugins = Sets.newHashSet();
-
-        if (!pluginsFile.exists() || !pluginsFile.isDirectory()) {
-            return sitePlugins;
-        }
-
-        for (File pluginFile : pluginsFile.listFiles()) {
-            if (new File(pluginFile, "_site").exists()) {
-                sitePlugins.add(pluginFile.getName());
-            }
-        }
-        return sitePlugins;
-    }
-
-}

+ 195 - 131
src/main/java/org/elasticsearch/plugins/PluginsService.java

@@ -20,11 +20,13 @@
 package org.elasticsearch.plugins;
 
 import com.google.common.collect.*;
+import org.apache.lucene.util.IOUtils;
 import org.elasticsearch.ElasticsearchException;
 import org.elasticsearch.action.admin.cluster.node.info.PluginInfo;
 import org.elasticsearch.action.admin.cluster.node.info.PluginsInfo;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.collect.MapBuilder;
+import org.elasticsearch.common.collect.Tuple;
 import org.elasticsearch.common.component.AbstractComponent;
 import org.elasticsearch.common.component.LifecycleComponent;
 import org.elasticsearch.common.inject.Module;
@@ -42,8 +44,6 @@ import java.lang.reflect.Method;
 import java.net.URL;
 import java.util.*;
 
-import static com.google.common.collect.Maps.newHashMap;
-
 /**
  *
  */
@@ -52,7 +52,10 @@ public class PluginsService extends AbstractComponent {
 
     private final Environment environment;
 
-    private final ImmutableMap<String, Plugin> plugins;
+    /**
+     * We keep around a list of jvm plugins
+     */
+    private final ImmutableList<Tuple<PluginInfo, Plugin>> plugins;
 
     private final ImmutableMap<Plugin, List<OnModuleReference>> onModuleReferences;
 
@@ -79,25 +82,47 @@ public class PluginsService extends AbstractComponent {
         super(settings);
         this.environment = environment;
 
-        Map<String, Plugin> plugins = Maps.newHashMap();
+        ImmutableList.Builder<Tuple<PluginInfo, Plugin>> tupleBuilder = ImmutableList.builder();
 
-        //first we load all the default plugins from the settings
+        // first we load all the default plugins from the settings
         String[] defaultPluginsClasses = settings.getAsArray("plugin.types");
         for (String pluginClass : defaultPluginsClasses) {
             Plugin plugin = loadPlugin(pluginClass, settings);
-            plugins.put(plugin.name(), plugin);
+            PluginInfo pluginInfo = new PluginInfo(plugin.name(), plugin.description(), hasSite(plugin.name()), true, PluginInfo.VERSION_NOT_AVAILABLE);
+            if (logger.isTraceEnabled()) {
+                logger.trace("plugin loaded from settings [{}]", pluginInfo);
+            }
+            tupleBuilder.add(new Tuple<PluginInfo, Plugin>(pluginInfo, plugin));
         }
 
         // now, find all the ones that are in the classpath
         loadPluginsIntoClassLoader();
-        plugins.putAll(loadPluginsFromClasspath(settings));
-        Set<String> sitePlugins = PluginsHelper.sitePlugins(this.environment);
+        tupleBuilder.addAll(loadPluginsFromClasspath(settings));
+        this.plugins = tupleBuilder.build();
+
+        // We need to build a List of jvm and site plugins for checking mandatory plugins
+        Map<String, Plugin> jvmPlugins = Maps.newHashMap();
+        List<String> sitePlugins = Lists.newArrayList();
 
+        for (Tuple<PluginInfo, Plugin> tuple : this.plugins) {
+            jvmPlugins.put(tuple.v2().name(), tuple.v2());
+            if (tuple.v1().isSite()) {
+                sitePlugins.add(tuple.v1().getName());
+            }
+        }
+
+        // we load site plugins
+        ImmutableList<Tuple<PluginInfo, Plugin>> tuples = loadSitePlugins();
+        for (Tuple<PluginInfo, Plugin> tuple : tuples) {
+            sitePlugins.add(tuple.v1().getName());
+        }
+
+        // Checking expected plugins
         String[] mandatoryPlugins = settings.getAsArray("plugin.mandatory", null);
         if (mandatoryPlugins != null) {
             Set<String> missingPlugins = Sets.newHashSet();
             for (String mandatoryPlugin : mandatoryPlugins) {
-                if (!plugins.containsKey(mandatoryPlugin) && !sitePlugins.contains(mandatoryPlugin) && !missingPlugins.contains(mandatoryPlugin)) {
+                if (!jvmPlugins.containsKey(mandatoryPlugin) && !sitePlugins.contains(mandatoryPlugin) && !missingPlugins.contains(mandatoryPlugin)) {
                     missingPlugins.add(mandatoryPlugin);
                 }
             }
@@ -106,12 +131,10 @@ public class PluginsService extends AbstractComponent {
             }
         }
 
-        logger.info("loaded {}, sites {}", plugins.keySet(), sitePlugins);
-
-        this.plugins = ImmutableMap.copyOf(plugins);
+        logger.info("loaded {}, sites {}", jvmPlugins.keySet(), sitePlugins);
 
         MapBuilder<Plugin, List<OnModuleReference>> onModuleReferences = MapBuilder.newMapBuilder();
-        for (Plugin plugin : plugins.values()) {
+        for (Plugin plugin : jvmPlugins.values()) {
             List<OnModuleReference> list = Lists.newArrayList();
             for (Method method : plugin.getClass().getDeclaredMethods()) {
                 if (!method.getName().equals("onModule")) {
@@ -139,7 +162,7 @@ public class PluginsService extends AbstractComponent {
 
     }
 
-    public ImmutableMap<String, Plugin> plugins() {
+    public ImmutableList<Tuple<PluginInfo, Plugin>> plugins() {
         return plugins;
     }
 
@@ -150,17 +173,17 @@ public class PluginsService extends AbstractComponent {
     }
 
     public void processModule(Module module) {
-        for (Plugin plugin : plugins().values()) {
-            plugin.processModule(module);
+        for (Tuple<PluginInfo, Plugin> plugin : plugins()) {
+            plugin.v2().processModule(module);
             // see if there are onModule references
-            List<OnModuleReference> references = onModuleReferences.get(plugin);
+            List<OnModuleReference> references = onModuleReferences.get(plugin.v2());
             if (references != null) {
                 for (OnModuleReference reference : references) {
                     if (reference.moduleClass.isAssignableFrom(module.getClass())) {
                         try {
-                            reference.onModuleMethod.invoke(plugin, module);
+                            reference.onModuleMethod.invoke(plugin.v2(), module);
                         } catch (Exception e) {
-                            logger.warn("plugin {}, failed to invoke custom onModule method", e, plugin.name());
+                            logger.warn("plugin {}, failed to invoke custom onModule method", e, plugin.v2().name());
                         }
                     }
                 }
@@ -171,80 +194,80 @@ public class PluginsService extends AbstractComponent {
     public Settings updatedSettings() {
         ImmutableSettings.Builder builder = ImmutableSettings.settingsBuilder()
                 .put(this.settings);
-        for (Plugin plugin : plugins.values()) {
-            builder.put(plugin.additionalSettings());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            builder.put(plugin.v2().additionalSettings());
         }
         return builder.build();
     }
 
     public Collection<Class<? extends Module>> modules() {
         List<Class<? extends Module>> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.modules());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().modules());
         }
         return modules;
     }
 
     public Collection<Module> modules(Settings settings) {
         List<Module> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.modules(settings));
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().modules(settings));
         }
         return modules;
     }
 
     public Collection<Class<? extends LifecycleComponent>> services() {
         List<Class<? extends LifecycleComponent>> services = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            services.addAll(plugin.services());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            services.addAll(plugin.v2().services());
         }
         return services;
     }
 
     public Collection<Class<? extends Module>> indexModules() {
         List<Class<? extends Module>> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.indexModules());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().indexModules());
         }
         return modules;
     }
 
     public Collection<Module> indexModules(Settings settings) {
         List<Module> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.indexModules(settings));
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().indexModules(settings));
         }
         return modules;
     }
 
     public Collection<Class<? extends CloseableIndexComponent>> indexServices() {
         List<Class<? extends CloseableIndexComponent>> services = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            services.addAll(plugin.indexServices());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            services.addAll(plugin.v2().indexServices());
         }
         return services;
     }
 
     public Collection<Class<? extends Module>> shardModules() {
         List<Class<? extends Module>> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.shardModules());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().shardModules());
         }
         return modules;
     }
 
     public Collection<Module> shardModules(Settings settings) {
         List<Module> modules = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            modules.addAll(plugin.shardModules(settings));
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            modules.addAll(plugin.v2().shardModules(settings));
         }
         return modules;
     }
 
     public Collection<Class<? extends CloseableIndexComponent>> shardServices() {
         List<Class<? extends CloseableIndexComponent>> services = Lists.newArrayList();
-        for (Plugin plugin : plugins.values()) {
-            services.addAll(plugin.shardServices());
+        for (Tuple<PluginInfo, Plugin> plugin : plugins) {
+            services.addAll(plugin.v2().shardServices());
         }
         return services;
     }
@@ -260,79 +283,35 @@ public class PluginsService extends AbstractComponent {
         if (refreshInterval.millis() != 0) {
             if (cachedPluginsInfo != null &&
                     (refreshInterval.millis() < 0 || (System.currentTimeMillis() - lastRefresh) < refreshInterval.millis())) {
-                if (logger.isTraceEnabled()) logger.trace("using cache to retrieve plugins info");
+                if (logger.isTraceEnabled()) {
+                    logger.trace("using cache to retrieve plugins info");
+                }
                 return cachedPluginsInfo;
             }
             lastRefresh = System.currentTimeMillis();
         }
 
-        if (logger.isTraceEnabled()) logger.trace("starting to fetch info on plugins");
-        cachedPluginsInfo = new PluginsInfo();
-
-        // We create a map to have only unique values
-        Set<String> plugins = new HashSet<String>();
-
-        for (Plugin plugin : plugins().values()) {
-            // We should detect if the plugin has also an embedded _site structure
-            File siteFile = new File(new File(environment.pluginsFile(), plugin.name()), "_site");
-            boolean isSite = siteFile.exists() && siteFile.isDirectory();
-            if (logger.isTraceEnabled()) logger.trace("found a jvm plugin [{}], [{}]{}",
-                    plugin.name(), plugin.description(), isSite ? ": with _site structure" : "");
-            cachedPluginsInfo.add(new PluginInfo(plugin.name(), plugin.description(), isSite, true));
-            plugins.add(plugin.name());
+        if (logger.isTraceEnabled()) {
+            logger.trace("starting to fetch info on plugins");
         }
+        cachedPluginsInfo = new PluginsInfo();
 
-        File pluginsFile = environment.pluginsFile();
-        if (!pluginsFile.exists()) {
-            return cachedPluginsInfo;
-        }
-        if (!pluginsFile.isDirectory()) {
-            return cachedPluginsInfo;
+        // We first add all JvmPlugins
+        for (Tuple<PluginInfo, Plugin> plugin : this.plugins) {
+            if (logger.isTraceEnabled()) {
+                logger.trace("adding jvm plugin [{}]", plugin.v1());
+            }
+            cachedPluginsInfo.add(plugin.v1());
         }
 
-        File[] pluginsFiles = pluginsFile.listFiles();
-        if (pluginsFiles != null) {
-            for (File plugin : pluginsFiles) {
-                // We skip already known jvm plugins
-                if (!plugins.contains(plugin.getName())) {
-                    File sitePluginDir = new File(plugin, "_site");
-                    if (sitePluginDir.exists()) {
-                        String name = plugin.getName();
-                        String description = "No description found for " + name + ".";
-
-                        // We check if es-plugin.properties exists in plugin/_site dir
-                        File pluginPropFile = new File(sitePluginDir, ES_PLUGIN_PROPERTIES);
-                        if (pluginPropFile.exists()) {
-
-                            Properties pluginProps = new Properties();
-                            InputStream is = null;
-                            try {
-                                is = new FileInputStream(pluginPropFile.getAbsolutePath());
-                                pluginProps.load(is);
-                                description = pluginProps.getProperty("description");
-                            } catch (Exception e) {
-                                logger.warn("failed to load plugin description from [" +
-                                        pluginPropFile.getAbsolutePath() + "]", e);
-                            } finally {
-                                if (is != null) {
-                                    try {
-                                        is.close();
-                                    } catch (IOException e) {
-                                        // ignore
-                                    }
-                                }
-                            }
-                        }
-
-                        if (logger.isTraceEnabled()) logger.trace("found a site plugin [{}], [{}]",
-                                name, description);
-                        cachedPluginsInfo.add(new PluginInfo(name, description, true, false));
-                    }
-                }
+        // We reload site plugins (in case of some changes)
+        for (Tuple<PluginInfo, Plugin> plugin : loadSitePlugins()) {
+            if (logger.isTraceEnabled()) {
+                logger.trace("adding site plugin [{}]", plugin.v1());
             }
+            cachedPluginsInfo.add(plugin.v1());
         }
 
-
         return cachedPluginsInfo;
     }
 
@@ -367,7 +346,9 @@ public class PluginsService extends AbstractComponent {
         if (pluginsFile != null) {
             for (File pluginFile : pluginsFiles) {
                 if (pluginFile.isDirectory()) {
-                    logger.trace("--- adding plugin [" + pluginFile.getAbsolutePath() + "]");
+                    if (logger.isTraceEnabled()) {
+                        logger.trace("--- adding plugin [" + pluginFile.getAbsolutePath() + "]");
+                    }
                     try {
                         // add the root
                         addURL.invoke(classLoader, pluginFile.toURI().toURL());
@@ -388,7 +369,7 @@ public class PluginsService extends AbstractComponent {
                             }
                             addURL.invoke(classLoader, libFile.toURI().toURL());
                         }
-                    } catch (Exception e) {
+                    } catch (Throwable e) {
                         logger.warn("failed to add plugin [" + pluginFile + "]", e);
                     }
                 }
@@ -398,48 +379,130 @@ public class PluginsService extends AbstractComponent {
         }
     }
 
-    private Map<String, Plugin> loadPluginsFromClasspath(Settings settings) {
-        Map<String, Plugin> plugins = newHashMap();
-        Enumeration<URL> pluginUrls = null;
+    private ImmutableList<Tuple<PluginInfo,Plugin>> loadPluginsFromClasspath(Settings settings) {
+        ImmutableList.Builder<Tuple<PluginInfo, Plugin>> plugins = ImmutableList.builder();
+
+        // Trying JVM plugins: looking for es-plugin.properties files
         try {
-            pluginUrls = settings.getClassLoader().getResources(ES_PLUGIN_PROPERTIES);
+            Enumeration<URL> pluginUrls = settings.getClassLoader().getResources(ES_PLUGIN_PROPERTIES);
+            while (pluginUrls.hasMoreElements()) {
+                URL pluginUrl = pluginUrls.nextElement();
+                Properties pluginProps = new Properties();
+                InputStream is = null;
+                try {
+                    is = pluginUrl.openStream();
+                    pluginProps.load(is);
+                    String pluginClassName = pluginProps.getProperty("plugin");
+                    String pluginVersion = pluginProps.getProperty("version", PluginInfo.VERSION_NOT_AVAILABLE);
+                    Plugin plugin = loadPlugin(pluginClassName, settings);
+
+                    // Is it a site plugin as well? Does it have also an embedded _site structure
+                    File siteFile = new File(new File(environment.pluginsFile(), plugin.name()), "_site");
+                    boolean isSite = siteFile.exists() && siteFile.isDirectory();
+                    if (logger.isTraceEnabled()) {
+                        logger.trace("found a jvm plugin [{}], [{}]{}",
+                                plugin.name(), plugin.description(), isSite ? ": with _site structure" : "");
+                    }
+
+                    PluginInfo pluginInfo = new PluginInfo(plugin.name(), plugin.description(), isSite, true, pluginVersion);
+
+                    plugins.add(new Tuple<PluginInfo, Plugin>(pluginInfo, plugin));
+                } catch (Throwable e) {
+                    logger.warn("failed to load plugin from [" + pluginUrl + "]", e);
+                } finally {
+                    IOUtils.closeWhileHandlingException(is);
+                }
+            }
         } catch (IOException e) {
-            logger.warn("failed to find plugins from classpath", e);
-            return ImmutableMap.of();
+            logger.warn("failed to find jvm plugins from classpath", e);
         }
-        while (pluginUrls.hasMoreElements()) {
-            URL pluginUrl = pluginUrls.nextElement();
-            Properties pluginProps = new Properties();
-            InputStream is = null;
-            try {
-                is = pluginUrl.openStream();
-                pluginProps.load(is);
-                String pluginClassName = pluginProps.getProperty("plugin");
-                Plugin plugin = loadPlugin(pluginClassName, settings);
-                plugins.put(plugin.name(), plugin);
-            } catch (Exception e) {
-                logger.warn("failed to load plugin from [" + pluginUrl + "]", e);
-            } finally {
-                if (is != null) {
-                    try {
-                        is.close();
-                    } catch (IOException e) {
-                        // ignore
+
+        return plugins.build();
+    }
+
+    private ImmutableList<Tuple<PluginInfo,Plugin>> loadSitePlugins() {
+        ImmutableList.Builder<Tuple<PluginInfo, Plugin>> sitePlugins = ImmutableList.builder();
+        List<String> loadedJvmPlugins = new ArrayList<String>();
+
+        // Already known jvm plugins are ignored
+        for(Tuple<PluginInfo, Plugin> tuple : plugins) {
+            if (tuple.v1().isSite()) {
+                loadedJvmPlugins.add(tuple.v1().getName());
+            }
+        }
+
+        // Let's try to find all _site plugins we did not already found
+        File pluginsFile = environment.pluginsFile();
+
+        if (!pluginsFile.exists() || !pluginsFile.isDirectory()) {
+            return sitePlugins.build();
+        }
+
+        for (File pluginFile : pluginsFile.listFiles()) {
+            if (!loadedJvmPlugins.contains(pluginFile.getName())) {
+                File sitePluginDir = new File(pluginFile, "_site");
+                if (sitePluginDir.exists()) {
+                    // We have a _site plugin. Let's try to get more information on it
+                    String name = pluginFile.getName();
+                    String version = PluginInfo.VERSION_NOT_AVAILABLE;
+                    String description = PluginInfo.DESCRIPTION_NOT_AVAILABLE;
+
+                    // We check if es-plugin.properties exists in plugin/_site dir
+                    File pluginPropFile = new File(sitePluginDir, ES_PLUGIN_PROPERTIES);
+                    if (pluginPropFile.exists()) {
+
+                        Properties pluginProps = new Properties();
+                        InputStream is = null;
+                        try {
+                            is = new FileInputStream(pluginPropFile.getAbsolutePath());
+                            pluginProps.load(is);
+                            description = pluginProps.getProperty("description", PluginInfo.DESCRIPTION_NOT_AVAILABLE);
+                            version = pluginProps.getProperty("version", PluginInfo.VERSION_NOT_AVAILABLE);
+                        } catch (Exception e) {
+                            // Can not load properties for this site plugin. Ignoring.
+                            logger.debug("can not load {} file.", e, ES_PLUGIN_PROPERTIES);
+                        } finally {
+                            IOUtils.closeWhileHandlingException(is);
+                        }
+                    }
+
+                    if (logger.isTraceEnabled()) {
+                        logger.trace("found a site plugin name [{}], version [{}], description [{}]",
+                                name, version, description);
                     }
+                    sitePlugins.add(new Tuple<PluginInfo, Plugin>(new PluginInfo(name, description, true, false, version), null));
                 }
             }
         }
-        return plugins;
+
+        return sitePlugins.build();
+    }
+
+    /**
+     * @param name plugin name
+     * @return if this jvm plugin has also a _site structure
+     */
+    private boolean hasSite(String name) {
+        // Let's try to find all _site plugins we did not already found
+        File pluginsFile = environment.pluginsFile();
+
+        if (!pluginsFile.exists() || !pluginsFile.isDirectory()) {
+            return false;
+        }
+
+        File sitePluginDir = new File(pluginsFile, name + "/_site");
+        return sitePluginDir.exists();
     }
 
     private Plugin loadPlugin(String className, Settings settings) {
         try {
             Class<? extends Plugin> pluginClass = (Class<? extends Plugin>) settings.getClassLoader().loadClass(className);
+            Plugin plugin;
             try {
-                return pluginClass.getConstructor(Settings.class).newInstance(settings);
+                plugin = pluginClass.getConstructor(Settings.class).newInstance(settings);
             } catch (NoSuchMethodException e) {
                 try {
-                    return pluginClass.getConstructor().newInstance();
+                    plugin = pluginClass.getConstructor().newInstance();
                 } catch (NoSuchMethodException e1) {
                     throw new ElasticsearchException("No constructor for [" + pluginClass + "]. A plugin class must " +
                             "have either an empty default constructor or a single argument constructor accepting a " +
@@ -447,9 +510,10 @@ public class PluginsService extends AbstractComponent {
                 }
             }
 
-        } catch (Exception e) {
+            return plugin;
+
+        } catch (Throwable e) {
             throw new ElasticsearchException("Failed to load plugin class [" + className + "]", e);
         }
-
     }
 }

+ 39 - 13
src/test/java/org/elasticsearch/nodesinfo/SimpleNodesInfoTests.java

@@ -60,7 +60,7 @@ public class SimpleNodesInfoTests extends ElasticsearchIntegrationTest {
     static final class Fields {
         static final String SITE_PLUGIN = "dummy";
         static final String SITE_PLUGIN_DESCRIPTION = "This is a description for a dummy test site plugin.";
-        static final String SITE_PLUGIN_NO_DESCRIPTION = "No description found for dummy.";
+        static final String SITE_PLUGIN_VERSION = "0.0.7-BOND-SITE";
     }
 
 
@@ -129,32 +129,41 @@ public class SimpleNodesInfoTests extends ElasticsearchIntegrationTest {
         ClusterHealthResponse clusterHealth = client().admin().cluster().health(clusterHealthRequest().waitForGreenStatus()).actionGet();
         logger.info("--> done cluster_health, status " + clusterHealth.getStatus());
 
-        NodesInfoResponse response = client().admin().cluster().prepareNodesInfo().execute().actionGet();
+        NodesInfoResponse response = client().admin().cluster().prepareNodesInfo().clear().setPlugin(true).execute().actionGet();
         logger.info("--> full json answer, status " + response.toString());
 
-        assertNodeContainsPlugins(response, server1NodeId, Collections.EMPTY_LIST, Collections.EMPTY_LIST,
-                Collections.EMPTY_LIST, Collections.EMPTY_LIST);
+        assertNodeContainsPlugins(response, server1NodeId,
+                Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_LIST, // No JVM Plugin
+                Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_LIST);// No Site Plugin
 
-        assertNodeContainsPlugins(response, server2NodeId, Collections.EMPTY_LIST, Collections.EMPTY_LIST,
-                Lists.newArrayList(Fields.SITE_PLUGIN),
-                Lists.newArrayList(Fields.SITE_PLUGIN_DESCRIPTION));
+        assertNodeContainsPlugins(response, server2NodeId,
+                Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_LIST, // No JVM Plugin
+                Lists.newArrayList(Fields.SITE_PLUGIN),                                 // Site Plugin
+                Lists.newArrayList(Fields.SITE_PLUGIN_DESCRIPTION),
+                Lists.newArrayList(Fields.SITE_PLUGIN_VERSION));
 
-        assertNodeContainsPlugins(response, server3NodeId, Lists.newArrayList(TestPlugin.Fields.NAME),
+        assertNodeContainsPlugins(response, server3NodeId,
+                Lists.newArrayList(TestPlugin.Fields.NAME),                             // JVM Plugin
                 Lists.newArrayList(TestPlugin.Fields.DESCRIPTION),
-                Collections.EMPTY_LIST, Collections.EMPTY_LIST);
+                Lists.newArrayList(PluginInfo.VERSION_NOT_AVAILABLE),
+                Collections.EMPTY_LIST, Collections.EMPTY_LIST, Collections.EMPTY_LIST);// No site Plugin
 
         assertNodeContainsPlugins(response, server4NodeId,
-                Lists.newArrayList(TestNoVersionPlugin.Fields.NAME),
+                Lists.newArrayList(TestNoVersionPlugin.Fields.NAME),                    // JVM Plugin
                 Lists.newArrayList(TestNoVersionPlugin.Fields.DESCRIPTION),
-                Lists.newArrayList(Fields.SITE_PLUGIN, TestNoVersionPlugin.Fields.NAME),
-                Lists.newArrayList(Fields.SITE_PLUGIN_NO_DESCRIPTION, TestNoVersionPlugin.Fields.DESCRIPTION));
+                Lists.newArrayList(PluginInfo.VERSION_NOT_AVAILABLE),
+                Lists.newArrayList(Fields.SITE_PLUGIN, TestNoVersionPlugin.Fields.NAME),// Site Plugin
+                Lists.newArrayList(PluginInfo.DESCRIPTION_NOT_AVAILABLE),
+                Lists.newArrayList(PluginInfo.VERSION_NOT_AVAILABLE));
     }
 
     private void assertNodeContainsPlugins(NodesInfoResponse response, String nodeId,
                                            List<String> expectedJvmPluginNames,
                                            List<String> expectedJvmPluginDescriptions,
+                                           List<String> expectedJvmVersions,
                                            List<String> expectedSitePluginNames,
-                                           List<String> expectedSitePluginDescriptions) {
+                                           List<String> expectedSitePluginDescriptions,
+                                           List<String> expectedSiteVersions) {
 
         assertThat(response.getNodesMap().get(nodeId), notNullValue());
 
@@ -171,6 +180,11 @@ public class SimpleNodesInfoTests extends ElasticsearchIntegrationTest {
             assertThat(pluginDescriptions, hasItem(expectedJvmPluginDescription));
         }
 
+        List<String> jvmPluginVersions = FluentIterable.from(plugins.getInfos()).filter(jvmPluginPredicate).transform(versionFunction).toList();
+        for (String pluginVersion : expectedJvmVersions) {
+            assertThat(jvmPluginVersions, hasItem(pluginVersion));
+        }
+
         FluentIterable<String> jvmUrls = FluentIterable.from(plugins.getInfos())
                 .filter(and(jvmPluginPredicate, Predicates.not(sitePluginPredicate)))
                 .filter(isNull())
@@ -189,6 +203,12 @@ public class SimpleNodesInfoTests extends ElasticsearchIntegrationTest {
 
         List<String> sitePluginUrls = FluentIterable.from(plugins.getInfos()).filter(sitePluginPredicate).transform(urlFunction).toList();
         assertThat(sitePluginUrls, not(contains(nullValue())));
+
+
+        List<String> sitePluginVersions = FluentIterable.from(plugins.getInfos()).filter(sitePluginPredicate).transform(versionFunction).toList();
+        for (String pluginVersion : expectedSiteVersions) {
+            assertThat(sitePluginVersions, hasItem(pluginVersion));
+        }
     }
 
     private String startNodeWithPlugins(int nodeId, String ... pluginClassNames) throws URISyntaxException {
@@ -242,4 +262,10 @@ public class SimpleNodesInfoTests extends ElasticsearchIntegrationTest {
             return pluginInfo.getUrl();
         }
     };
+
+    private Function<PluginInfo, String> versionFunction = new Function<PluginInfo, String>() {
+        public String apply(PluginInfo pluginInfo) {
+            return pluginInfo.getVersion();
+        }
+    };
 }

+ 1 - 0
src/test/resources/org/elasticsearch/nodesinfo/node2/dummy/_site/es-plugin.properties

@@ -17,4 +17,5 @@
 # under the License.
 ################################################################
 description=This is a description for a dummy test site plugin.
+version=0.0.7-BOND-SITE