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

devonfw / IDEasy / 25721470472

12 May 2026 07:59AM UTC coverage: 70.666% (+0.04%) from 70.624%
25721470472

Pull #1891

github

web-flow
Merge ec5525ac8 into e6efbbdff
Pull Request #1891: #1880: Allow reset of installed plugins when launching IDE in force mode

4427 of 6920 branches covered (63.97%)

Branch coverage included in aggregate %.

11405 of 15484 relevant lines covered (73.66%)

3.12 hits per line

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

86.56
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.AbstractIdeContext;
15
import com.devonfw.tools.ide.context.IdeContext;
16
import com.devonfw.tools.ide.context.IdeStartContextImpl;
17
import com.devonfw.tools.ide.io.FileAccess;
18
import com.devonfw.tools.ide.process.ProcessContext;
19
import com.devonfw.tools.ide.process.ProcessErrorHandling;
20
import com.devonfw.tools.ide.property.FlagProperty;
21
import com.devonfw.tools.ide.step.Step;
22
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
23
import com.devonfw.tools.ide.tool.ToolInstallRequest;
24
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
25

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

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

33
  private ToolPlugins plugins;
34

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

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

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

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

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

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

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

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

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

76
    return this.plugins;
3✔
77
  }
78

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

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

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

94
    return false;
2✔
95
  }
96

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

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

105
  private Path getUserHomePluginsConfigPath() {
106

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

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

115
    return this.context.getPluginsPath().resolve(this.tool);
7✔
116
  }
117

118
  @Override
119
  protected void postInstall(ToolInstallRequest request) {
120

121
    super.postInstall(request);
3✔
122
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
123
    FileAccess fileAccess = this.context.getFileAccess();
4✔
124

125
    IdeStartContextImpl startContext = ((AbstractIdeContext) this.context).getStartContext();
5✔
126
    startContext.setForcePluginReinstall(this.forcePluginReinstall.isTrue());
5✔
127

128
    if (!request.isAlreadyInstalled() || this.context.isForcePluginReinstall()) {
7!
129
      LOG.debug("Deleting all installed plugins...");
3✔
130
      deleteAllPlugins(pluginsInstallationPath);
3✔
131
    }
132
    fileAccess.mkdirs(pluginsInstallationPath);
3✔
133
    installPlugins(request.getProcessContext());
4✔
134
  }
1✔
135

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

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

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

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

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

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

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

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

228
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
229

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

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

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

264
  /**
265
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
266
   */
267
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
268

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

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

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

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

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

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