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

devonfw / IDEasy / 22345446363

24 Feb 2026 09:49AM UTC coverage: 70.247% (-0.2%) from 70.474%
22345446363

Pull #1714

github

web-flow
Merge 5655b6589 into 379acdc9d
Pull Request #1714: #404: #1713: advanced logging

4065 of 6384 branches covered (63.67%)

Branch coverage included in aggregate %.

10597 of 14488 relevant lines covered (73.14%)

3.08 hits per line

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

81.78
cli/src/main/java/com/devonfw/tools/ide/merge/xml/XmlMerger.java
1
package com.devonfw.tools.ide.merge.xml;
2

3
import java.io.BufferedWriter;
4
import java.io.InputStream;
5
import java.nio.file.Files;
6
import java.nio.file.Path;
7
import javax.xml.namespace.QName;
8
import javax.xml.parsers.DocumentBuilder;
9
import javax.xml.parsers.DocumentBuilderFactory;
10
import javax.xml.transform.OutputKeys;
11
import javax.xml.transform.Transformer;
12
import javax.xml.transform.TransformerFactory;
13
import javax.xml.transform.dom.DOMSource;
14
import javax.xml.transform.stream.StreamResult;
15

16
import org.slf4j.Logger;
17
import org.slf4j.LoggerFactory;
18
import org.w3c.dom.Attr;
19
import org.w3c.dom.Document;
20
import org.w3c.dom.Element;
21
import org.w3c.dom.NamedNodeMap;
22
import org.w3c.dom.Node;
23
import org.w3c.dom.NodeList;
24
import org.w3c.dom.Text;
25

26
import com.devonfw.tools.ide.context.IdeContext;
27
import com.devonfw.tools.ide.environment.EnvironmentVariables;
28
import com.devonfw.tools.ide.merge.FileMerger;
29
import com.devonfw.tools.ide.merge.xml.matcher.ElementMatcher;
30
import com.devonfw.tools.ide.variable.IdeVariables;
31

32
/**
33
 * {@link FileMerger} for XML files.
34
 */
35
public class XmlMerger extends FileMerger implements XmlMergeSupport {
36

37
  private static final Logger LOG = LoggerFactory.getLogger(XmlMerger.class);
3✔
38

39
  private static final DocumentBuilder DOCUMENT_BUILDER;
40

41
  private static final TransformerFactory TRANSFORMER_FACTORY;
42

43
  protected final boolean legacyXmlSupport;
44

45
  /** The namespace URI for this XML merger. */
46
  public static final String MERGE_NS_URI = "https://github.com/devonfw/IDEasy/merge";
47

48
  static {
49
    try {
50
      DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
2✔
51
      documentBuilderFactory.setNamespaceAware(true);
3✔
52
      DOCUMENT_BUILDER = documentBuilderFactory.newDocumentBuilder();
3✔
53
      TRANSFORMER_FACTORY = TransformerFactory.newInstance();
2✔
54
    } catch (Exception e) {
×
55
      throw new IllegalStateException("Invalid XML DOM support in JDK.", e);
×
56
    }
1✔
57
  }
1✔
58

59
  /**
60
   * The constructor.
61
   *
62
   * @param context the {@link IdeContext}.
63
   */
64
  public XmlMerger(IdeContext context) {
65

66
    super(context);
3✔
67
    this.legacyXmlSupport = Boolean.TRUE.equals(IdeVariables.IDE_XML_MERGE_LEGACY_SUPPORT_ENABLED.get(context));
7✔
68
  }
1✔
69

70
  @Override
71
  protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) {
72

73
    XmlMergeDocument workspaceDocument = null;
2✔
74
    boolean updateFileExists = Files.exists(update);
5✔
75
    boolean workspaceFileExists = Files.exists(workspace);
5✔
76
    if (workspaceFileExists) {
2✔
77
      if (!updateFileExists) {
2✔
78
        return; // nothing to do ...
1✔
79
      }
80
      workspaceDocument = load(workspace);
5✔
81
    } else if (Files.exists(setup)) {
5!
82
      workspaceDocument = loadAndResolve(setup, resolver);
5✔
83
    }
84
    Document resultDocument = null;
2✔
85
    if (updateFileExists) {
2✔
86
      XmlMergeDocument templateDocument = loadAndResolve(update, resolver);
5✔
87
      if (workspaceDocument == null) {
2!
88
        resultDocument = templateDocument.getDocument();
×
89
      } else {
90
        resultDocument = merge(templateDocument, workspaceDocument, workspaceFileExists);
6✔
91
        if ((resultDocument == null) && !workspaceFileExists) {
2!
92
          // if the merge failed due to incompatible roots and we have no workspace file
93
          // then at least we should take the resolved setup file as result
94
          resultDocument = workspaceDocument.getDocument();
×
95
        }
96
      }
97
    } else if (workspaceDocument != null) {
3!
98
      resultDocument = workspaceDocument.getDocument();
3✔
99
    }
100
    if (resultDocument != null) {
2!
101
      XmlMergeDocument result = new XmlMergeDocument(resultDocument, workspace);
6✔
102
      XmlMergeSupport.removeMergeNsAttributes(result);
2✔
103
      save(result);
3✔
104
    }
105
  }
