5656import java .util .HashMap ;
5757import java .util .HashSet ;
5858import java .util .Iterator ;
59+ import java .util .LinkedHashMap ;
5960import java .util .LinkedHashSet ;
60- import java .util .LinkedList ;
6161import java .util .List ;
6262import java .util .Locale ;
6363import java .util .Map ;
6464import java .util .Objects ;
6565import java .util .Set ;
6666import java .util .function .Function ;
6767import java .util .stream .Collectors ;
68+ import java .util .stream .Stream ;
6869
6970import static org .elasticsearch .common .io .FileSystemUtils .isAccessibleDirectory ;
7071
@@ -102,6 +103,8 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
102103
103104 List <Tuple <PluginInfo , Plugin >> pluginsLoaded = new ArrayList <>();
104105 List <PluginInfo > pluginsList = new ArrayList <>();
106+ // we need to build a List of plugins for checking mandatory plugins
107+ final List <String > pluginsNames = new ArrayList <>();
105108 // first we load plugins that are on the classpath. this is for tests and transport clients
106109 for (Class <? extends Plugin > pluginClass : classpathPlugins ) {
107110 Plugin plugin = loadPlugin (pluginClass , settings , configPath );
@@ -112,6 +115,7 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
112115 }
113116 pluginsLoaded .add (new Tuple <>(pluginInfo , plugin ));
114117 pluginsList .add (pluginInfo );
118+ pluginsNames .add (pluginInfo .getName ());
115119 }
116120
117121 Set <Bundle > seenBundles = new LinkedHashSet <>();
@@ -135,11 +139,15 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
135139 // TODO: remove this leniency, but tests bogusly rely on it
136140 if (isAccessibleDirectory (pluginsDirectory , logger )) {
137141 checkForFailedPluginRemovals (pluginsDirectory );
138- Set <Bundle > plugins = getPluginBundles (pluginsDirectory );
139- for (Bundle bundle : plugins ) {
140- pluginsList .add (bundle .plugin );
142+ List <BundleCollection > plugins = getPluginBundleCollections (pluginsDirectory );
143+ for (final BundleCollection plugin : plugins ) {
144+ final Collection <Bundle > bundles = plugin .bundles ();
145+ for (final Bundle bundle : bundles ) {
146+ pluginsList .add (bundle .plugin );
147+ }
148+ seenBundles .addAll (bundles );
149+ pluginsNames .add (plugin .name ());
141150 }
142- seenBundles .addAll (plugins );
143151 }
144152 } catch (IOException ex ) {
145153 throw new IllegalStateException ("Unable to initialize plugins" , ex );
@@ -152,12 +160,6 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
152160 this .info = new PluginsAndModules (pluginsList , modulesList );
153161 this .plugins = Collections .unmodifiableList (pluginsLoaded );
154162
155- // We need to build a List of plugins for checking mandatory plugins
156- Set <String > pluginsNames = new HashSet <>();
157- for (Tuple <PluginInfo , Plugin > tuple : this .plugins ) {
158- pluginsNames .add (tuple .v1 ().getName ());
159- }
160-
161163 // Checking expected plugins
162164 List <String > mandatoryPlugins = MANDATORY_SETTING .get (settings );
163165 if (mandatoryPlugins .isEmpty () == false ) {
@@ -168,7 +170,11 @@ public PluginsService(Settings settings, Path configPath, Path modulesDirectory,
168170 }
169171 }
170172 if (!missingPlugins .isEmpty ()) {
171- throw new ElasticsearchException ("Missing mandatory plugins [" + Strings .collectionToDelimitedString (missingPlugins , ", " ) + "]" );
173+ final String message = String .format (
174+ Locale .ROOT ,
175+ "missing mandatory plugins [%s]" ,
176+ Strings .collectionToDelimitedString (missingPlugins , ", " ));
177+ throw new IllegalStateException (message );
172178 }
173179 }
174180
@@ -244,9 +250,17 @@ public PluginsAndModules info() {
244250 return info ;
245251 }
246252
253+ /**
254+ * An abstraction over a single plugin and meta-plugins.
255+ */
256+ interface BundleCollection {
257+ String name ();
258+ Collection <Bundle > bundles ();
259+ }
260+
247261 // a "bundle" is a group of plugins in a single classloader
248262 // really should be 1-1, but we are not so fortunate
249- static class Bundle {
263+ static class Bundle implements BundleCollection {
250264 final PluginInfo plugin ;
251265 final Set <URL > urls ;
252266
@@ -266,6 +280,16 @@ static class Bundle {
266280 this .urls = Objects .requireNonNull (urls );
267281 }
268282
283+ @ Override
284+ public String name () {
285+ return plugin .getName ();
286+ }
287+
288+ @ Override
289+ public Collection <Bundle > bundles () {
290+ return Collections .singletonList (this );
291+ }
292+
269293 @ Override
270294 public boolean equals (Object o ) {
271295 if (this == o ) return true ;
@@ -281,35 +305,78 @@ public int hashCode() {
281305 }
282306
283307 /**
284- * Extracts all installed plugin directories from the provided {@code rootPath} expanding meta plugins if needed.
308+ * Represents a meta-plugin and the {@link Bundle}s corresponding to its constituents.
309+ */
310+ static class MetaBundle implements BundleCollection {
311+ private final String name ;
312+ private final List <Bundle > bundles ;
313+
314+ MetaBundle (final String name , final List <Bundle > bundles ) {
315+ this .name = name ;
316+ this .bundles = bundles ;
317+ }
318+
319+ @ Override
320+ public String name () {
321+ return name ;
322+ }
323+
324+ @ Override
325+ public Collection <Bundle > bundles () {
326+ return bundles ;
327+ }
328+
329+ }
330+
331+ /**
332+ * Extracts all installed plugin directories from the provided {@code rootPath} expanding meta-plugins if needed.
333+ *
285334 * @param rootPath the path where the plugins are installed
286- * @return A list of all plugin paths installed in the {@code rootPath}
335+ * @return a list of all plugin paths installed in the {@code rootPath}
287336 * @throws IOException if an I/O exception occurred reading the directories
288337 */
289338 public static List <Path > findPluginDirs (final Path rootPath ) throws IOException {
339+ final Tuple <List <Path >, Map <String , List <Path >>> groupedPluginDirs = findGroupedPluginDirs (rootPath );
340+ return Stream .concat (
341+ groupedPluginDirs .v1 ().stream (),
342+ groupedPluginDirs .v2 ().values ().stream ().flatMap (Collection ::stream ))
343+ .collect (Collectors .toList ());
344+ }
345+
346+ /**
347+ * Extracts all installed plugin directories from the provided {@code rootPath} expanding meta-plugins if needed. The plugins are
348+ * grouped into plugins and meta-plugins. The meta-plugins are keyed by the meta-plugin name.
349+ *
350+ * @param rootPath the path where the plugins are installed
351+ * @return a tuple of plugins as the first component and meta-plugins keyed by meta-plugin name as the second component
352+ * @throws IOException if an I/O exception occurred reading the directories
353+ */
354+ private static Tuple <List <Path >, Map <String , List <Path >>> findGroupedPluginDirs (final Path rootPath ) throws IOException {
290355 final List <Path > plugins = new ArrayList <>();
356+ final Map <String , List <Path >> metaPlugins = new LinkedHashMap <>();
291357 final Set <String > seen = new HashSet <>();
292358 if (Files .exists (rootPath )) {
293359 try (DirectoryStream <Path > stream = Files .newDirectoryStream (rootPath )) {
294360 for (Path plugin : stream ) {
295361 if (FileSystemUtils .isDesktopServicesStore (plugin ) ||
296- plugin .getFileName ().toString ().startsWith (".removing-" )) {
362+ plugin .getFileName ().toString ().startsWith (".removing-" )) {
297363 continue ;
298364 }
299365 if (seen .add (plugin .getFileName ().toString ()) == false ) {
300366 throw new IllegalStateException ("duplicate plugin: " + plugin );
301367 }
302368 if (MetaPluginInfo .isMetaPlugin (plugin )) {
369+ final String name = plugin .getFileName ().toString ();
303370 try (DirectoryStream <Path > subStream = Files .newDirectoryStream (plugin )) {
304371 for (Path subPlugin : subStream ) {
305372 if (MetaPluginInfo .isPropertiesFile (subPlugin ) ||
306- FileSystemUtils .isDesktopServicesStore (subPlugin )) {
373+ FileSystemUtils .isDesktopServicesStore (subPlugin )) {
307374 continue ;
308375 }
309376 if (seen .add (subPlugin .getFileName ().toString ()) == false ) {
310377 throw new IllegalStateException ("duplicate plugin: " + subPlugin );
311378 }
312- plugins .add (subPlugin );
379+ metaPlugins . computeIfAbsent ( name , n -> new ArrayList <>()) .add (subPlugin );
313380 }
314381 }
315382 } else {
@@ -318,7 +385,7 @@ public static List<Path> findPluginDirs(final Path rootPath) throws IOException
318385 }
319386 }
320387 }
321- return plugins ;
388+ return Tuple . tuple ( plugins , metaPlugins ) ;
322389 }
323390
324391 /**
@@ -380,26 +447,46 @@ static void checkForFailedPluginRemovals(final Path pluginsDirectory) throws IOE
380447 * @throws IOException if an I/O exception occurs reading the plugin bundles
381448 */
382449 static Set <Bundle > getPluginBundles (final Path pluginsDirectory ) throws IOException {
383- Logger logger = Loggers . getLogger ( PluginsService . class );
384- Set < Bundle > bundles = new LinkedHashSet <>();
450+ return getPluginBundleCollections ( pluginsDirectory ). stream (). flatMap ( b -> b . bundles (). stream ()). collect ( Collectors . toSet () );
451+ }
385452
386- List <Path > infos = findPluginDirs (pluginsDirectory );
387- for (Path plugin : infos ) {
388- logger .trace ("--- adding plugin [{}]" , plugin .toAbsolutePath ());
389- final PluginInfo info ;
390- try {
391- info = PluginInfo .readFromProperties (plugin );
392- } catch (IOException e ) {
393- throw new IllegalStateException ("Could not load plugin descriptor for existing plugin ["
394- + plugin .getFileName () + "]. Was the plugin built before 2.0?" , e );
395- }
396- if (bundles .add (new Bundle (info , plugin )) == false ) {
397- throw new IllegalStateException ("duplicate plugin: " + info );
453+ private static List <BundleCollection > getPluginBundleCollections (final Path pluginsDirectory ) throws IOException {
454+ final List <BundleCollection > bundles = new ArrayList <>();
455+ final Set <Bundle > seenBundles = new HashSet <>();
456+ final Tuple <List <Path >, Map <String , List <Path >>> groupedPluginDirs = findGroupedPluginDirs (pluginsDirectory );
457+ for (final Path plugin : groupedPluginDirs .v1 ()) {
458+ final Bundle bundle = bundle (seenBundles , plugin );
459+ bundles .add (bundle );
460+ }
461+ for (final Map .Entry <String , List <Path >> metaPlugin : groupedPluginDirs .v2 ().entrySet ()) {
462+ final List <Bundle > metaPluginBundles = new ArrayList <>();
463+ for (final Path metaPluginPlugin : metaPlugin .getValue ()) {
464+ final Bundle bundle = bundle (seenBundles , metaPluginPlugin );
465+ metaPluginBundles .add (bundle );
398466 }
467+ final MetaBundle metaBundle = new MetaBundle (metaPlugin .getKey (), metaPluginBundles );
468+ bundles .add (metaBundle );
399469 }
470+
400471 return bundles ;
401472 }
402473
474+ private static Bundle bundle (final Set <Bundle > bundles , final Path plugin ) throws IOException {
475+ Loggers .getLogger (PluginsService .class ).trace ("--- adding plugin [{}]" , plugin .toAbsolutePath ());
476+ final PluginInfo info ;
477+ try {
478+ info = PluginInfo .readFromProperties (plugin );
479+ } catch (final IOException e ) {
480+ throw new IllegalStateException ("Could not load plugin descriptor for existing plugin ["
481+ + plugin .getFileName () + "]. Was the plugin built before 2.0?" , e );
482+ }
483+ final Bundle bundle = new Bundle (info , plugin );
484+ if (bundles .add (bundle ) == false ) {
485+ throw new IllegalStateException ("duplicate plugin: " + info );
486+ }
487+ return bundle ;
488+ }
489+
403490 /**
404491 * Return the given bundles, sorted in dependency loading order.
405492 *
0 commit comments