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

devonfw / IDEasy / 30072174238

24 Jul 2026 06:24AM UTC coverage: 72.588% (+0.03%) from 72.561%
30072174238

Pull #2195

github

web-flow
Merge 2561b9c45 into dd45e9162
Pull Request #2195: #2181: Merge VSCodium plugins folder to VSCode

4985 of 7588 branches covered (65.7%)

Branch coverage included in aggregate %.

12839 of 16967 relevant lines covered (75.67%)

3.2 hits per line

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

86.98
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.Set;
8

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

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

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

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

31
  private ToolPlugins plugins;
32

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

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

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

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

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

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

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

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

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

74
    return this.plugins;
3✔
75
  }
76

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

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

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

92
    return false;
2✔
93
  }
94

95
  /**
96
   * @return the {@link Path} to the folder with the plugin configuration files inside the settings.
97
   */
98
  protected Path getPluginsConfigPath() {
99

100
    return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
9✔
101
  }
102

103
  private Path getUserHomePluginsConfigPath() {
104

105
    return this.context.getUserHomeIde().resolve(IdeContext.FOLDER_SETTINGS).resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
11✔
106
  }
107

108
  /**
109
   * @return the {@link Path} where the plugins of this {@link IdeToolCommandlet} shall be installed.
110
   */
111
  public Path getPluginsInstallationPath() {
112

113
    return this.context.getPluginsPath().resolve(this.tool);
7✔
114
  }
115

116
  @Override
117
  protected void postInstall(ToolInstallRequest request) {
118

119
    super.postInstall(request);
3✔
120
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
121

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

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

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

148
  private void installPlugins(ProcessContext pc) {
149
    installPlugins(getPlugins().getPlugins(), pc);
6✔
150
  }
1✔
151

152
  /**
153
   * Method to install active plugins or to handle install for inactive plugins
154
   *
155
   * @param plugins as {@link Collection} of plugins to install.
156
   * @param pc the {@link ProcessContext} to use.
157
   */
158
  protected void installPlugins(Collection<ToolPluginDescriptor> plugins, ProcessContext pc) {
159
    long currentPluginIndex = 1;
2✔
160
    long totalActivePlugins = plugins.stream().filter(ToolPluginDescriptor::active).count();
6✔
161
    for (ToolPluginDescriptor plugin : plugins) {
10✔
162
      if (plugin.excludedEditions().contains(getConfiguredEdition())) {
6✔
163
        LOG.debug("Skipping plugin '{}' (excluded for edition '{}').", plugin.name(), getConfiguredEdition());
7✔
164
        continue;
1✔
165
      }
166
      Path pluginMarkerFile = retrievePluginMarkerFilePath(plugin);
4✔
167
      boolean pluginMarkerFileExists = pluginMarkerFile != null && Files.exists(pluginMarkerFile);
11!
168
      if (pluginMarkerFileExists) {
2✔
169
        LOG.debug("Markerfile for IDE {} and plugin '{}' already exists.", getName(), plugin.name());
7✔
170
      }
171
      if (plugin.active()) {
3✔
172
        if (this.context.isForcePlugins() || !pluginMarkerFileExists) {
6✔
173
          String progressMarker = " (" + currentPluginIndex + "/" + totalActivePlugins + ")";
4✔
174
          Step step = this.context.newStep("Install plugin " + plugin.name() + progressMarker);
8✔
175
          step.run(() -> doInstallPluginStep(plugin, step, pc));
14✔
176
        } else {
1✔
177
          LOG.debug("Skipping installation of plugin '{}' due to existing marker file: {}", plugin.name(), pluginMarkerFile);
6✔
178
        }
179
        currentPluginIndex++;
5✔
180
      } else {
181
        if (!pluginMarkerFileExists) {
2!
182
          handleInstallForInactivePlugin(plugin);
3✔
183
        }
184
      }
185
    }
1✔
186
  }
1✔
187

188
  private void doInstallPluginStep(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
189
    boolean result = installPlugin(plugin, step, pc);
6✔
190
    if (result) {
2!
191
      createPluginMarkerFile(plugin);
3✔
192
    }
193
  }
1✔
194

195
  /**
196
   * @param plugin the {@link ToolPluginDescriptor plugin} to search for.
197
   * @return Path to the plugin marker file.
198
   */
199
  public Path retrievePluginMarkerFilePath(ToolPluginDescriptor plugin) {
200
    if (this.context.getIdeHome() != null) {
4!
201
      String markerFileName = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
202
      String version = plugin.version();
3✔
203
      if ((version != null) && !version.isBlank()) {
5!
204
        markerFileName = markerFileName + ".version-" + normalizeMarkerFileSegment(version);
6✔
205
      }
206
      return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(markerFileName);
8✔
207
    }
208
    return null;
×
209
  }
210

211
  private String normalizeMarkerFileSegment(String value) {
212
    // replace all characters that are not allowed in filenames with "_"
213
    return value.replaceAll("[^A-Za-z0-9._-]", "_");
5✔
214
  }
215

216
  /**
217
   * Creates a marker file for a plugin in $IDE_HOME/.ide/plugin.«ide».«plugin-name»
218
   *
219
   * @param plugin the {@link ToolPluginDescriptor plugin} for which the marker file should be created.
220
   */
221
  public void createPluginMarkerFile(ToolPluginDescriptor plugin) {
222
    Path pluginMarkerFilePath = retrievePluginMarkerFilePath(plugin);
4✔
223
    if (pluginMarkerFilePath != null) {
2!
224
      FileAccess fileAccess = this.context.getFileAccess();
4✔
225
      fileAccess.mkdirs(pluginMarkerFilePath.getParent());
4✔
226
      deleteExistingPluginMarkerFiles(fileAccess, plugin, pluginMarkerFilePath);
5✔
227
      fileAccess.touch(pluginMarkerFilePath);
3✔
228
    }
229
  }
1✔
230

231
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
232

233
    String markerFilePrefix = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
234
    List<Path> markerFiles = fileAccess.listChildren(currentMarkerFilePath.getParent(),
7✔
235
        p -> {
236
          String fileName = p.getFileName().toString();
4✔
237
          return Files.isRegularFile(p) && (fileName.equals(markerFilePrefix) || fileName.startsWith(markerFilePrefix + ".version-"));
18!
238
        });
239
    for (Path markerFile : markerFiles) {
10✔
240
      if (!markerFile.equals(currentMarkerFilePath)) {
4✔
241
        fileAccess.delete(markerFile);
3✔
242
        LOG.debug("Deleted stale plugin marker file {} before creating {}.", markerFile, currentMarkerFilePath);
5✔
243
      }
244
    }
1✔
245
  }