1✔
106

107
  /**
108
   * Merges the source document with the target document.
109
   *
110
   * @param templateDocument the {@link XmlMergeDocument} representing the template xml file from the settings.
111
   * @param workspaceDocument the {@link XmlMergeDocument} of the actual source XML file (typically from the workspace of the real IDE) to merge with the
112
   *     {@code templateDocument}.
113
   * @param workspaceFileExists indicates whether the workspace document already exists or if setup templates are loaded
114
   * @return the merged {@link Document}.
115
   */
116
  public Document merge(XmlMergeDocument templateDocument, XmlMergeDocument workspaceDocument, boolean workspaceFileExists) {
117

118
    Document resultDocument;
119
    Path template = templateDocument.getPath();
3✔
120
    Path source = workspaceDocument.getPath();
3✔
121
    LOG.debug("Merging {} into {} ...", template, source);
5✔
122
    Element templateRoot = templateDocument.getRoot();
3✔
123
    QName templateQName = XmlMergeSupport.getQualifiedName(templateRoot);
3✔
124
    Document document = workspaceDocument.getDocument();
3✔
125
    Element workspaceRoot = workspaceDocument.getRoot();
3✔
126
    if (workspaceRoot == null) {
2✔
127
      workspaceRoot = (Element) document.importNode(templateRoot, false);
6✔
128
      NamedNodeMap attributes = workspaceRoot.getAttributes();
3✔
129
      int length = attributes.getLength();
3✔
130
      for (int i = 0; i < length; i++) {
7✔
131
        Attr attribute = (Attr) attributes.item(i);
5✔
132
        if (XmlMergeSupport.hasMergeNamespace(attribute)) {
3✔
133
          workspaceRoot.removeAttributeNode(attribute);
4✔
134
        }
135
      }
136
      workspaceRoot.removeAttributeNS(XmlMergeSupport.MERGE_NS_URI, "xmlns:xsi");
4✔
137
      document.appendChild(workspaceRoot);
4✔
138
    }
139
    QName workspaceQName = XmlMergeSupport.getQualifiedName(workspaceRoot);
3✔
140
    if (templateQName.equals(workspaceQName)) {
4!
141
      XmlMergeStrategy strategy = XmlMergeSupport.getMergeStrategy(templateRoot);
3✔
142
      if (strategy == null) {
2✔
143
        strategy = XmlMergeStrategy.COMBINE; // default strategy used as fallback
2✔
144
      }
145
      if (templateRoot.lookupPrefix(MERGE_NS_URI) == null) {
4✔
146
        if (this.legacyXmlSupport) {
3✔
147
          if (workspaceFileExists) {
2✔
148
            strategy = XmlMergeStrategy.OVERRIDE;
3✔
149
          } else {
150
            strategy = XmlMergeStrategy.KEEP;
3✔
151
          }
152
        } else {
153
          LOG.warn(
4✔
154
              "XML merge namespace not found in file {}. If you are working in a legacy devonfw-ide project, please set IDE_XML_MERGE_LEGACY_SUPPORT_ENABLED=true to "
155
                  + "proceed correctly.", source);
156
        }
157
      }
158
      ElementMatcher elementMatcher = new ElementMatcher(this.context, templateDocument.getPath(), workspaceDocument.getPath());
10✔
159
      strategy.merge(templateRoot, workspaceRoot, elementMatcher);
5✔
160
      resultDocument = document;
2✔
161
    } else {
1✔
162
      LOG.error("Cannot merge XML template {} with root {} into XML file {} with root {} as roots do not match.", templateDocument.getPath(),
×
163
          templateQName, workspaceDocument.getPath(), workspaceQName);
×
164
      return null;
×
165
    }
166
    return resultDocument;
2✔
167
  }
