• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

devonfw / IDEasy / 32478854936

21 Aug 2026 11:47AM UTC coverage: 73.563% (-0.002%) from 73.565%
32478854936

Pull #2237

github

web-flow
Merge 52dac2a95 into ab68bc7b6
Pull Request #2237: #2180: Add *_EXTRA_PLUGINS variable to install extra IDE plugins per user

5195 of 7881 branches covered (65.92%)

Branch coverage included in aggregate %.

13682 of 17780 relevant lines covered (76.95%)

3.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.66
cli/src/main/java/com/devonfw/tools/ide/tool/plugin/PluginBasedCommandlet.java
1
package com.devonfw.tools.ide.tool.plugin;
2

3
import java.nio.file.Files;
4
import java.nio.file.Path;
5
import java.util.ArrayList;
6
import java.util.Collection;
7
import java.util.LinkedHashSet;
8
import java.util.List;
9
import java.util.Set;
10

11
import org.slf4j.Logger;
12
import org.slf4j.LoggerFactory;
13

14
import com.devonfw.tools.ide.cli.CliException;
15
import com.devonfw.tools.ide.common.Tag;
16
import com.devonfw.tools.ide.context.IdeContext;
17
import com.devonfw.tools.ide.environment.EnvironmentVariables;
18
import com.devonfw.tools.ide.environment.VariableLine;
19
import com.devonfw.tools.ide.io.FileAccess;
20
import com.devonfw.tools.ide.process.ProcessContext;
21
import com.devonfw.tools.ide.process.ProcessErrorHandling;
22
import com.devonfw.tools.ide.property.FlagProperty;
23
import com.devonfw.tools.ide.step.Step;
24
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
25
import com.devonfw.tools.ide.tool.ToolInstallRequest;
26
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
27

28
/**
29
 * Base class for {@link LocalToolCommandlet}s that support plugins. It can automatically install configured plugins for the tool managed by this commandlet.
30
 */
31
public abstract class PluginBasedCommandlet extends LocalToolCommandlet {
32

33
  private static final Logger LOG = LoggerFactory.getLogger(PluginBasedCommandlet.class);
4✔
34

35
  private ToolPlugins plugins;
36

37
  /** {@link FlagProperty} to force the reset and reinstallation of plugins as configured in the project settings. */
38
  public FlagProperty forcePluginReinstall;
39

40
  /**
41
   * The constructor.
42
   *
43
   * @param context the {@link IdeContext}.
44
   * @param tool the {@link #getName() tool name}.
45
   * @param tags the {@link #getTags() tags} classifying the tool. Should be created via {@link Set#of(Object) Set.of} method.
46
   */
47
  public PluginBasedCommandlet(IdeContext context, String tool, Set<Tag> tags) {
48

49
    super(context, tool, tags);
5✔
50
  }
1✔
51

52
  @Override
53
  protected void initProperties() {
54
    this.forcePluginReinstall = add(new FlagProperty("--force-plugin-reinstall"));
9✔
55
    super.initProperties();
2✔
56
  }
1✔
57

58
  /**
59
   * @return the {@link ToolPlugins} of this {@link PluginBasedCommandlet}.
60
   */
61
  public ToolPlugins getPlugins() {
62

63
    if (this.plugins == null) {
3✔
64
      ToolPlugins toolPlugins = new ToolPlugins();
4✔
65

66
      // Load project-specific plugins
67
      Path pluginsPath = getPluginsConfigPath();
3✔
68
      loadPluginsFromDirectory(toolPlugins, pluginsPath);
4✔
69

70
      // Load user-specific plugins, this is done after loading the project-specific plugins so the user can potentially
71
      // override plugins (e.g. change active flag).
72
      Path userPluginsPath = getUserHomePluginsConfigPath();
3✔
73
      loadPluginsFromDirectory(toolPlugins, userPluginsPath);
4✔
74

75
      this.plugins = toolPlugins;
3✔
76
    }
77

78
    return this.plugins;
3✔
79
  }
80

81
  private void loadPluginsFromDirectory(ToolPlugins map, Path pluginsPath) {
82

83
    List<Path> children = this.context.getFileAccess()
5✔
84
        .listChildren(pluginsPath, p -> p.getFileName().toString().endsWith(IdeContext.EXT_PROPERTIES));
8✔
85
    for (Path child : children) {
10✔
86
      ToolPluginDescriptor descriptor = ToolPluginDescriptor.of(child, this.context, isPluginUrlNeeded());
7✔
87
      map.add(descriptor);
3✔
88
    }
1✔
89
  }
1✔
90

91
  /**
92
   * @return {@code true} if {@link ToolPluginDescriptor#url() plugin URL} property is needed, {@code false} otherwise.
93
   */
94
  protected boolean isPluginUrlNeeded() {
95

96
    return false;
2✔
97
  }
98

99
  /**
100
   * @return the {@link Path} to the folder with the plugin configuration files inside the settings.
101
   */
102
  protected Path getPluginsConfigPath() {
103

104
    return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
9✔
105
  }
106

107
  private Path getUserHomePluginsConfigPath() {
108

109
    return this.context.getUserHomeIde().resolve(IdeContext.FOLDER_SETTINGS).resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
11✔
110
  }
111

112
  /**
113
   * @return the {@link Path} where the plugins of this {@link IdeToolCommandlet} shall be installed.
114
   */
115
  public Path getPluginsInstallationPath() {
116

117
    return this.context.getPluginsPath().resolve(this.tool);
7✔
118
  }
119

120
  @Override
121
  protected void postInstall(ToolInstallRequest request) {
122

123
    super.postInstall(request);
3✔
124
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
125

126
    if (!request.isAlreadyInstalled() || this.forcePluginReinstall.isTrue()) {
7!
127
      LOG.info("Resetting all installed plugins...");
3✔
128
      deleteAllPlugins(pluginsInstallationPath);
3✔
129
    }
130
    this.context.getFileAccess().mkdirs(pluginsInstallationPath);
5✔
131
    installPlugins(request.getProcessContext());
4✔
132
  }
1✔
133

134
  /**
135
   * Deletes all installed plugins for this {@link IdeToolCommandlet} by deleting the plugins installation folder and all plugin marker files.
136
   *
137
   * @param pluginsInstallationPath the {@link Path} to the plugins installation folder.
138
   */
139
  private void deleteAllPlugins(Path pluginsInstallationPath) {
140

141
    FileAccess fileAccess = this.context.getFileAccess();
4✔
142
    fileAccess.delete(pluginsInstallationPath);
3✔
143
    List<Path> markerFiles = fileAccess.listChildren(this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE), Files::isRegularFile);
14✔
144
    for (Path path : markerFiles) {
10✔
145
      if (path.getFileName().toString().startsWith("plugin." + getName())) {
8!
146
        fileAccess.delete(path);
3✔
147
        LOG.debug("Plugin marker file {} got deleted.", path);
4✔
148
      }
149
    }
1✔
150
  }
