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

devonfw / IDEasy / 29213236129

12 Jul 2026 11:20PM UTC coverage: 71.519% (-0.7%) from 72.225%
29213236129

Pull #2150

github

web-flow
Merge 9d5982c3b into 365f379a1
Pull Request #2150: #2097: Configure Ide Plugins in the GUI

4996 of 7716 branches covered (64.75%)

Branch coverage included in aggregate %.

12921 of 17336 relevant lines covered (74.53%)

3.16 hits per line

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

82.95
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.Collection;
6
import java.util.List;
7
import java.util.Properties;
8
import java.util.Set;
9

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

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

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

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

32
  private ToolPlugins plugins;
33

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

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

46
    super(context, tool, tags);
5✔
47
  }
1✔
48

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

55
  /**
56
   * @return the {@link ToolPlugins} of this {@link PluginBasedCommandlet}.
57
   */
58
  public ToolPlugins getPlugins() {
59

60
    if (this.plugins == null) {
3✔
61
      ToolPlugins toolPlugins = new ToolPlugins();
4✔
62

63
      // Load project-specific plugins
64
      Path pluginsPath = getPluginsConfigPath();
3✔
65
      loadPluginsFromDirectory(toolPlugins, pluginsPath);
4✔
66

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

72
      this.plugins = toolPlugins;
3✔
73
    }
74

75
    return this.plugins;
3✔
76
  }
77

78
  private void loadPluginsFromDirectory(ToolPlugins map, Path pluginsPath) {
79

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

88
  /**
89
   * @return {@code true} if {@link ToolPluginDescriptor#url() plugin URL} property is needed, {@code false} otherwise.
90
   */
91
  public boolean isPluginUrlNeeded() {
92

93
    return false;
2✔
94
  }
95

96
  /**
97
   * Creates a new plugin properties file in the project settings and returns the resulting {@link ToolPluginDescriptor}. The plugin is created as active by
98
   * default.
99
   *
100
   * @param name the plugin name (used as the properties file name).
101
   * @param id the plugin id (e.g. marketplace or extension identifier).
102
   * @param url the plugin URL (update site / marketplace URL); may be {@code null} when not needed.
103
   * @return the created {@link ToolPluginDescriptor}.
104
   */
105
  public ToolPluginDescriptor createPlugin(String name, String id, String url, String tags) {
106

107
    Path pluginsDir = getPluginsConfigPath();
3✔
108
    this.context.getFileAccess().mkdirs(pluginsDir);
5✔
109
    Path pluginFile = pluginsDir.resolve(name + IdeContext.EXT_PROPERTIES);
5✔
110
    Properties props = new Properties();
4✔
111
    if (id != null && !id.isBlank()) {
5!
112
      props.setProperty("id", id);
5✔
113
    }
114
    props.setProperty("active", "true");
5✔
115
    if (url != null && !url.isBlank()) {
5!
116
      props.setProperty("url", url);
5✔
117
    }
118
    if (tags != null && !tags.isBlank()) {
5!
119
      props.setProperty("tags", tags);
5✔
120
    }
121
    this.context.getFileAccess().writeProperties(props, pluginFile);
6✔
122
    this.plugins = null;
3✔
123
    return new ToolPluginDescriptor(id, name, url, null, true, Tag.parseCsv(tags));
11✔
124
  }
125

126
  /**
127
   * @return the {@link Path} to the folder with the plugin configuration files inside the settings.
128
   */
129
  public Path getPluginsConfigPath() {
130

131
    return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
9✔
132
  }
133

134
  private Path getUserHomePluginsConfigPath() {
135

136
    return this.context.getUserHomeIde().resolve(IdeContext.FOLDER_SETTINGS).resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
11✔
137
  }
138

139
  /**
140
   * @return the {@link Path} where the plugins of this {@link IdeToolCommandlet} shall be installed.
141
   */
142
  public Path getPluginsInstallationPath() {
143

144
    return this.context.getPluginsPath().resolve(this.tool);
7✔
145
  }
146

147
  @Override
148
  protected void postInstall(ToolInstallRequest request) {
149

150
    super.postInstall(request);
3✔
151
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
152

153
    if (!request.isAlreadyInstalled() || this.forcePluginReinstall.isTrue()) {
7!
154
      LOG.info("Resetting all installed plugins...");
3✔
155
      deleteAllPlugins(pluginsInstallationPath);
3✔
156
    }
157
    this.context.getFileAccess().mkdirs(pluginsInstallationPath);
5✔
158
    installPlugins(request.getProcessContext());
4✔
159
  }
1✔
160

161
  /**
162
   * Deletes all installed plugins for this {@link IdeToolCommandlet} by deleting the plugins installation folder and all plugin marker files.
163
   *
164
   * @param pluginsInstallationPath the {@link Path} to the plugins installation folder.
165
   */
166
  private void deleteAllPlugins(Path pluginsInstallationPath) {
167

168
    FileAccess fileAccess = this.context.getFileAccess();
4✔
169
    fileAccess.delete(pluginsInstallationPath);
3✔
170
    List<Path> markerFiles = fileAccess.listChildren(this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE), Files::isRegularFile);
14✔
171
    for (Path path : markerFiles) {
10✔
172
      if (path.getFileName().toString().startsWith("plugin." + getName())) {
8!
173
        fileAccess.delete(path);
3✔
174
        LOG.debug("Plugin marker file {} got deleted.", path);
4✔
175
      }
176
    }
1✔
177
  }
