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

devonfw / IDEasy / 25682298567

11 May 2026 04:14PM UTC coverage: 70.687% (-0.06%) from 70.743%
25682298567

Pull #1891

github

web-flow
Merge 5e9c01d8b into 8cec022f6
Pull Request #1891: #1880: Allow reset of installed plugins when launching IDE in force mode

4424 of 6914 branches covered (63.99%)

Branch coverage included in aggregate %.

11395 of 15465 relevant lines covered (73.68%)

3.12 hits per line

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

83.42
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.step.Step;
19
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
20
import com.devonfw.tools.ide.tool.ToolInstallRequest;
21
import com.devonfw.tools.ide.tool.ide.IdeToolCommandlet;
22

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

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

30
  private ToolPlugins plugins;
31

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

41
    super(context, tool, tags);
5✔
42
  }
1✔
43

44
  /**
45
   * @return the {@link ToolPlugins} of this {@link PluginBasedCommandlet}.
46
   */
47
  public ToolPlugins getPlugins() {
48

49
    if (this.plugins == null) {
3✔
50
      ToolPlugins toolPlugins = new ToolPlugins();
4✔
51

52
      // Load project-specific plugins
53
      Path pluginsPath = getPluginsConfigPath();
3✔
54
      loadPluginsFromDirectory(toolPlugins, pluginsPath);
4✔
55

56
      // Load user-specific plugins, this is done after loading the project-specific plugins so the user can potentially
57
      // override plugins (e.g. change active flag).
58
      Path userPluginsPath = getUserHomePluginsConfigPath();
3✔
59
      loadPluginsFromDirectory(toolPlugins, userPluginsPath);
4✔
60

61
      this.plugins = toolPlugins;
3✔
62
    }
63

64
    return this.plugins;
3✔
65
  }
66

67
  private void loadPluginsFromDirectory(ToolPlugins map, Path pluginsPath) {
68

69
    List<Path> children = this.context.getFileAccess()
5✔
70
        .listChildren(pluginsPath, p -> p.getFileName().toString().endsWith(IdeContext.EXT_PROPERTIES));
8✔
71
    for (Path child : children) {
10✔
72
      ToolPluginDescriptor descriptor = ToolPluginDescriptor.of(child, this.context, isPluginUrlNeeded());
7✔
73
      map.add(descriptor);
3✔
74
    }
1✔
75
  }
1✔
76

77
  /**
78
   * @return {@code true} if {@link ToolPluginDescriptor#url() plugin URL} property is needed, {@code false} otherwise.
79
   */
80
  protected boolean isPluginUrlNeeded() {
81

82
    return false;
2✔
83
  }
84

85
  /**
86
   * @return the {@link Path} to the folder with the plugin configuration files inside the settings.
87
   */
88
  protected Path getPluginsConfigPath() {
89

90
    return this.context.getSettingsPath().resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
9✔
91
  }
92

93
  private Path getUserHomePluginsConfigPath() {
94

95
    return this.context.getUserHomeIde().resolve(IdeContext.FOLDER_SETTINGS).resolve(this.tool).resolve(IdeContext.FOLDER_PLUGINS);
11✔
96
  }
97

98
  /**
99
   * @return the {@link Path} where the plugins of this {@link IdeToolCommandlet} shall be installed.
100
   */
101
  public Path getPluginsInstallationPath() {
102

103
    return this.context.getPluginsPath().resolve(this.tool);
7✔
104
  }
105

106
  @Override
107
  protected void postInstall(ToolInstallRequest request) {
108

109
    super.postInstall(request);
3✔
110
    Path pluginsInstallationPath = getPluginsInstallationPath();
3✔
111
    FileAccess fileAccess = this.context.getFileAccess();
4✔
112
    if (!request.isAlreadyInstalled()) {
3✔
113
      deleteAllPlugins(pluginsInstallationPath);
4✔
114
    } else if (this.context.isForceMode()) {
4!
115
      // Prompt user if they want to reset all plugins
116
      boolean resetPlugins = this.context.question(
×
117
        "You are launching " + getName() + " in force mode. Do you want to reset all plugins for " + getName() + "? "
×
118
        + "This will uninstall all currently installed plugins and reinstall them as configured in your IDEasy project settings.");
119
      if (resetPlugins) {
×
120
        deleteAllPlugins(pluginsInstallationPath);
×
121
      }
122
    }
123
    fileAccess.mkdirs(pluginsInstallationPath);
3✔
124
    installPlugins(request.getProcessContext());
4✔
125
  }
