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

devonfw / IDEasy / 12764736303

14 Jan 2025 09:28AM UTC coverage: 68.082% (+0.5%) from 67.541%
12764736303

Pull #820

github

web-flow
Merge 115768cca into 875fbff84
Pull Request #820: #759: upgrade settings commandlet

2689 of 4311 branches covered (62.38%)

Branch coverage included in aggregate %.

6946 of 9841 relevant lines covered (70.58%)

3.1 hits per line

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

50.38
cli/src/main/java/com/devonfw/tools/ide/merge/JsonMerger.java
1
package com.devonfw.tools.ide.merge;
2

3
import java.io.IOException;
4
import java.io.OutputStream;
5
import java.io.Reader;
6
import java.io.Writer;
7
import java.nio.file.Files;
8
import java.nio.file.Path;
9
import java.util.Collections;
10
import java.util.HashMap;
11
import java.util.Iterator;
12
import java.util.Map;
13
import java.util.Set;
14
import jakarta.json.Json;
15
import jakarta.json.JsonArray;
16
import jakarta.json.JsonArrayBuilder;
17
import jakarta.json.JsonObject;
18
import jakarta.json.JsonObjectBuilder;
19
import jakarta.json.JsonReader;
20
import jakarta.json.JsonString;
21
import jakarta.json.JsonStructure;
22
import jakarta.json.JsonValue;
23
import jakarta.json.JsonWriter;
24
import jakarta.json.JsonWriterFactory;
25
import jakarta.json.stream.JsonGenerator;
26

27
import com.devonfw.tools.ide.context.IdeContext;
28
import com.devonfw.tools.ide.environment.EnvironmentVariables;
29
import com.fasterxml.jackson.core.util.DefaultIndenter;
30
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
31
import com.fasterxml.jackson.databind.JsonNode;
32
import com.fasterxml.jackson.databind.ObjectMapper;
33
import com.fasterxml.jackson.databind.node.ArrayNode;
34
import com.fasterxml.jackson.databind.node.ObjectNode;
35
import com.fasterxml.jackson.databind.node.TextNode;
36

37
/**
38
 * Implementation of {@link FileMerger} for JSON.
39
 */
40
public class JsonMerger extends FileMerger {
1✔
41

42
  /**
43
   * The constructor.
44
   *
45
   * @param context the {@link #context}.
46
   */
47
  public JsonMerger(IdeContext context) {
48

49
    super(context);
3✔
50
  }
1✔
51

52
  @Override
53
  protected void doMerge(Path setup, Path update, EnvironmentVariables variables, Path workspace) {
54

55
    JsonStructure json = null;
2✔
56
    Path template = setup;
2✔
57
    boolean updateFileExists = Files.exists(update);
5✔
58
    if (Files.exists(workspace)) {
5✔
59
      if (!updateFileExists) {
2!
60
        return; // nothing to do ...
×
61
      }
62
      json = load(workspace);
4✔
63
    } else if (Files.exists(setup)) {
5✔
64
      json = load(setup);
3✔
65
    }
66
    JsonStructure mergeJson = null;
2✔
67
    if (updateFileExists) {
2!
68
      if (json == null) {
2✔
69
        json = load(update);
4✔
70
      } else {
71
        mergeJson = load(update);
3✔
72
      }
73
      template = update;
2✔
74
    }
75
    Status status = new Status();
4✔
76
    JsonStructure result = (JsonStructure) mergeAndResolve(json, mergeJson, variables, status, template.toString());
10✔
77
    if (status.updated) {
3✔
78
      save(result, workspace);
3✔
79
      this.context.debug("Saved created/updated file {}", workspace);
11✔
80
    } else {
81
      this.context.trace("No changes for file {}", workspace);
10✔
82
    }
83
  }
1✔
84

85
  private static JsonStructure load(Path file) {
86

87
    try (Reader reader = Files.newBufferedReader(file)) {
3✔
88
      JsonReader jsonReader = Json.createReader(reader);
3✔
89
      return jsonReader.read();
5✔
90
    } catch (Exception e) {
×
91
      throw new IllegalStateException("Failed to read JSON from " + file, e);
×
92
    }
93
  }
94

95
  private static void save(JsonStructure json, Path file) {
96

97
    ensureParentDirectoryExists(file);
2✔
98
    try (OutputStream out = Files.newOutputStream(file)) {
5✔
99

100
      Map<String, Object> config = new HashMap<>();
4✔
101
      config.put(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE);
5✔
102
      // JSON-P API sucks: no way to set the indentation string
103
      // preferred would be two spaces, implementation has four whitespaces hardcoded
104
      // See org.glassfish.json.JsonPrettyGeneratorImpl
105
      // when will they ever learn...?
106
      JsonWriterFactory jsonWriterFactory = Json.createWriterFactory(config);
3✔
107
      JsonWriter jsonWriter = jsonWriterFactory.createWriter(out);
4✔
108
      jsonWriter.write(json);
3✔
109
      jsonWriter.close();
2✔
110
    } catch (Exception e) {
×
111
      throw new IllegalStateException("Failed to save JSON to " + file, e);
×
112
    }
1✔
113
  }
1✔
114

115
  @Override
116
  public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path updateFile) {
117

118
    if (!Files.exists(workspace) || !Files.exists(updateFile)) {
×
119
      return;
×
120
    }
121
    JsonStructure updateDocument = load(updateFile);
×
122
    JsonStructure workspaceDocument = load(workspace);
×
123
    Status status = new Status(addNewProperties);
×
124
    JsonStructure result = (JsonStructure) mergeAndResolve(workspaceDocument, updateDocument, variables, status,
×
125
        workspace.getFileName());
×
126
    if (status.updated) {
×
127
      save(result, updateFile);
×
128
      this.context.debug("Saved changes from {} to {}", workspace.getFileName(), updateFile);
×
129
    } else {
130
      this.context.trace("No changes for {}", updateFile);
×
131
    }
132
  }