1✔
178

179
  private void installPlugins(ProcessContext pc) {
180
    installPlugins(getPlugins().getPlugins(), pc);
6✔
181
  }
1✔
182

183
  /**
184
   * Method to install active plugins or to handle install for inactive plugins
185
   *
186
   * @param plugins as {@link Collection} of plugins to install.
187
   * @param pc the {@link ProcessContext} to use.
188
   */
189
  protected void installPlugins(Collection<ToolPluginDescriptor> plugins, ProcessContext pc) {
190
    for (ToolPluginDescriptor plugin : plugins) {
10✔
191
      Path pluginMarkerFile = retrievePluginMarkerFilePath(plugin);
4✔
192
      boolean pluginMarkerFileExists = pluginMarkerFile != null && Files.exists(pluginMarkerFile);
11!
193
      if (pluginMarkerFileExists) {
2✔
194
        LOG.debug("Markerfile for IDE {} and plugin '{}' already exists.", getName(), plugin.name());
7✔
195
      }
196
      if (plugin.active()) {
3✔
197
        if (this.context.isForcePlugins() || !pluginMarkerFileExists) {
6✔
198
          Step step = this.context.newStep("Install plugin " + plugin.name());
7✔
199
          step.run(() -> doInstallPluginStep(plugin, step, pc));
14✔
200
        } else {
1✔
201
          LOG.debug("Skipping installation of plugin '{}' due to existing marker file: {}", plugin.name(), pluginMarkerFile);
7✔
202
        }
203
      } else {
204
        if (!pluginMarkerFileExists) {
2!
205
          handleInstallForInactivePlugin(plugin);
3✔
206
        }
207
      }
208
    }
1✔
209
  }
1✔
210

211
  private void doInstallPluginStep(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
212
    boolean result = installPlugin(plugin, step, pc);
6✔
213
    if (result) {
2!
214
      createPluginMarkerFile(plugin);
3✔
215
    }
216
  }
1✔
217

218
  /**
219
   * @param plugin the {@link ToolPluginDescriptor plugin} to search for.
220
   * @return Path to the plugin marker file.
221
   */
222
  public Path retrievePluginMarkerFilePath(ToolPluginDescriptor plugin) {
223
    if (this.context.getIdeHome() != null) {
4!
224
      String markerFileName = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
225
      String version = plugin.version();
3✔
226
      if ((version != null) && !version.isBlank()) {
5!
227
        markerFileName = markerFileName + ".version-" + normalizeMarkerFileSegment(version);
6✔
228
      }
229
      return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(markerFileName);
8✔
230
    }
231
    return null;
×
232
  }
233

234
  private String normalizeMarkerFileSegment(String value) {
235
    // replace all characters that are not allowed in filenames with "_"
236
    return value.replaceAll("[^A-Za-z0-9._-]", "_");
5✔
237
  }
238

239
  /**
240
   * Creates a marker file for a plugin in $IDE_HOME/.ide/plugin.«ide».«plugin-name»
241
   *
242
   * @param plugin the {@link ToolPluginDescriptor plugin} for which the marker file should be created.
243
   */
244
  public void createPluginMarkerFile(ToolPluginDescriptor plugin) {
245
    Path pluginMarkerFilePath = retrievePluginMarkerFilePath(plugin);
4✔
246
    if (pluginMarkerFilePath != null) {
2!
247
      FileAccess fileAccess = this.context.getFileAccess();
4✔
248
      fileAccess.mkdirs(pluginMarkerFilePath.getParent());
4✔
249
      deleteExistingPluginMarkerFiles(fileAccess, plugin, pluginMarkerFilePath);
5✔
250
      fileAccess.touch(pluginMarkerFilePath);
3✔
251
    }
252
  }
1✔
253

254
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
255

256
    String markerFilePrefix = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
