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

devonfw / IDEasy / 14648852631

24 Apr 2025 06:22PM UTC coverage: 67.434% (+0.04%) from 67.392%
14648852631

push

github

web-flow
#1037: Fix devonfw-ide compatibility with XML templates (#1212)

Co-authored-by: Jörg Hohwiller <hohwille@users.noreply.github.com>

3078 of 4968 branches covered (61.96%)

Branch coverage included in aggregate %.

7907 of 11322 relevant lines covered (69.84%)

3.06 hits per line

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

80.08
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 source = templateDocument.getPath();
3✔
116
    Path template = 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(
4✔
136
              "XML merge namespace not found. If you are working in a legacy devonfw-ide project, please set IDE_XML_MERGE_LEGACY_SUPPORT_ENABLED=true to "
137
                  + "proceed correctly.");
138
        }
139
      }
140
      ElementMatcher elementMatcher = new ElementMatcher(this.context);
6✔
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
   * @param document the XML {@link XmlMergeDocument} to save.
190
   */
191
  public void save(XmlMergeDocument document) {
192

193
    save(document.getDocument(), document.getPath());
6✔
194
  }
1✔
195

196
  /**
197
   * @param document the XML {@link Document} to save.
198
   * @param file the {@link Path} to the file where to save the XML.
199
   */
200
  public void save(Document document, Path file) {
201

202
    ensureParentDirectoryExists(file);
2✔
203
    try {
204
      Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3✔
205
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
4✔
206
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
4✔
207
      transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
4✔
208
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
4✔
209

210
      // Workaround:
211
      // Remove whitespace from the target document before saving, because if target XML Document is already formatted
212
      // then indent 2 keeps adding empty lines for nothing, and if we don't use indentation then appending/ overriding
213
      // isn't properly formatted.
214
      // https://bugs.openjdk.org/browse/JDK-8262285
215
      removeWhitespace(document.getDocumentElement());
4✔
216

217
      DOMSource source = new DOMSource(document);
5✔
218
      StreamResult result = new StreamResult(file.toFile());
6✔
219
      transformer.transform(source, result);
4✔
220
    } catch (Exception e) {
×
221
      throw new IllegalStateException("Failed to save XML to file: " + file, e);
×
222
    }
1✔
223
  }
1✔
224

225
  private void removeWhitespace(Node node) {
226

227
    NodeList children = node.getChildNodes();
3✔
228
    for (int i = 0; i < children.getLength(); i++) {
8✔
229
      Node child = children.item(i);
4✔
230
      short nodeType = child.getNodeType();
3✔
231
      if (nodeType == Node.TEXT_NODE) {
3✔
232
        if (child.getTextContent().trim().isEmpty()) {
5✔
233
          node.removeChild(child);
4✔
234
          i--;
2✔
235
        }
236
      } else if (nodeType == Node.ELEMENT_NODE) {
3✔
237
        removeWhitespace(child);
3✔
238
      }
239
    }
240
  }
1✔
241

242
  private void resolveDocument(XmlMergeDocument document, EnvironmentVariables variables, boolean inverse) {
243

244
    NodeList nodeList = document.getAllElements();
3✔
245
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
246
      Element element = (Element) nodeList.item(i);
5✔
247
      resolveElement(element, variables, inverse, document.getPath());
7✔
248
    }
249
  }
1✔
250

251
  private void resolveElement(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
252

253
    resolveAttributes(element.getAttributes(), variables, inverse, src);
7✔
254
    NodeList nodeList = element.getChildNodes();
3✔
255
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
256
      Node node = nodeList.item(i);
4✔
257
      if (XmlMergeSupport.isTextual(node)) {
3✔
258
        resolveValue(node, variables, inverse, src);
6✔
259
      }
260
    }
261
  }
1✔
262

263
  private void resolveAttributes(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
264

265
    for (int i = 0; i < attributes.getLength(); i++) {
8✔
266
      Attr attribute = (Attr) attributes.item(i);
5✔
267
      resolveValue(attribute, variables, inverse, src);
6✔
268
    }
269
  }