×
133

134
  private JsonValue mergeAndResolve(JsonValue json, JsonValue mergeJson, EnvironmentVariables variables, Status status,
135
      Object src) {
136

137
    if (json == null) {
2✔
138
      if (mergeJson == null) {
2!
139
        return null;
×
140
      } else {
141
        return mergeAndResolve(mergeJson, null, variables, status, src);
8✔
142
      }
143
    } else {
144
      if (mergeJson == null) {
2✔
145
        status.updated = true; // JSON to merge does not exist and needs to be created
3✔
146
      }
147
      return switch (json.getValueType()) {
7!
148
        case OBJECT -> mergeAndResolveObject((JsonObject) json, (JsonObject) mergeJson, variables, status, src);
10✔
149
        case ARRAY -> mergeAndResolveArray((JsonArray) json, (JsonArray) mergeJson, variables, status, src);
10✔
150
        case STRING -> mergeAndResolveString((JsonString) json, (JsonString) mergeJson, variables, status, src);
10✔
151
        case NUMBER, FALSE, TRUE, NULL -> mergeAndResolveNativeType(json, mergeJson, variables, status);
7✔
152
        default -> {
153
          this.context.error("Undefined JSON type {}", json.getClass());
×
154
          yield null;
×
155
        }
156
      };
157
    }
158
  }
159

160
  private JsonObject mergeAndResolveObject(JsonObject json, JsonObject mergeJson, EnvironmentVariables variables,
161
      Status status, Object src) {
162

163
    // json = workspace/setup
164
    // mergeJson = update
165
    JsonObjectBuilder builder = Json.createObjectBuilder();
2✔
166
    Set<String> mergeKeySet = Collections.emptySet();
2✔
167
    if (mergeJson != null) {
2✔
168
      mergeKeySet = mergeJson.keySet();
3✔
169
      for (String key : mergeKeySet) {
10✔
170
        JsonValue mergeValue = mergeJson.get(key);
5✔
171
        JsonValue value = json.get(key);
5✔
172
        value = mergeAndResolve(value, mergeValue, variables, status, src);
8✔
173
        builder.add(key, value);
5✔
174
      }
1✔
175
    }
176
    if (status.addNewProperties || !status.inverse) {
6!
177
      for (String key : json.keySet()) {
11✔
178
        if (!mergeKeySet.contains(key)) {
4✔
179
          JsonValue value = json.get(key);
5✔
180
          value = mergeAndResolve(value, null, variables, status, src);
8✔
181
          builder.add(key, value);
5✔
182
          if (status.inverse) {
3!
183
            // added new property on inverse merge...
184
            status.updated = true;
×
185
          }
186
        }
187
      }
1✔
188
    }
189
    return builder.build();
3✔
190
  }
