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

devonfw / IDEasy / 20115895339

10 Dec 2025 10:55PM UTC coverage: 70.14% (+0.2%) from 69.924%
20115895339

push

github

web-flow
#1166 automatic project import for intellij #1508: fix xml merge when file is empty (#1649)

Co-authored-by: cthies <caroline.thies@capgemini.com>
Co-authored-by: jan-vcapgemini <59438728+jan-vcapgemini@users.noreply.github.com>
Co-authored-by: jan-vcapgemini <jan-vincent.hoelzle@capgemini.com>

3963 of 6211 branches covered (63.81%)

Branch coverage included in aggregate %.

10138 of 13893 relevant lines covered (72.97%)

3.15 hits per line

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

81.72
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.w3c.dom.Attr;
17
import org.w3c.dom.Document;
18
import org.w3c.dom.Element;
19
import org.w3c.dom.NamedNodeMap;
20
import org.w3c.dom.Node;
21
import org.w3c.dom.NodeList;
22
import org.w3c.dom.Text;
23

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

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

35
  private static final DocumentBuilder DOCUMENT_BUILDER;
36

37
  private static final TransformerFactory TRANSFORMER_FACTORY;
38

39
  protected final boolean legacyXmlSupport;
40

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

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

55
  /**
56
   * The constructor.
57
   *
58
   * @param context the {@link IdeContext}.
59
   */
60
  public XmlMerger(IdeContext context) {
61

62
    super(context);
3✔
63
    this.legacyXmlSupport = Boolean.TRUE.equals(IdeVariables.IDE_XML_MERGE_LEGACY_SUPPORT_ENABLED.get(context));
7✔
64
  }
1✔
65

66
  @Override
67
  protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) {
68

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

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

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

165
  @Override
166
  public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) {
167

168
    if (!Files.exists(workspace) || !Files.exists(update)) {
×
169
      return;
×
170
    }
171
    throw new UnsupportedOperationException("not implemented!");
×
172
  }
173

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

183
    XmlMergeDocument document = load(file);
4✔
184
    resolveDocument(document, variables, false);
5✔
185
    return document;
2✔
186
  }
187

188
  /**
189
   * @param file the {@link Path} to the XML file.
190
   * @return the loaded {@link XmlMergeDocument}.
191
   */
192
  public XmlMergeDocument load(Path file) {
193

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

207
  /**
208
   * @param document the XML {@link XmlMergeDocument} to save.
209
   */
210
  public void save(XmlMergeDocument document) {
211

212
    save(document.getDocument(), document.getPath());
6✔
213
  }
1✔
214

215
  /**
216
   * @param document the XML {@link Document} to save.
217
   * @param file the {@link Path} to the file where to save the XML.
218
   */
219
  public void save(Document document, Path file) {
220

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

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

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

244
  private void removeWhitespace(Node node) {
245

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

261
  private void resolveDocument(XmlMergeDocument document, EnvironmentVariables variables, boolean inverse) {
262

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

270
  private void resolveElement(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
271

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

282
  private void resolveAttributes(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
283

284
    for (int i = 0; i < attributes.getLength(); i++) {
8✔
285
      Attr attribute = (Attr) attributes.item(i);
5✔
286
      resolveValue(attribute, variables, inverse, src);
6✔
287
    }
288
  }
1✔
289

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

301
  @Override
302
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
303

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

321
  private boolean updateWorkspaceXml(Element element) {
322

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

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

360
  private void checkForXmlNamespace(Document document, Path workspaceFile) {
361

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

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