1✔
126

127
  private void deleteAllPlugins(Path pluginsInstallationPath) {
128

129
    FileAccess fileAccess = this.context.getFileAccess();
4✔
130
    fileAccess.delete(pluginsInstallationPath);
3✔
131
      List<Path> markerFiles = fileAccess.listChildren(this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE), Files::isRegularFile);
14✔
132
      for (Path path : markerFiles) {
10✔
133
        if (path.getFileName().toString().startsWith("plugin." + getName())) {
8!
134
          fileAccess.delete(path);
3✔
135
          LOG.debug("Plugin marker file {} got deleted.", path);
4✔
136
        }
137
      }
1✔
138

139
  }
1✔
140

141
  private void installPlugins(ProcessContext pc) {
142
    installPlugins(getPlugins().getPlugins(), pc);
6✔
143
  }
1✔
144

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

173
  private void doInstallPluginStep(ToolPluginDescriptor plugin, Step step, ProcessContext pc) {
174
    boolean result = installPlugin(plugin, step, pc);
6✔
175
    if (result) {
2!
176
      createPluginMarkerFile(plugin);
3✔
177
    }
178
  }
1✔
179

180
  /**
181
   * @param plugin the {@link ToolPluginDescriptor plugin} to search for.
182
   * @return Path to the plugin marker file.
183
   */
184
  public Path retrievePluginMarkerFilePath(ToolPluginDescriptor plugin) {
185
    if (this.context.getIdeHome() != null) {
4!
186
      String markerFileName = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
187
      String version = plugin.version();
3✔
188
      if ((version != null) && !version.isBlank()) {
5!
189
        markerFileName = markerFileName + ".version-" + normalizeMarkerFileSegment(version);
6✔
190
      }
191
      return this.context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve(markerFileName);
8✔
192
    }
193
    return null;
×
194
  }
195

196
  private String normalizeMarkerFileSegment(String value) {
197
    // replace all characters that are not allowed in filenames with "_"
198
    return value.replaceAll("[^A-Za-z0-9._-]", "_");
5✔
199
  }
200

201
  /**
202
   * Creates a marker file for a plugin in $IDE_HOME/.ide/plugin.«ide».«plugin-name»
203
   *
204
   * @param plugin the {@link ToolPluginDescriptor plugin} for which the marker file should be created.
205
   */
206
  public void createPluginMarkerFile(ToolPluginDescriptor plugin) {
207
    Path pluginMarkerFilePath = retrievePluginMarkerFilePath(plugin);
4✔
208
    if (pluginMarkerFilePath != null) {
2!
209
      FileAccess fileAccess = this.context.getFileAccess();
4✔
210
      fileAccess.mkdirs(pluginMarkerFilePath.getParent());
4✔
211
      deleteExistingPluginMarkerFiles(fileAccess, plugin, pluginMarkerFilePath);
5✔
212
      fileAccess.touch(pluginMarkerFilePath);
3✔
213
    }
214
  }
1✔
215

216
  private void deleteExistingPluginMarkerFiles(FileAccess fileAccess, ToolPluginDescriptor plugin, Path currentMarkerFilePath) {
217

218
    String markerFilePrefix = "plugin" + "." + getName() + "." + getInstalledEdition() + "." + plugin.name();
8✔
219
    List<Path> markerFiles = fileAccess.listChildren(currentMarkerFilePath.getParent(),
7✔
220
        p -> {
221
          String fileName = p.getFileName().toString();
4✔
222
          return Files.isRegularFile(p) && (fileName.equals(markerFilePrefix) || fileName.startsWith(markerFilePrefix + ".version-"));
18!
223
        });
224
    for (Path markerFile : markerFiles) {
10✔
225
      if (!markerFile.equals(currentMarkerFilePath)) {
4✔
226
        fileAccess.delete(markerFile);
3✔
227
        LOG.debug("Deleted stale plugin marker file {} before creating {}.", markerFile, currentMarkerFilePath);
5✔
228
      }
229
    }
1✔
230
  }
1✔
231

232
  /**
233
   * @param plugin the {@link ToolPluginDescriptor} to install.
234
   * @param step the {@link Step} for the plugin installation.
235
   * @param pc the {@link ProcessContext} to use.
236
   * @return boolean true if the installation of the plugin succeeded, false if not.
237
   */
238
  public abstract boolean installPlugin(ToolPluginDescriptor plugin, Step step, ProcessContext pc);
239

240
  /**
241
   * @param plugin the {@link ToolPluginDescriptor} to install.
242
   * @param step the {@link Step} for the plugin installation.
243
   */
244
  public void installPlugin(ToolPluginDescriptor plugin, final Step step) {
245
    ProcessContext pc = this.context.newProcess().errorHandling(ProcessErrorHandling.THROW_CLI);
6✔
246
    ToolInstallRequest request = new ToolInstallRequest(true);
5✔
247
    request.setProcessContext(pc);
3✔
248
    install(request);
4✔
249
    installPlugin(plugin, step, pc);
6✔
250
  }
1✔
251

252
  /**
253
   * @param plugin the {@link ToolPluginDescriptor} to uninstall.
254
   */
255
  public void uninstallPlugin(ToolPluginDescriptor plugin) {
256

257
    boolean error = false;
2✔
258
    Path pluginsPath = getPluginsInstallationPath();
3✔
259
    if (!Files.isDirectory(pluginsPath)) {
5!
260
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not exist at {}",
×
261
          plugin.name(), plugin.id(), pluginsPath);
×
262
      error = true;
×
263
    }
264
    FileAccess fileAccess = this.context.getFileAccess();
4✔
265
    Path match = fileAccess.findFirst(pluginsPath, p -> p.getFileName().toString().startsWith(plugin.id()), false);
7✔
266
    if (match == null) {
2!
267
      LOG.debug("Omitting to uninstall plugin {} ({}) as plugins folder does not contain a match at {}",
8✔
268
          plugin.name(), plugin.id(), pluginsPath);
11✔
269
      error = true;
2✔
270
    }
271
    if (error) {
2!
272
      LOG.error("Could not uninstall plugin {} because we could not find an installation", plugin);
5✔
273
    } else {
274
      fileAccess.delete(match);
×
275
      LOG.info("Successfully uninstalled plugin {}", plugin);
×
276
    }
277
  }
1✔
278

279
  /**
280
   * @param key the filename of the properties file configuring the requested plugin (typically excluding the ".properties" extension).
281
   * @return the {@link ToolPluginDescriptor} for the given {@code key}.
282
   */
283
  public ToolPluginDescriptor getPlugin(String key) {
284

285
    if (key == null) {
2!
286
      return null;
×
287
    }
288
    if (key.endsWith(IdeContext.EXT_PROPERTIES)) {
4!
289
      key = key.substring(0, key.length() - IdeContext.EXT_PROPERTIES.length());
×
290
    }
291

292
    ToolPlugins toolPlugins = getPlugins();
3✔
293
    ToolPluginDescriptor pluginDescriptor = toolPlugins.getByName(key);
4✔
294
    if (pluginDescriptor == null) {
2!
295
      throw new CliException(
×
296
          "Could not find plugin " + key + " at " + getPluginsConfigPath().resolve(key) + ".properties");
×
297
    }
298
    return pluginDescriptor;
2✔
299
  }
300

301
  /**
302
   * @param plugin the in{@link ToolPluginDescriptor#active() active} {@link ToolPluginDescriptor} that is skipped for regular plugin installation.
303
   */
304
  protected void handleInstallForInactivePlugin(ToolPluginDescriptor plugin) {
305

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