168

169
  @Override
170
  public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) {
171

172
    if (!Files.exists(workspace) || !Files.exists(update)) {
×
173
      return;
×
174
    }
175
    throw new UnsupportedOperationException("not implemented!");
×
176
  }
177

178
  /**
179
   * {@link #load(Path) Loads} and {@link #resolveDocument(XmlMergeDocument, EnvironmentVariables, boolean) resolves} XML from the given file.
180
   *
181
   * @param file the {@link Path} to the XML file.
182
   * @param variables the {@link EnvironmentVariables}.
183
   * @return the loaded {@link XmlMergeDocument}.
184
   */
185
  public XmlMergeDocument loadAndResolve(Path file, EnvironmentVariables variables) {
186

187
    XmlMergeDocument document = load(file);
4✔
188
    resolveDocument(document, variables, false);
5✔
189
    return document;
2✔
190
  }
191

192
  /**
193
   * @param file the {@link Path} to the XML file.
194
   * @return the loaded {@link XmlMergeDocument}.
195
   */
196
  public XmlMergeDocument load(Path file) {
197

198
    Document document;
199
    if (this.context.getFileAccess().isNonEmptyFile(file)) {
6✔
200
      try (InputStream in = Files.newInputStream(file)) {
5✔
201
        document = DOCUMENT_BUILDER.parse(in);
4✔
202
      } catch (Exception e) {
×
203
        throw new IllegalStateException("Failed to load XML from: " + file, e);
×
204
      }
1✔
205
    } else {
206
      document = DOCUMENT_BUILDER.newDocument();
3✔
207
    }
208
    return new XmlMergeDocument(document, file);
6✔
209
  }
210

211
  /**
212
   * @param document the XML {@link XmlMergeDocument} to save.
213
   */
214
  public void save(XmlMergeDocument document) {
215

216
    save(document.getDocument(), document.getPath());
6✔
217
  }
1✔
218

219
  /**
220
   * @param document the XML {@link Document} to save.
221
   * @param file the {@link Path} to the file where to save the XML.
222
   */
223
  public void save(Document document, Path file) {
224

225
    ensureParentDirectoryExists(file);
2✔
226
    try {
227
      Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3✔
228
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
4✔
229
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
4✔
230
      transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
4✔
231
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
4✔
232

233
      // Workaround:
234
      // Remove whitespace from the target document before saving, because if target XML Document is already formatted
235
      // then indent 2 keeps adding empty lines for nothing, and if we don't use indentation then appending/ overriding
236
      // isn't properly formatted.
237
      // https://bugs.openjdk.org/browse/JDK-8262285
238
      removeWhitespace(document.getDocumentElement());
4✔
239

240
      DOMSource source = new DOMSource(document);
5✔
241
      StreamResult result = new StreamResult(file.toFile());
6✔
242
      transformer.transform(source, result);
4✔
243
    } catch (Exception e) {
×
244
      throw new IllegalStateException("Failed to save XML to file: " + file, e);
×
245
    }
1✔
246
  }
1✔
247

248
  private void removeWhitespace(Node node) {
249

250
    NodeList children = node.getChildNodes();
3✔
251
    for (int i = 0; i < children.getLength(); i++) {
8✔
252
      Node child = children.item(i);
4✔
253
      short nodeType = child.getNodeType();
3✔
254
      if (nodeType == Node.TEXT_NODE) {
3✔
255
        if (child.getTextContent().trim().isEmpty()) {
5✔
256
          node.removeChild(child);
4✔
257
          i--;
2✔
258
        }
259
      } else if (nodeType == Node.ELEMENT_NODE) {
3✔
260
        removeWhitespace(child);
3✔
261
      }
262
    }
263
  }
1✔
264

265
  private void resolveDocument(XmlMergeDocument document, EnvironmentVariables variables, boolean inverse) {
266

267
    NodeList nodeList = document.getAllElements();
3✔
268
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
269
      Element element = (Element) nodeList.item(i);
5✔
270
      resolveElement(element, variables, inverse, document.getPath());
7✔
271
    }
272
  }
1✔
273

