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

devonfw / IDEasy / 19833443579

01 Dec 2025 06:36PM UTC coverage: 70.042% (+0.2%) from 69.854%
19833443579

Pull #1466

github

web-flow
Merge 8f5bcafad into 4921aa3d0
Pull Request #1466: #1166: import maven repos in intellij by editing misc.xml

3856 of 6039 branches covered (63.85%)

Branch coverage included in aggregate %.

9880 of 13572 relevant lines covered (72.8%)

3.16 hits per line

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

80.48
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
    Element workspaceRoot = workspaceDocument.getRoot();
3✔
121
    QName workspaceQName = XmlMergeSupport.getQualifiedName(workspaceRoot);
3✔
122
    if (templateQName.equals(workspaceQName)) {
4!
123
      XmlMergeStrategy strategy = XmlMergeSupport.getMergeStrategy(templateRoot);
3✔
124
      if (strategy == null) {
2✔
125
        strategy = XmlMergeStrategy.COMBINE; // default strategy used as fallback
2✔
126
      }
127
      if (templateRoot.lookupPrefix(MERGE_NS_URI) == null) {
4✔
128
        if (this.legacyXmlSupport) {
3✔
129
          if (workspaceFileExists) {
2✔
130
            strategy = XmlMergeStrategy.OVERRIDE;
3✔
131
          } else {
132
            strategy = XmlMergeStrategy.KEEP;
3✔
133
          }
134
        } else {
135
          this.context.warning(
10✔
136
              "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 "
137
                  + "proceed correctly.", source);
138
        }
139
      }
140
      ElementMatcher elementMatcher = new ElementMatcher(this.context, templateDocument.getPath(), workspaceDocument.getPath());
10✔
141
      strategy.merge(templateRoot, workspaceRoot, elementMatcher);
5✔
142
      resultDocument = workspaceDocument.getDocument();
3✔
143
    } else {
1✔
144
      this.context.error("Cannot merge XML template {} with root {} into XML file {} with root {} as roots do not match.", templateDocument.getPath(),
×
145
          templateQName, workspaceDocument.getPath(), workspaceQName);
×
146
      return null;
×
147
    }
148
    return resultDocument;
2✔
149
  }
150

151
  @Override
152
  public void inverseMerge(Path workspace, EnvironmentVariables variables, boolean addNewProperties, Path update) {
153

154
    if (!Files.exists(workspace) || !Files.exists(update)) {
×
155
      return;
×
156
    }
157
    throw new UnsupportedOperationException("not implemented!");
×
158
  }
159

160
  /**
161
   * {@link #load(Path) Loads} and {@link #resolveDocument(XmlMergeDocument, EnvironmentVariables, boolean) resolves} XML from the given file.
162
   *
163
   * @param file the {@link Path} to the XML file.
164
   * @param variables the {@link EnvironmentVariables}.
165
   * @return the loaded {@link XmlMergeDocument}.
166
   */
167
  public XmlMergeDocument loadAndResolve(Path file, EnvironmentVariables variables) {
168

169
    XmlMergeDocument document = load(file);
4✔
170
    resolveDocument(document, variables, false);
5✔
171
    return document;
2✔
172
  }
173

174
  /**
175
   * @param file the {@link Path} to the XML file.
176
   * @return the loaded {@link XmlMergeDocument}.
177
   */
178
  public XmlMergeDocument load(Path file) {
179

180
    try (InputStream in = Files.newInputStream(file)) {
5✔
181
      Document document = DOCUMENT_BUILDER.parse(in);
4✔
182
      return new XmlMergeDocument(document, file);
8✔
183
    } catch (Exception e) {
×
184
      throw new IllegalStateException("Failed to load XML from: " + file, e);
×
185
    }
186
  }
187

188
  /**
189
   * Creates an empty but valid xml file.
190
   *
191
   * @param tagName name of the tag to use for the root node.
192
   * @param file Path of the file to create.
193
   */
194
  public void createValidEmptyXmlFile(String tagName, Path file) {
195
    Document document = DOCUMENT_BUILDER.newDocument();
3✔
196
    Element root = document.createElement(tagName);
4✔
197
    document.appendChild(root);
4✔
198
    save(new XmlMergeDocument(document, file));
7✔
199
  }
1✔
200

201
  /**
202
   * @param document the XML {@link XmlMergeDocument} to save.
203
   */
204
  public void save(XmlMergeDocument document) {
205

206
    save(document.getDocument(), document.getPath());
6✔
207
  }
1✔
208

209
  /**
210
   * @param document the XML {@link Document} to save.
211
   * @param file the {@link Path} to the file where to save the XML.
212
   */
213
  public void save(Document document, Path file) {
214

215
    ensureParentDirectoryExists(file);
2✔
216
    try {
217
      Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3✔
218
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
4✔
219
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
4✔
220
      transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
4✔
221
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
4✔
222

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

230
      DOMSource source = new DOMSource(document);
5✔
231
      StreamResult result = new StreamResult(file.toFile());
6✔
232
      transformer.transform(source, result);
4✔
233
    } catch (Exception e) {
×
234
      throw new IllegalStateException("Failed to save XML to file: " + file, e);
×
235
    }
1✔
236
  }
1✔
237

238
  private void removeWhitespace(Node node) {
239

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

255
  private void resolveDocument(XmlMergeDocument document, EnvironmentVariables variables, boolean inverse) {
256

257
    NodeList nodeList = document.getAllElements();
3✔
258
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
259
      Element element = (Element) nodeList.item(i);
5✔
260
      resolveElement(element, variables, inverse, document.getPath());
7✔
261
    }
262
  }
1✔
263

264
  private void resolveElement(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
265

266
    resolveAttributes(element.getAttributes(), variables, inverse, src);
7✔
267
    NodeList nodeList = element.getChildNodes();
3✔
268
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
269
      Node node = nodeList.item(i);
4✔
270
      if (XmlMergeSupport.isTextual(node)) {
3✔
271
        resolveValue(node, variables, inverse, src);
6✔
272
      }
273
    }
274
  }
1✔
275

276
  private void resolveAttributes(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
277

278
    for (int i = 0; i < attributes.getLength(); i++) {
8✔
279
      Attr attribute = (Attr) attributes.item(i);
5✔
280
      resolveValue(attribute, variables, inverse, src);
6✔
281
    }
282
  }
1✔
283

284
  private void resolveValue(Node node, EnvironmentVariables variables, boolean inverse, Object src) {
285
    String value = node.getNodeValue();
3✔
286
    String resolvedValue;
287
    if (inverse) {
2!
288
      resolvedValue = variables.inverseResolve(value, src);
×
289
    } else {
290
      resolvedValue = variables.resolve(value, src, this.legacySupport);
7✔
291
    }
292
    node.setNodeValue(resolvedValue);
3✔
293
  }
1✔
294

295
  @Override
296
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
297

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

315
  private boolean updateWorkspaceXml(Element element) {
316

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

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

354
  private void checkForXmlNamespace(Document document, Path workspaceFile) {
355

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