191

192
  private JsonArray mergeAndResolveArray(JsonArray json, JsonArray mergeJson, EnvironmentVariables variables,
193
      Status status, Object src) {
194

195
    JsonArrayBuilder builder = Json.createArrayBuilder();
2✔
196
    // KISS: Merging JSON arrays could be very complex. We simply let mergeJson override json...
197
    JsonArray source = json;
2✔
198
    if (mergeJson != null) {
2!
199
      source = mergeJson;
2✔
200
    }
201
    for (JsonValue value : source) {
10✔
202
      JsonValue resolvedValue = mergeAndResolve(value, null, variables, status, src);
8✔
203
      builder.add(resolvedValue);
4✔
204
    }
1✔
205
    return builder.build();
3✔
206
  }
207

208
  private JsonString mergeAndResolveString(JsonString json, JsonString mergeJson, EnvironmentVariables variables,
209
      Status status, Object src) {
210

211
    JsonString jsonString = json;
2✔
212
    if (mergeJson != null) {
2✔
213
      jsonString = mergeJson;
2✔
214
    }
215
    String string = jsonString.getString();
3✔
216
    String resolvedString;
217
    if (status.inverse) {
3!
218
      resolvedString = variables.inverseResolve(string, src);
×
219
    } else {
220
      resolvedString = variables.resolve(string, src, this.legacySupport);
7✔
221
    }
222
    if (!resolvedString.equals(string)) {
4✔
223
      status.updated = true;
3✔
224
    }
225
    return Json.createValue(resolvedString);
3✔
226
  }
227

228
  private JsonValue mergeAndResolveNativeType(JsonValue json, JsonValue mergeJson, EnvironmentVariables variables,
229
      Status status) {
230

231
    if (mergeJson == null) {
2✔
232
      return json;
2✔
233
    } else {
234
      return mergeJson;
2✔
235
    }
236
  }
237

238
  @Override
239
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
240

241
    JsonNode jsonNode;
242
    ObjectMapper mapper = new ObjectMapper();
×
243
    try (Reader reader = Files.newBufferedReader(workspaceFile)) {
×
244
      jsonNode = mapper.reader().readTree(reader);
×
245
    }
246
    JsonNode migratedNode = upgradeJsonNode(jsonNode);
×
247
    boolean modified = (migratedNode != jsonNode);
×
248
    if (migratedNode == null) {
×
249
      migratedNode = jsonNode;
×
250
    }
251
    if (modified) {
×
252
      try (Writer writer = Files.newBufferedWriter(workspaceFile)) {
×
253
        mapper.writer(new JsonPrettyPrinter()).writeValue(writer, migratedNode);
×
254
      }
255
    }
256
    return modified;
×
257
  }
258

259
  /**
260
   * @param jsonNode the {@link JsonNode} to upgrade.
261
   * @return the given {@link JsonNode} if unmodified after upgrade. Otherwise, a new migrated {@link JsonNode} or {@code null} if the given {@link JsonNode}
262
   *     was mutable and the migration could be applied directly.
263
   */
264
  private JsonNode upgradeJsonNode(JsonNode jsonNode) {
265

266
    if (jsonNode instanceof ArrayNode jsonArray) {
×
267
      return upgradeJsonArray(jsonArray);
×
268
    } else if (jsonNode instanceof ObjectNode jsonObject) {
×
269
      return upgradeJsonObject(jsonObject);
×
270
    } else if (jsonNode instanceof TextNode jsonString) {
×
271
      return upgradeJsonString(jsonString);
×
272
    } else {
273
      assert jsonNode.isValueNode();
×
274
      return jsonNode;
×
275
    }
276
  }