274
  private void resolveElement(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
275

276
    resolveAttributes(element.getAttributes(), variables, inverse, src);
7✔
277
    NodeList nodeList = element.getChildNodes();
3✔
278
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
279
      Node node = nodeList.item(i);
4✔
280
      if (XmlMergeSupport.isTextual(node)) {
3✔
281
        resolveValue(node, variables, inverse, src);
6✔
282
      }
283
    }
284
  }
1✔
285

286
  private void resolveAttributes(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
287

288
    for (int i = 0; i < attributes.getLength(); i++) {
8✔
289
      Attr attribute = (Attr) attributes.item(i);
5✔
290
      resolveValue(attribute, variables, inverse, src);
6✔
291
    }
292
  }
1✔
293

294
  private void resolveValue(Node node, EnvironmentVariables variables, boolean inverse, Object src) {
295
    String value = node.getNodeValue();
3✔
296
    String resolvedValue;
297
    if (inverse) {
2!
298
      resolvedValue = variables.inverseResolve(value, src);
×
299
    } else {
300
      resolvedValue = variables.resolve(value, src, this.legacySupport);
7✔
301
    }
302
    node.setNodeValue(resolvedValue);
3✔
303
  }
1✔
304

305
  @Override
306
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
307

308
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
2✔
309
    DocumentBuilder builder = factory.newDocumentBuilder();
3✔
310
    Document document = builder.parse(workspaceFile.toFile());
5✔
311
    checkForXmlNamespace(document, workspaceFile);
4✔
312
    boolean modified = updateWorkspaceXml(document.getDocumentElement());
5✔
313
    if (modified) {
2!
314
      TransformerFactory transformerFactory = TransformerFactory.newInstance();
×
315
      Transformer transformer = transformerFactory.newTransformer();
×
316
      DOMSource source = new DOMSource(document);
×
317
      try (BufferedWriter writer = Files.newBufferedWriter(workspaceFile)) {
×
318
        StreamResult result = new StreamResult(writer);
×
319
        transformer.transform(source, result);
×
320
      }
321
    }
322
    return modified;
2✔
323
  }
324

325
  private boolean updateWorkspaceXml(Element element) {
326

327
    boolean modified = false;
2✔
328
    NamedNodeMap attributes = element.getAttributes();
3✔
329
    if (attributes != null) {
2!
330
      for (int i = 0; i < attributes.getLength(); i++) {
8✔
331
        Node node = attributes.item(i);
4✔
332
        if (node instanceof Attr attribute) {
6!
333
          String value = attribute.getValue();
3✔
334
          String migratedValue = upgradeWorkspaceContent(value);
4✔
335
          if (!migratedValue.equals(value)) {
4!
336
            modified = true;
×
337
            attribute.setValue(migratedValue);
×
338
          }
339
        }
340
      }
341
    }
342

343
    NodeList childNodes = element.getChildNodes();
3✔
344
    for (int i = 0; i < childNodes.getLength(); i++) {
8✔
345
      Node childNode = childNodes.item(i);
4✔
346
      boolean childModified = false;
2✔
347
      if (childNode instanceof Element childElement) {
6✔
348
        childModified = updateWorkspaceXml(childElement);
5✔
349
      } else if (childNode instanceof Text childText) {
6!
350
        String text = childText.getTextContent();
3✔
351
        String migratedText = upgradeWorkspaceContent(text);
4✔
352
        childModified = !migratedText.equals(text);
6!
353
        if (childModified) {
2!
354
          childText.setTextContent(migratedText);
×
355
        }
356
      }
357
      if (childModified) {
2!
358
        modified = true;
×
359
      }
360
    }
361
    return modified;
2✔
362
  }
363

364
  private void checkForXmlNamespace(Document document, Path workspaceFile) {
365

366
    NamedNodeMap attributes = document.getDocumentElement().getAttributes();
4✔
367
    if (attributes != null) {
2!
368
      for (int i = 0; i < attributes.getLength(); i++) {
8✔
369
        Node node = attributes.item(i);
4✔
370
        String uri = node.getNamespaceURI();
3✔
371
        if (MERGE_NS_URI.equals(uri)) {
4!
372
          return;
×
373
        }
374
      }
375
    }
376
    LOG.warn(
4✔
377
        "The XML file {} does not contain the XML merge namespace and seems outdated. For details see:\n"
378
            + "https://github.com/devonfw/IDEasy/blob/main/documentation/configurator.adoc#xml-merger", workspaceFile);
379
  }
1✔
380

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