1✔
270

271
  private void resolveValue(Node node, EnvironmentVariables variables, boolean inverse, Object src) {
272
    String value = node.getNodeValue();
3✔
273
    String resolvedValue;
274
    if (inverse) {
2!
275
      resolvedValue = variables.inverseResolve(value, src);
×
276
    } else {
277
      resolvedValue = variables.resolve(value, src, this.legacySupport);
7✔
278
    }
279
    node.setNodeValue(resolvedValue);
3✔
280
  }
1✔
281

282
  @Override
283
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
284

285
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
2✔
286
    DocumentBuilder builder = factory.newDocumentBuilder();
3✔
287
    Document document = builder.parse(workspaceFile.toFile());
5✔
288
    checkForXmlNamespace(document, workspaceFile);
4✔
289
    boolean modified = updateWorkspaceXml(document.getDocumentElement());
5✔
290
    if (modified) {
2!
291
      TransformerFactory transformerFactory = TransformerFactory.newInstance();
×
292
      Transformer transformer = transformerFactory.newTransformer();
×
293
      DOMSource source = new DOMSource(document);
×
294
      try (BufferedWriter writer = Files.newBufferedWriter(workspaceFile)) {
×
295
        StreamResult result = new StreamResult(writer);
×
296
        transformer.transform(source, result);
×
297
      }
298
    }
299
    return modified;
2✔
300
  }
301

302
  private boolean updateWorkspaceXml(Element element) {
303

304
    boolean modified = false;
2✔
305
    NamedNodeMap attributes = element.getAttributes();
3✔
306
    if (attributes != null) {
2!
307
      for (int i = 0; i < attributes.getLength(); i++) {
8✔
308
        Node node = attributes.item(i);
4✔
309
        if (node instanceof Attr attribute) {
6!
310
          String value = attribute.getValue();
3✔
311
          String migratedValue = upgradeWorkspaceContent(value);
4✔
312
          if (!migratedValue.equals(value)) {
4!
313
            modified = true;
×
314
            attribute.setValue(migratedValue);
×
315
          }
316
        }
317
      }
318
    }
319

320
    NodeList childNodes = element.getChildNodes();
3✔
321
    for (int i = 0; i < childNodes.getLength(); i++) {
8✔
322
      Node childNode = childNodes.item(i);
4✔
323
      boolean childModified = false;
2✔
324
      if (childNode instanceof Element childElement) {
6✔
325
        childModified = updateWorkspaceXml(childElement);
5✔
326
      } else if (childNode instanceof Text childText) {
6!
327
        String text = childText.getTextContent();
3✔
328
        String migratedText = upgradeWorkspaceContent(text);
4✔
329
        childModified = !migratedText.equals(text);
6!
330
        if (childModified) {
2!
331
          childText.setTextContent(migratedText);
×
332
        }
333
      }
334
      if (childModified) {
2!
335
        modified = true;
×
336
      }
337
    }
338
    return modified;
2✔
339
  }
340

341
  private void checkForXmlNamespace(Document document, Path workspaceFile) {
342

343
    NamedNodeMap attributes = document.getDocumentElement().getAttributes();
4✔
344
    if (attributes != null) {
2!
345
      for (int i = 0; i < attributes.getLength(); i++) {
8✔
346
        Node node = attributes.item(i);
4✔
347
        String uri = node.getNamespaceURI();
3✔
348
        if (MERGE_NS_URI.equals(uri)) {
4!
349
          return;
×
350
        }
351
      }
352
    }
353
    this.context.warning(
10✔
354
        "The XML file {} does not contain the XML merge namespace and seems outdated. For details see:\n"
355
            + "https://github.com/devonfw/IDEasy/blob/main/documentation/configurator.adoc#xml-merger", workspaceFile);
356
  }
1✔
357

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