1✔
151

152
  private void installPlugins(ProcessContext pc) {
153
    installPlugins(getPlugins().getPlugins(), pc);
6✔
154
  }
1✔
155

156
  /**
157
   * Method to install active plugins or to handle install for inactive plugins
158
   *
159
   * @param plugins as {@link Collection} of plugins to install.
160
   * @param pc the {@link ProcessContext} to use.
161
   */
162
  protected void installPlugins(Collection<ToolPluginDescriptor> plugins, ProcessContext pc) {
163

164
    Set<String> extraPlugins = getExtraPlugins(plugins);
4✔
165
    String edition = getConfiguredEdition();
3✔
166
    List<ToolPluginDescriptor> pluginsToInstall = new ArrayList<>(plugins.size());
6✔
167
    for (ToolPluginDescriptor plugin : plugins) {
10✔
168
      if (plugin.excludedEditions().contains(edition)) {
5✔
169
        LOG.debug("Skipping plugin '{}' (excluded for edition '{}').", plugin.name(), edition);
7✔
170
      } else if (plugin.active() || extraPlugins.contains(plugin.name())) {
8✔
171
        pluginsToInstall.add(plugin);
5✔
172
      } else {
173
        Path pluginMarkerFile = retrievePluginMarkerFilePath(plugin);
4✔
174
        if ((pluginMarkerFile == null) || !Files.exists(pluginMarkerFile)) {
7!
175
          handleInstallForInactivePlugin(plugin);
3✔
176
        }
177
      }
178
    }
1✔
179
    int currentPluginIndex = 1;
2✔
180
    int totalPlugins = pluginsToInstall.size();
3✔
181
    for (ToolPluginDescriptor plugin : pluginsToInstall) {
10✔
182
      Path pluginMarkerFile = retrievePluginMarkerFilePath(plugin);
4✔
183
      boolean pluginMarkerFileExists = (pluginMarkerFile != null) && Files.exists(pluginMarkerFile);
11!
184
      if (pluginMarkerFileExists) {
2✔
185
        LOG.debug("Markerfile for IDE {} and plugin '{}' already exists.", getName(), plugin.name());
7✔
186
      }
187
      if (this.context.isForcePlugins() || !pluginMarkerFileExists) {
6✔
188
        String progressMarker = " (" + currentPluginIndex + "/" + totalPlugins + ")";
4✔
189
        Step step = this.context.newStep("Install plugin " + plugin.name() + progressMarker);
8✔
190
        step.run(() -> doInstallPluginStep(plugin, step, pc));
14✔
191
      } else {
1✔
192
        LOG.debug("Skipping installation of plugin '{}' due to existing marker file: {}", plugin.name(), pluginMarkerFile);
6✔
193
      }
194
      currentPluginIndex++;
1✔
195
    }
1✔
196
  }