1✔
246

247
  /**
248
   * @param plugin the {@link ToolPluginDescriptor} to install.
249
   * @param step the {@link Step} for the plugin installation.
250
   * @param pc the {@link ProcessContext} to use.
251
   * @return boolean true if the installation of the plugin succeeded, false if not.
252
   */
253
  public abstract boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc);
254

255
  /**
256
   * @param plugin the {@link ToolPluginDescriptor} to install.
257
   * @param step the {@link Step} for the plugin installation.
258
   */
259
  public void installPlugin(ToolPluginDescriptor plugin, final Step step) {
260
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
261
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
262
    request.setProcessContext(pc);
3✔
263
    install(request);
4✔
264
    installPlugin(plugin, step, pc);
6✔
265
  }
1✔
266

267
  /**
268
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
269
   */
270
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
271

272
    boolean error = false;
2✔
273
    Path pluginsPath = getPluginsInstallationPath();
3✔
274
    if (!Files.isDirectory(pluginsPath)) {
5!
275
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not exist at {}",
×
276
          plugin.name(), plugin.id(), pluginsPath);
×
277
      error = true;
×
278
    }
279
    FileAccess fileAccess = this.context.getFileAccess();
4✔
280
    Path match = fileAccess.findFirst(pluginsPath, p -> p.getFileName().toString().startsWith(plugin.id()), false);
7✔
281
    if (match == null) {
2!
282
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not contain a match at {}",
8✔
283
          plugin.name(), plugin.id(), pluginsPath);
11✔
284
      error = true;
2✔
285
    }
286
    if (error) {
2!
287
      LOG.error("Could not uninstall plugin {} because we could not find an installation", plugin);
5✔
288
    } else {
289
      fileAccess.delete(match);
×
290
      LOG.info("Successfully uninstalled plugin {}", plugin);
×
291
    }
292
  }
1✔
293

294
  /**
295
   * @param key the filename of the properties file configuring the requested plugin (typically excluding the ".properties" extension).
296
   * @return the {@link ToolPluginDescriptor} for the given {@code key}.
297
   */
298
  public ToolPluginDescriptor getPlugin(String key) {
299

300
    if (key == null) {
2!
301
      return null;
×
302
    }
303
    if (key.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
304
      key = key.substring(0, key.length() - IdeContext.EXT_PROPERTIES.length());
×
305
    }
306

307
    ToolPlugins toolPlugins = getPlugins();
3✔
308
    ToolPluginDescriptor pluginDescriptor = toolPlugins.getByName(key);
4✔
309
    if (pluginDescriptor == null) {
2!
310
      throw new CliException(
×
311
          "Could not find plugin " + key + " at " + getPluginsConfigPath().resolve(key) + ".properties");
×
312
    }
313
    return pluginDescriptor;
2✔
314
  }
315

316
  /**
317
   * @param plugin the in{@link ToolPluginDescriptor#active() active} {@link ToolPluginDescriptor} that is skipped for regular plugin installation.
318
   */
319
  protected void handleInstallForInactivePlugin(ToolPluginDescriptor plugin) {
320

321
    LOG.debug("Omitting installation of inactive plugin {} ({}).", plugin.name(), plugin.id());
7✔
322
  }
1✔
323
}
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