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

devonfw / IDEasy / 14646105316

24 Apr 2025 03:57PM UTC coverage: 67.476% (+0.05%) from 67.429%
14646105316

Pull #1212

github

web-flow
Merge f7ff62046 into b3452f8b4
Pull Request #1212: #1037: Fix devonfw-ide compatibility with XML templates

3085 of 4974 branches covered (62.02%)

Branch coverage included in aggregate %.

7925 of 11343 relevant lines covered (69.87%)

3.06 hits per line

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

80.24
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 (this.legacyXmlSupport && (templateRoot.lookupPrefix(MERGE_NS_URI) == null)) {
7✔
128
        if (workspaceFileExists) {
2✔
129
          strategy = XmlMergeStrategy.OVERRIDE;
3✔
130
        } else {
131
          strategy = XmlMergeStrategy.KEEP;
3✔
132
        }
133
      } else if (templateRoot.lookupNamespaceURI("merge") == null) {
4✔
134
        this.context.warning(
4✔
135
            "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 "
136
                + "proceed correctly.");
137
      }
138
      ElementMatcher elementMatcher = new ElementMatcher(this.context);
6✔
139
      strategy.merge(templateRoot, workspaceRoot, elementMatcher);
5✔
140
      resultDocument = workspaceDocument.getDocument();
3✔
141
    } else {
1✔
142
      this.context.error("Cannot merge XML template {} with root {} into XML file {} with root {} as roots do not match.", templateDocument.getPath(),
×
143
          templateQName, workspaceDocument.getPath(), workspaceQName);
×
144
      return null;
×
145
    }
146
    return resultDocument;
2✔
147
  }
148

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

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

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

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

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

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

186
  /**
187
   * @param document the XML {@link XmlMergeDocument} to save.
188
   */
189
  public void save(XmlMergeDocument document) {
190

191
    save(document.getDocument(), document.getPath());
6✔
192
  }
1✔
193

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

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

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

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

223
  private void removeWhitespace(Node node) {
224

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

240
  private void resolveDocument(XmlMergeDocument document, EnvironmentVariables variables, boolean inverse) {
241

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

249
  private void resolveElement(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
250

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

261
  private void resolveAttributes(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
262

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

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

280
  @Override
281
  protected boolean doUpgrade(Path workspaceFile) throws Exception {
282

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

300
  private boolean updateWorkspaceXml(Element element) {
301

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

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

339
  private void checkForXmlNamespace(Document document, Path workspaceFile) {
340

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

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

© 2025 Coveralls, Inc