1✔
197

198
  /**
199
   * @param plugins the configured {@link ToolPluginDescriptor plugins} used to detect undefined entries.
200
   * @return the {@link Set} of {@link ToolPluginDescriptor#name() plugin names} configured in the tool-specific {@code «TOOL»_EXTRA_PLUGINS} variable (e.g.
201
   *     {@code VSCODE_EXTRA_PLUGINS=copilot,docker}). This allows a user to permanently opt-in to plugins that are not {@link ToolPluginDescriptor#active()
202
   *     active} in the project settings, without modifying the shared settings and without losing them when plugins are purged and reinstalled on IDE upgrade.
203
   *     Values refer to the {@link ToolPluginDescriptor#name() name} of the plugin (the filename of its {@code .properties} file) and not to the
204
   *     {@link ToolPluginDescriptor#id() id}. Names that do not resolve to a configured plugin are logged as a warning and skipped so that a single stale entry
205
   *     cannot break the entire installation.
206
   */
207
  protected Set<String> getExtraPlugins(Collection<ToolPluginDescriptor> plugins) {
208

209
    String variable = EnvironmentVariables.getToolExtraPluginsVariable(this.tool);
4✔
210
    String value = this.context.getVariables().get(variable);
6✔
211
    if ((value == null) || value.isBlank()) {
5!
212
      return Set.of();
2✔
213
    }
214
    Set<String> extraPlugins = new LinkedHashSet<>();
4✔
215
    for (String name : VariableLine.parseArray(value)) {
11✔
216
      if (name.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
217
        name = name.substring(0, name.length() - IdeContext.EXT_PROPERTIES.length());
×
218
      }
219
      extraPlugins.add(name);
4✔
220
    }
1✔
221
    Set<String> undefinedPlugins = new LinkedHashSet<>(extraPlugins);
5✔
222
    for (ToolPluginDescriptor plugin : plugins) {
10✔
223
      undefinedPlugins.remove(plugin.name());
5✔
224
    }
1✔
225
    for (String name : undefinedPlugins) {
10✔
226
      LOG.info("Ignoring undefined plugin '{}' configured in variable {} - no file {}{} found in {} or {}.", name, variable, name, IdeContext.EXT_PROPERTIES,
24✔
227
          getPluginsConfigPath(), getUserHomePluginsConfigPath());
7✔
228
    }
1✔
229
    return extraPlugins;
2✔
230
  }
231

232
  private void doInstallPluginStep(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
233
    boolean result = installPlugin(plugin, step, pc);
6✔
234
    if (result) {
2!
235
      createPluginMarkerFile(plugin);
3✔
236
    }
237
  }
1✔
238

239
  /**
240
   * @param plugin the {@link ToolPluginDescriptor plugin} to search for.
241
   * @return Path to the plugin marker file.
242
   */
243
  public Path retrievePluginMarkerFilePath(ToolPluginDescriptor plugin) {
244
    if (this.context.getIdeHome() != null) {
4!
245
      String markerFileName = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
246
      String version = plugin.version();
3✔
247
      if ((version != null) && !version.isBlank()) {
5!
248
        markerFileName = markerFileName + ".version-" + normalizeMarkerFileSegment(version);
6✔
249
      }
250
      return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(markerFileName);
8✔
251
    }
252
    return null;
×
253
  }
254

255
  private String normalizeMarkerFileSegment(String value) {
256
    // replace all characters that are not allowed in filenames with "_"
257
    return value.replaceAll("[^A-Za-z0-9._-]", "_");
5✔
258
  }
259

260
  /**
261
   * Creates a marker file for a plugin in $IDE_HOME/.ide/plugin.«ide».«plugin-name»
262
   *
263
   * @param plugin the {@link ToolPluginDescriptor plugin} for which the marker file should be created.
264
   */