257
    List<Path> markerFiles = fileAccess.listChildren(currentMarkerFilePath.getParent(),
7✔
258
        p -> {
259
          String fileName = p.getFileName().toString();
4✔
260
          return Files.isRegularFile(p) && (fileName.equals(markerFilePrefix) || fileName.startsWith(markerFilePrefix + ".version-"));
18!
261
        });
262
    for (Path markerFile : markerFiles) {
10✔
263
      if (!markerFile.equals(currentMarkerFilePath)) {
4✔
264
        fileAccess.delete(markerFile);
3✔
265
        LOG.debug("Deleted stale plugin marker file {} before creating {}.", markerFile, currentMarkerFilePath);
5✔
266
      }
267
    }
1✔
268
  }
1✔
269

270
  /**
271
   * @param plugin the {@link ToolPluginDescriptor} to install.
272
   * @param step the {@link Step} for the plugin installation.
273
   * @param pc the {@link ProcessContext} to use.
274
   * @return boolean true if the installation of the plugin succeeded, false if not.
275
   */
276
  public abstract boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc);
277

278
  /**
279
   * @param plugin the {@link ToolPluginDescriptor} to install.
280
   * @param step the {@link Step} for the plugin installation.
281
   */
282
  public void installPlugin(ToolPluginDescriptor plugin, final Step step) {
283
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
284
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
285
    request.setProcessContext(pc);
3✔
286
    install(request);
4✔
287
    installPlugin(plugin, step, pc);
6✔
288
  }
1✔
289

290
  /**
291
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
292
   */
293
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
294

295
    boolean error = false;
2✔
296
    Path pluginsPath = getPluginsInstallationPath();
3✔
297
    if (!Files.isDirectory(pluginsPath)) {
5!
298
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not exist at {}",
×
299
          plugin.name(), plugin.id(), pluginsPath);
×
300
      error = true;
×
301
    }
302
    FileAccess fileAccess = this.context.getFileAccess();
4✔
303
    Path match = fileAccess.findFirst(pluginsPath, p -> p.getFileName().toString().startsWith(plugin.id()), false);
7✔
304
    if (match == null) {
2!
305
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not contain a match at {}",
8✔
306
          plugin.name(), plugin.id(), pluginsPath);
11✔
307
      error = true;
2✔
308
    }
309
    if (error) {
2!
310
      LOG.error("Could not uninstall plugin {} because we could not find an installation", plugin);
5✔
311
    } else {
312
      fileAccess.delete(match);
×
313
      LOG.info("Successfully uninstalled plugin {}", plugin);
×
314
    }
315
  }
1✔
316

317
  /**
318
   * @param key the filename of the properties file configuring the requested plugin (typically excluding the ".properties" extension).
319
   * @return the {@link ToolPluginDescriptor} for the given {@code key}.
320
   */
321
  public ToolPluginDescriptor getPlugin(String key) {
322

323
    if (key == null) {
2!
324
      return null;
×
325
    }
326
    if (key.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
327
      key = key.substring(0, key.length() - IdeContext.EXT_PROPERTIES.length());
×
328
    }
329

330
    ToolPlugins toolPlugins = getPlugins();
3✔
331
    ToolPluginDescriptor pluginDescriptor = toolPlugins.getByName(key);
4✔
332
    if (pluginDescriptor == null) {
2!
333
      throw new CliException(
×
334
          "Could not find plugin " + key + " at " + getPluginsConfigPath().resolve(key) + ".properties");
×
335
    }
336
    return pluginDescriptor;
2✔
337
  }
338

339
  /**
340
   * Persists the {@link ToolPluginDescriptor#active() active} flag for the given plugin to its properties file in the project settings.
341
   *
342
   * @param plugin the {@link ToolPluginDescriptor} whose active state to save.
343
   * @param active {@code true} to activate, {@code false} to deactivate.
344
   */
345
  public void savePluginActive(ToolPluginDescriptor plugin, boolean active) {
346

347
    Path pluginFile = getPluginsConfigPath().resolve(plugin.name() + IdeContext.EXT_PROPERTIES);
×
348
    Properties props = this.context.getFileAccess().readProperties(pluginFile);
×
349
    String activeKey = props.containsKey("plugin_active") ? "plugin_active" : "active";
×
350
    props.setProperty(activeKey, Boolean.toString(active));
×
351
    this.context.getFileAccess().writeProperties(props, pluginFile);
×
352
  }
×
353

354
  /**
355
   * @param plugin the in{@link ToolPluginDescriptor#active() active} {@link ToolPluginDescriptor} that is skipped for regular plugin installation.
356
   */
357
  protected void handleInstallForInactivePlugin(ToolPluginDescriptor plugin) {
358

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

© 2026 Coveralls, Inc