277

278
  private ObjectNode upgradeJsonObject(ObjectNode jsonObject) {
279

280
    ObjectNode result = jsonObject;
×
281
    Iterator<String> fieldNames = jsonObject.fieldNames();
×
282
    while (fieldNames.hasNext()) {
×
283
      String fieldName = fieldNames.next();
×
284
      JsonNode child = jsonObject.get(fieldName);
×
285
      JsonNode migratedChild = upgradeJsonNode(child);
×
286
      if (migratedChild != child) {
×
287
        result = null;
×
288
        if (migratedChild != null) {
×
289
          jsonObject.put(fieldName, migratedChild);
×
290
        }
291
      }
292
    }
×
293
    return result;
×
294
  }
295

296
  private ArrayNode upgradeJsonArray(ArrayNode jsonArray) {
297

298
    ArrayNode result = jsonArray;
×
299
    int size = jsonArray.size();
×
300
    for (int i = 0; i < size; i++) {
×
301
      JsonNode child = jsonArray.get(i);
×
302
      JsonNode migratedChild = upgradeJsonNode(child);
×
303
      if (migratedChild != child) {
×
304
        result = null;
×
305
        if (migratedChild != null) {
×
306
          jsonArray.set(i, migratedChild);
×
307
        }
308
      }
309
    }
310
    return result;
×
311
  }
312

313
  private JsonNode upgradeJsonString(TextNode jsonString) {
314

315
    String text = jsonString.textValue();
×
316
    String migratedText = upgradeWorkspaceContent(text);
×
317
    if (migratedText.equals(text)) {
×
318
      return jsonString;
×
319
    } else {
320
      return new TextNode(migratedText);
×
321
    }
322
  }
323

324
  private static class Status {
325

326
    /**
327
     * {@code true} for inverse merge, {@code false} otherwise (for regular forward merge).
328
     */
329
    private final boolean inverse;
330

331
    private final boolean addNewProperties;
332

333
    private boolean updated;
334

335
    /**
336
     * The constructor.
337
     */
338
    public Status() {
339

340
      this(false, false);
4✔
341
    }
1✔
342

343
    /**
344
     * The constructor.
345
     *
346
     * @param addNewProperties - {@code true} to add new properties from workspace on reverse merge, {@code false} otherwise.
347
     */
348
    public Status(boolean addNewProperties) {
349

350
      this(true, addNewProperties);
×
351
    }
×
352

353
    private Status(boolean inverse, boolean addNewProperties) {
354

355
      super();
2✔
356
      this.inverse = inverse;
3✔
357
      this.addNewProperties = addNewProperties;
3✔
358
      this.updated = false;
3✔
359
    }
1✔
360

361
  }
362

363
  /**
364
   * Extends {@link DefaultPrettyPrinter} to get nicely formatted JSON output.
365
   */
366
  private static class JsonPrettyPrinter extends DefaultPrettyPrinter {
367

368
    public JsonPrettyPrinter() {
×
369
      DefaultPrettyPrinter.Indenter indenter = new DefaultIndenter("  ", "\n");
×
370
      indentObjectsWith(indenter);
×
371
      indentArraysWith(indenter);
×
372
      _objectFieldValueSeparatorWithSpaces = ": ";
×
373
    }
×
374

375
    private JsonPrettyPrinter(JsonPrettyPrinter pp) {
376
      super(pp);
×
377
    }
×
378

379
    @Override
380
    public void writeEndArray(com.fasterxml.jackson.core.JsonGenerator g, int nrOfValues) throws IOException {
381

382
      if (!_arrayIndenter.isInline()) {
×
383
        _nesting--;
×
384
      }
385
      if (nrOfValues > 0) {
×
386
        _arrayIndenter.writeIndentation(g, _nesting);
×
387
      }
388
      g.writeRaw(']');
×
389
    }
×
390

391
    @Override
392
    public DefaultPrettyPrinter createInstance() {
393
      return new JsonPrettyPrinter(this);
×
394
    }
395
  }
396
}
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