265
  public void createPluginMarkerFile(ToolPluginDescriptor plugin) {
266
    Path pluginMarkerFilePath = retrievePluginMarkerFilePath(plugin);
4✔
267
    if (pluginMarkerFilePath != null) {
2!
268
      FileAccess fileAccess = this.context.getFileAccess();
4✔
269
      fileAccess.mkdirs(pluginMarkerFilePath.getParent());
4✔
270
      deleteExistingPluginMarkerFiles(fileAccess, plugin, pluginMarkerFilePath);
5✔
271
      fileAccess.touch(pluginMarkerFilePath);
3✔
272
    }
273
  }
1✔
274

275
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
276

277
    String markerFilePrefix = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
278
    List<Path> markerFiles = fileAccess.listChildren(currentMarkerFilePath.getParent(),
7✔
279
        p -> {
280
          String fileName = p.getFileName().toString();
4✔
281
          return Files.isRegularFile(p) && (fileName.equals(markerFilePrefix) || fileName.startsWith(markerFilePrefix + ".version-"));
18!
282
        });
283
    for (Path markerFile : markerFiles) {
10✔
284
      if (!markerFile.equals(currentMarkerFilePath)) {
4✔
285
        fileAccess.delete(markerFile);
3✔
286
        LOG.debug("Deleted stale plugin marker file {} before creating {}.", markerFile, currentMarkerFilePath);
5✔
287
      }
288
    }
1✔
289
  }
1✔
290

291
  /**
292
   * @param plugin the {@link ToolPluginDescriptor} to install.
293
   * @param step the {@link Step} for the plugin installation.
294
   * @param pc the {@link ProcessContext} to use.
295
   * @return boolean true if the installation of the plugin succeeded, false if not.
296
   */
297
  public abstract boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc);
298

299
  /**
300
   * @param plugin the {@link ToolPluginDescriptor} to install.
301
   * @param step the {@link Step} for the plugin installation.
302
   */
303
  public void installPlugin(ToolPluginDescriptor plugin, final Step step) {
304
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
305
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
306
    request.setProcessContext(pc);
3✔
307
    install(request);
4✔
308
    installPlugin(plugin, step, pc);
6✔
309
  }
1✔
310

311
  /**
312
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
313
   */
314
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
315

316
    boolean error = false;
2✔
317
    Path pluginsPath = getPluginsInstallationPath();
3✔
318
    if (!Files.isDirectory(pluginsPath)) {
5!
319
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not exist at {}",
×
320
          plugin.name(), plugin.id(), pluginsPath);
×
321
      error = true;
×
322
    }
323
    FileAccess fileAccess = this.context.getFileAccess();
4✔
324
    Path match = fileAccess.findFirst(pluginsPath, p -> p.getFileName().toString().startsWith(plugin.id()), false);
7✔
325
    if (match == null) {
2!
326
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not contain a match at {}",
8✔
327
          plugin.name(), plugin.id(), pluginsPath);
11✔
328
      error = true;
2✔
329
    }
330
    if (error) {
2!
331
      LOG.error("Could not uninstall plugin {} because we could not find an installation", plugin);
5✔
332
    } else {
333
      fileAccess.delete(match);
×
334
      LOG.info("Successfully uninstalled plugin {}", plugin);
×
335
    }
336
  }
1✔
337

338
  /**
339
   * @param key the filename of the properties file configuring the requested plugin (typically excluding the ".properties" extension).
340
   * @return the {@link ToolPluginDescriptor} for the given {@code key}.
341
   */
342
  public ToolPluginDescriptor getPlugin(String key) {
343

344
    if (key == null) {
2!
345
      return null;
×
346
    }
347
    if (key.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
348
      key = key.substring(0, key.length() - IdeContext.EXT_PROPERTIES.length());
×
349
    }
350

351
    ToolPlugins toolPlugins = getPlugins();
3✔
352
    ToolPluginDescriptor pluginDescriptor = toolPlugins.getByName(key);
4✔
353
    if (pluginDescriptor == null) {
2!
354
      throw new CliException(
×
355
          "Could not find plugin " + key + " at " + getPluginsConfigPath().resolve(key) + ".properties");
×
356
    }
357
    return pluginDescriptor;
2✔
358
  }
359

360
  /**
361
   * @param plugin the in{@link ToolPluginDescriptor#active() active} {@link ToolPluginDescriptor} that is skipped for regular plugin installation.
362
   */
363
  protected void handleInstallForInactivePlugin(ToolPluginDescriptor plugin) {
364

365
    LOG.debug("Omitting installation of inactive plugin {} ({}).", plugin.name(), plugin.id());
7✔
366
  }
1✔
367
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc