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

devonfw / IDEasy / 22241505980

20 Feb 2026 09:16PM UTC coverage: 70.656% (+0.2%) from 70.474%
22241505980

Pull #1710

github

web-flow
Merge 04e4bdacd into 379acdc9d
Pull Request #1710: #404: allow logging via SLF4J

4121 of 6440 branches covered (63.99%)

Branch coverage included in aggregate %.

10704 of 14542 relevant lines covered (73.61%)

3.13 hits per line

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

93.82
cli/src/main/java/com/devonfw/tools/ide/environment/EnvironmentVariablesPropertiesFile.java
1
package com.devonfw.tools.ide.environment;
2

3
import java.io.BufferedReader;
4
import java.io.BufferedWriter;
5
import java.io.IOException;
6
import java.nio.file.Files;
7
import java.nio.file.Path;
8
import java.util.ArrayList;
9
import java.util.HashMap;
10
import java.util.HashSet;
11
import java.util.List;
12
import java.util.Map;
13
import java.util.Objects;
14
import java.util.Set;
15

16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18

19
import com.devonfw.tools.ide.context.IdeContext;
20
import com.devonfw.tools.ide.variable.IdeVariables;
21
import com.devonfw.tools.ide.variable.VariableDefinition;
22

23
/**
24
 * Implementation of {@link EnvironmentVariables}.
25
 */
26
public final class EnvironmentVariablesPropertiesFile extends EnvironmentVariablesMap {
27

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

30
  private static final String NEWLINE = "\n";
31

32
  private final EnvironmentVariablesType type;
33

34
  private final Path propertiesFilePath;
35

36
  private final Path legacyPropertiesFilePath;
37

38
  private final Map<String, String> variables;
39

40
  private final Set<String> exportedVariables;
41

42
  private final Set<String> modifiedVariables;
43

44
  private Boolean legacyConfiguration;
45

46
  /**
47
   * The constructor.
48
   *
49
   * @param parent the parent {@link EnvironmentVariables} to inherit from.
50
   * @param type the {@link #getType() type}.
51
   * @param propertiesFilePath the {@link #getSource() source}.
52
   * @param context the {@link IdeContext}.
53
   */
54
  public EnvironmentVariablesPropertiesFile(AbstractEnvironmentVariables parent, EnvironmentVariablesType type,
55
      Path propertiesFilePath, IdeContext context) {
56

57
    this(parent, type, getParent(propertiesFilePath), propertiesFilePath, context);
8✔
58
  }
1✔
59

60
  /**
61
   * The constructor.
62
   *
63
   * @param parent the parent {@link EnvironmentVariables} to inherit from.
64
   * @param type the {@link #getType() type}.
65
   * @param propertiesFolderPath the {@link Path} to the folder where the properties file is expected.
66
   * @param propertiesFilePath the {@link #getSource() source}.
67
   * @param context the {@link IdeContext}.
68
   */
69
  public EnvironmentVariablesPropertiesFile(AbstractEnvironmentVariables parent, EnvironmentVariablesType type, Path propertiesFolderPath,
70
      Path propertiesFilePath, IdeContext context) {
71

72
    super(parent, context);
4✔
73
    Objects.requireNonNull(type);
3✔
74
    assert (type != EnvironmentVariablesType.RESOLVED);
4!
75
    this.type = type;
3✔
76
    if (propertiesFolderPath == null) {
2✔
77
      this.propertiesFilePath = null;
3✔
78
      this.legacyPropertiesFilePath = null;
4✔
79
    } else {
80
      if (propertiesFilePath == null) {
2✔
81
        this.propertiesFilePath = propertiesFolderPath.resolve(DEFAULT_PROPERTIES);
6✔
82
      } else {
83
        this.propertiesFilePath = propertiesFilePath;
3✔
84
        assert (propertiesFilePath.getParent().equals(propertiesFolderPath));
6!
85
      }
86
      Path legacyPropertiesFolderPath = propertiesFolderPath;
2✔
87
      if (type == EnvironmentVariablesType.USER) {
3✔
88
        // ~/devon.properties vs. ~/.ide/ide.properties
89
        legacyPropertiesFolderPath = propertiesFolderPath.getParent();
3✔
90
      }
91
      this.legacyPropertiesFilePath = legacyPropertiesFolderPath.resolve(LEGACY_PROPERTIES);
5✔
92
    }
93
    this.variables = new HashMap<>();
5✔
94
    this.exportedVariables = new HashSet<>();
5✔
95
    this.modifiedVariables = new HashSet<>();
5✔
96
    load();
2✔
97
  }
1✔
98

99
  private static Path getParent(Path path) {
100

101
    if (path == null) {
2!
102
      return null;
×
103
    }
104
    return path.getParent();
3✔
105
  }
106

107
  private void load() {
108

109
    boolean success = load(this.propertiesFilePath);
5✔
110
    if (success) {
2✔
111
      this.legacyConfiguration = Boolean.FALSE;
4✔
112
    } else {
113
      success = load(this.legacyPropertiesFilePath);
5✔
114
      if (success) {
2✔
115
        this.legacyConfiguration = Boolean.TRUE;
3✔
116
      }
117
    }
118
  }
1✔
119

120
  private boolean load(Path file) {
121
    if (file == null) {
2✔
122
      return false;
2✔
123
    }
124
    if (!Files.exists(file)) {
5✔
125
      LOG.trace("Properties not found at {}", file);
4✔
126
      return false;
2✔
127
    }
128
    LOG.trace("Loading properties from {}", file);
4✔
129
    boolean legacyProperties = file.getFileName().toString().equals(LEGACY_PROPERTIES);
6✔
130
    try (BufferedReader reader = Files.newBufferedReader(file)) {
3✔
131
      String line;
132
      do {
133
        line = reader.readLine();
3✔
134
        if (line != null) {
2✔
135
          VariableLine variableLine = VariableLine.of(line, this.context, getSource());
7✔
136
          String name = variableLine.getName();
3✔
137
          if (name != null) {
2✔
138
            VariableLine migratedVariableLine = migrateLine(variableLine, false);
5✔
139
            if (migratedVariableLine == null) {
2!
140
              LOG.warn("Illegal variable definition: {}", variableLine);
×
141
              continue;
×
142
            }
143
            String migratedName = migratedVariableLine.getName();
3✔
144
            String migratedValue = migratedVariableLine.getValue();
3✔
145
            boolean legacyVariable = IdeVariables.isLegacyVariable(name);
3✔
146
            if (legacyVariable && !legacyProperties) {
4✔
147
              LOG.warn("Legacy variable name is used to define variable {} in {} - please cleanup your configuration.", variableLine,
5✔
148
                  file);
149
            }
150
            String oldValue = this.variables.get(migratedName);
6✔
151
            if (oldValue != null) {
2✔
152
              VariableDefinition<?> variableDefinition = IdeVariables.get(name);
3✔
153
              if (legacyVariable) {
2✔
154
                // if the legacy name was configured we do not want to override the official variable!
155
                LOG.warn("Both legacy variable {} and official variable {} are configured in {} - ignoring legacy variable declaration!",
9✔
156
                    variableDefinition.getLegacyName(), variableDefinition.getName(), file);
11✔
157
              } else {
158
                LOG.warn("Duplicate variable definition {} with old value '{}' and new value '{}' in {}", name, oldValue, migratedValue,
21✔
159
                    file);
160
                this.variables.put(migratedName, migratedValue);
6✔
161
              }
162
            } else {
1✔
163
              this.variables.put(migratedName, migratedValue);
6✔
164
            }
165
            if (variableLine.isExport()) {
3✔
166
              this.exportedVariables.add(migratedName);
5✔
167
            }
168
          }
169
        }
170
      } while (line != null);
2✔
171
      return true;
4✔
172
    } catch (IOException e) {
×
173
      throw new IllegalStateException("Failed to load properties from " + file, e);
×
174
    }
175
  }
176

177
  @Override
178
  public void save() {
179

180
    boolean isLegacy = Boolean.TRUE.equals(this.legacyConfiguration);
5✔
181
    if (this.modifiedVariables.isEmpty() && !isLegacy) {
6✔
182
      LOG.trace("No changes to save in properties file {}", this.propertiesFilePath);
5✔
183
      return;
1✔
184
    }
185

186
    Path file = this.propertiesFilePath;
3✔
187
    if (isLegacy) {
2✔
188
      LOG.info("Converting legacy properties to {}", this.propertiesFilePath);
5✔
189
      file = this.legacyPropertiesFilePath;
3✔
190
    }
191

192
    List<VariableLine> lines = loadVariableLines(file);
4✔
193

194
    this.context.getFileAccess().mkdirs(this.propertiesFilePath.getParent());
7✔
195
    try (BufferedWriter writer = Files.newBufferedWriter(this.propertiesFilePath)) {
6✔
196
      // copy and modify original lines from properties file
197
      for (VariableLine line : lines) {
10✔
198
        VariableLine newLine = migrateLine(line, true);
5✔
199
        if (newLine == null) {
2✔
200
          LOG.debug("Removed variable line '{}' from {}", line, this.propertiesFilePath);
7✔
201
        } else {
202
          if (newLine != line) {
3✔
203
            LOG.debug("Changed variable line from '{}' to '{}' in {}", line, newLine, this.propertiesFilePath);
18✔
204
          }
205
          writer.append(newLine.toString());
5✔
206
          writer.append(NEWLINE);
4✔
207
          String name = line.getName();
3✔
208
          if (name != null) {
2✔
209
            this.modifiedVariables.remove(name);
5✔
210
          }
211
        }
212
      }
1✔
213
      // append variables that have been newly added
214
      for (String name : this.modifiedVariables) {
11✔
215
        String value = this.variables.get(name);
6✔
216
        if (value == null) {
2✔
217
          LOG.trace("Internal error: removed variable {} was not found in {}", name, this.propertiesFilePath);
7✔
218
        } else {
219
          boolean export = this.exportedVariables.contains(name);
5✔
220
          VariableLine line = VariableLine.of(export, name, value);
5✔
221
          writer.append(line.toString());
5✔
222
          writer.append(NEWLINE);
4✔
223
        }
224
      }
1✔
225
      this.modifiedVariables.clear();
3✔
226
    } catch (IOException e) {
×
227
      throw new IllegalStateException("Failed to save properties to " + this.propertiesFilePath, e);
×
228
    }
1✔
229
    this.legacyConfiguration = Boolean.FALSE;
3✔
230
  }
1✔
231

232
  private List<VariableLine> loadVariableLines(Path file) {
233
    List<VariableLine> lines = new ArrayList<>();
4✔
234
    if (!Files.exists(file)) {
5✔
235
      // Skip reading if the file does not exist
236
      LOG.debug("Properties file {} does not exist, skipping read.", file);
4✔
237
      return lines;
2✔
238
    }
239
    try (BufferedReader reader = Files.newBufferedReader(file)) {
3✔
240
      String line;
241
      do {
242
        line = reader.readLine();
3✔
243
        if (line != null) {
2✔
244
          VariableLine variableLine = VariableLine.of(line, this.context, getSource());
7✔
245
          lines.add(variableLine);
4✔
246
        }
247
      } while (line != null);
2✔
248
    } catch (IOException e) {
×
249
      throw new IllegalStateException("Failed to load existing properties from " + file, e);
×
250
    }
1✔
251
    return lines;
2✔
252
  }
253

254
  private VariableLine migrateLine(VariableLine line, boolean saveNotLoad) {
255

256
    String name = line.getName();
3✔
257
    if (name != null) {
2✔
258
      VariableDefinition<?> variableDefinition = IdeVariables.get(name);
3✔
259
      if (variableDefinition != null) {
2✔
260
        line = variableDefinition.migrateLine(line);
4✔
261
      }
262
      if (saveNotLoad) {
2✔
263
        name = line.getName();
3✔
264
        if (this.modifiedVariables.contains(name)) {
5✔
265
          String value = this.variables.get(name);
6✔
266
          if (value == null) {
2✔
267
            return null;
2✔
268
          } else {
269
            line = line.withValue(value);
4✔
270
          }
271
        }
272
        boolean newExport = this.exportedVariables.contains(name);
5✔
273
        if (line.isExport() != newExport) {
4✔
274
          line = line.withExport(newExport);
4✔
275
        }
276
      }
277
    }
278
    return line;
2✔
279
  }
280

281
  @Override
282
  protected Map<String, String> getVariables() {
283

284
    return this.variables;
3✔
285
  }
286

287
  @Override
288
  protected void collectVariables(Map<String, VariableLine> variables, boolean onlyExported, AbstractEnvironmentVariables resolver) {
289

290
    for (String key : this.variables.keySet()) {
12✔
291
      variables.computeIfAbsent(key, k -> createVariableLine(key, onlyExported, resolver));
15✔
292
    }
1✔
293
    super.collectVariables(variables, onlyExported, resolver);
5✔
294
  }
1✔
295

296
  @Override
297
  public boolean isExported(String name) {
298

299
    if (this.exportedVariables.contains(name)) {
5!
300
      return true;
×
301
    }
302
    return super.isExported(name);
4✔
303
  }
304

305
  @Override
306
  public EnvironmentVariablesType getType() {
307

308
    return this.type;
3✔
309
  }
310

311
  @Override
312
  public Path getPropertiesFilePath() {
313

314
    return this.propertiesFilePath;
3✔
315
  }
316

317
  @Override
318
  public Path getLegacyPropertiesFilePath() {
319

320
    return this.legacyPropertiesFilePath;
3✔
321
  }
322

323
  /**
324
   * @return {@code Boolean#TRUE} if the current variable state comes from {@link #getLegacyPropertiesFilePath()}, {@code Boolean#FALSE} if state comes from
325
   *     {@link #getPropertiesFilePath()}), and {@code null} if neither of these files existed (nothing was loaded).
326
   */
327
  public Boolean getLegacyConfiguration() {
328

329
    return this.legacyConfiguration;
3✔
330
  }
331

332
  @Override
333
  public String set(String name, String value) {
334

335
    return set(name, value, this.exportedVariables.contains(name));
9✔
336
  }
337

338
  @Override
339
  public String set(String name, String value, boolean export) {
340

341
    String oldValue = this.variables.put(name, value);
7✔
342
    boolean flagChanged = export != this.exportedVariables.contains(name);
10✔
343
    if (Objects.equals(value, oldValue) && !flagChanged) {
6✔
344
      this.context.trace("Set variable '{}={}' caused no change in {}", name, value, this.propertiesFilePath);
20✔
345
    } else {
346
      this.context.debug("Set variable '{}={}' in {}", name, value, this.propertiesFilePath);
19✔
347
      this.modifiedVariables.add(name);
5✔
348
      if (export && (value != null)) {
4!
349
        this.exportedVariables.add(name);
6✔
350
      } else {
351
        this.exportedVariables.remove(name);
5✔
352
      }
353
    }
354
    return oldValue;
2✔
355
  }
356

357
  /**
358
   * Removes a property.
359
   *
360
   * @param name name of the property to remove.
361
   */
362
  public void remove(String name) {
363
    String oldValue = this.variables.remove(name);
6✔
364
    if (oldValue != null) {
2✔
365
      this.modifiedVariables.add(name);
5✔
366
      this.exportedVariables.remove(name);
5✔
367
      this.context.debug("Removed variable name of '{}' in {}", name, this.propertiesFilePath);
15✔
368
    }
369
  }
1✔
370

371
}
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