• 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

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

3
import java.io.StringWriter;
4
import java.util.Objects;
5
import java.util.function.BiFunction;
6
import java.util.function.Predicate;
7
import javax.xml.XMLConstants;
8
import javax.xml.namespace.QName;
9
import javax.xml.transform.OutputKeys;
10
import javax.xml.transform.Transformer;
11
import javax.xml.transform.TransformerFactory;
12
import javax.xml.transform.dom.DOMSource;
13
import javax.xml.transform.stream.StreamResult;
14

15
import org.w3c.dom.Attr;
16
import org.w3c.dom.Element;
17
import org.w3c.dom.NamedNodeMap;
18
import org.w3c.dom.Node;
19
import org.w3c.dom.NodeList;
20

21
/**
22
 * Utility class with static helper methods for XML merging.
23
 */
24
public interface XmlMergeSupport {
25

26
  /**
27
   * The XML namespace used by this {@link XmlMerger} to configure merge specific meta-information.
28
   *
29
   * @see XmlMergeSupport#hasMergeNamespace(Attr)
30
   */
31
  String MERGE_NS_URI = "https://github.com/devonfw/IDEasy/merge";
32

33
  /** {@link Attr#getName() Attribute name} {@value} */
34
  String ATTR_ID = "id";
35

36
  /** XPath for {@link Attr} {@value} */
37
  String XPATH_ATTR_ID = "@id";
38

39
  /** {@link Attr#getName() Attribute name} {@value} */
40
  String ATTR_NAME = "name";
41

42
  /** XPath for {@link Attr} {@value} */
43
  String XPATH_ATTR_NAME = "@name";
44

45
  /** XPath for {@link Element#getTagName() element tag name}: {@value} */
46
  String XPATH_ELEMENT_NAME = "name()";
47

48
  /** {@link Attr#getName() Attribute name} {@value} */
49
  String ATTR_STRATEGY = "strategy";
50

51
  /** XPath for {@link Element#getTextContent() element text}: {@value} */
52
  String XPATH_ELEMENT_TEXT = "text()";
53

54
  /**
55
   * @param attribute the XML {@link Attr} to check.
56
   * @return {@code true} if the {@code attribute} is specific for our XML merger and related to {@link XmlMergeSupport#MERGE_NS_URI} (that needs to be omitted
57
   *     in the XML output), {@code false} otherwise (regular {@code attribute} for the target payload).
58
   */
59
  static boolean hasMergeNamespace(Attr attribute) {
60

61
    if (MERGE_NS_URI.equals(attribute.getNamespaceURI())) {
5✔
62
      return true;
2✔
63
    } else {
64
      return "xmlns".equals(attribute.getPrefix()) && MERGE_NS_URI.equals(attribute.getValue());
14✔
65
    }
66
  }
67

68
  /**
69
   * @param element the {@link Element} for which the XPath is requested.
70
   * @return the XPath expression.
71
   */
72
  static String getXPath(Element element) {
73

74
    return getXPath(element, false);
×
75
  }
76

77
  /**
78
   * @param element the {@link Element} for which the XPath is requested.
79
   * @param includeAttributes {@code true} to also include the attributes of the {@link Element}s to the XPath (for debugging), {@code false} otherwise.
80
   * @return the XPath expression.
81
   */
82
  static String getXPath(Element element, boolean includeAttributes) {
83

84
    StringBuilder sb = new StringBuilder();
4✔
85
    getXPath(sb, element, includeAttributes);
4✔
86
    return sb.toString();
3✔
87
  }
88

89
  private static void getXPath(StringBuilder sb, Element element, boolean includeAttributes) {
90

91
    Node parent = element.getParentNode();
3✔
92
    if ((parent != null) && (parent.getNodeType() == Node.ELEMENT_NODE)) {
6!
93
      getXPath(sb, (Element) parent, includeAttributes);
5✔
94
    }
95
    sb.append('/');
4✔
96
    // for qualified node this will append «prefix»:«local-name» and then the matching of XPath depends on the chosen prefix so it would not match
97
    // «prefix2»:«local-name» even if «prefix» and «prefix2» would resolve to the same URL (each in their own XML document).
98
    appendName(sb, element);
3✔
99
    if (includeAttributes) {
2!
100
      sb.append('[');
4✔
101
      appendAttributes(sb, element, "@", false);
5✔
102
      sb.append(']');
4✔
103
    }
104
  }
1✔
105

106
  /**
107
   * @param sb the {@link StringBuilder} where to {@link StringBuilder#append(String) append} the XML.
108
   * @param node the {@link Node} ({@link Element} or {@link Attr}) with the name to append.
109
   */
110
  static void appendName(StringBuilder sb, Node node) {
111

112
    sb.append(node.getNodeName());
5✔
113
  }
1✔
114

115
  /**
116
   * @param sb the {@link StringBuilder} where to {@link StringBuilder#append(String) append} the XML.
117
   * @param element the {@link Element} {@link Element#getAttributes() containing the attributes} to append.
118
   */
119
  static void appendAttributes(StringBuilder sb, Element element) {
120

121
    appendAttributes(sb, element, "", true);
×
122
  }
×
123

124
  /**
125
   * @param sb the {@link StringBuilder} where to {@link StringBuilder#append(String) append} the XML.
126
   * @param element the {@link Element} {@link Element#getAttributes() containing the attributes} to append.
127
   * @param attributePrefix the prefix for each attribute (typically the empty string but may be "@" for XPath syntax).
128
   * @param includeMergeNs {@code true} to also include attributes {@link #hasMergeNamespace(Attr) with merge namespace}, {@code false} otherwise.
129
   */
130
  static void appendAttributes(StringBuilder sb, Element element, String attributePrefix, boolean includeMergeNs) {
131
    NamedNodeMap attributes = element.getAttributes();
3✔
132
    int attributeCount = attributes.getLength();
3✔
133
    boolean separator = false;
2✔
134
    for (int i = 0; i < attributeCount; i++) {
7✔
135
      Attr attribute = (Attr) attributes.item(i);
5✔
136
      if (includeMergeNs || !hasMergeNamespace(attribute)) {
5!
137
        if (separator) {
2✔
138
          sb.append(" ");
5✔
139
        } else {
140
          separator = true;
2✔
141
        }
142
        sb.append(attributePrefix);
4✔
143
        appendName(sb, attribute);
3✔
144
        sb.append("='");
4✔
145
        sb.append(escapeSingleQuotes(attribute.getValue()));
6✔
146
        sb.append('\'');
4✔
147
      }
148
    }
149
  }
1✔
150

151
  /**
152
   * @param value the {@link String} value.
153
   * @return the given {@link String} with potential single quotes escaped for XML (e.g. to use in attributes).
154
   */
155
  static String escapeSingleQuotes(String value) {
156

157
    return value.replace("'", "&apos;");
5✔
158
  }
159

160
  /**
161
   * @param element the {@link Element}.
162
   * @return the {@link QName} (URI + local name).
163
   */
164
  static QName getQualifiedName(Element element) {
165

166
    if (element == null) {
2!
167
      return null;
×
168
    }
169
    String namespaceURI = element.getNamespaceURI();
3✔
170
    String localName = element.getLocalName();
3✔
171
    if (localName == null) {
2!
172
      localName = element.getTagName();
×
173
    }
174
    return new QName(namespaceURI, localName);
6✔
175
  }
176

177
  /**
178
   * @param node the {@link Node} to check.
179
   * @return {@code true} if the {@link Node} is a {@link Node#TEXT_NODE text node} or a {@link Node#CDATA_SECTION_NODE CDATA section}.
180
   */
181
  static boolean isTextual(Node node) {
182

183
    short nodeType = node.getNodeType();
3✔
184
    return (nodeType == Node.TEXT_NODE) || (nodeType == Node.CDATA_SECTION_NODE);
10!
185
  }
186

187
  /**
188
   * @param document the {@link XmlMergeSupport} from which to remove all {@link #hasMergeNamespace(Attr) merge namespace} {@link Attr attributes}.
189
   */
190
  static void removeMergeNsAttributes(XmlMergeDocument document) {
191

192
    NodeList nodeList = document.getAllElements();
3✔
193
    for (int i = nodeList.getLength() - 1; i >= 0; i--) {
9✔
194
      Element element = (Element) nodeList.item(i);
5✔
195
      removeMergeNsAttributes(element);
2✔
196
    }
197
  }
1✔
198

199
  /**
200
   * @param element the {@link Element} from which to remove {@link #hasMergeNamespace(Attr) merge namespace} {@link Attr attributes}.
201
   */
202
  static void removeMergeNsAttributes(Element element) {
203

204
    removeAttributes(element, XmlMergeSupport::hasMergeNamespace);
3✔
205
  }
1✔
206

207
  /**
208
   * @param element the {@link Element} from which to remove matching {@link Attr attributes}.
209
   * @param filter the {@link Predicate} that {@link Predicate#test(Object) decides} if an {@link Attr attribute} shall be removed (if {@code true}) or
210
   *     not.
211
   */
212
  static void removeAttributes(Element element, Predicate<Attr> filter) {
213

214
    NamedNodeMap attributes = element.getAttributes();
3✔
215
    // we iterate backwards so we do not cause errors by removing attributes inside the loop
216
    for (int i = attributes.getLength() - 1; i >= 0; i--) {
9✔
217
      Attr attribute = (Attr) attributes.item(i);
5✔
218
      if (filter.test(attribute)) {
4✔
219
        element.removeAttributeNode(attribute);
4✔
220
      }
221
    }
222
  }
1✔
223

224
  /**
225
   * @param templateElement the template {@link Element} with the {@link Attr attributes} to combine.
226
   * @param resultElement the workspace {@link Element} where to add the {@link Attr attributes} to combine.
227
   * @param attributeMerger the {@link BiFunction} that decides how to merge {@link Attr attributes} with identical name or {@code null} to always override
228
   *     template attribute value in workspace attribute.
229
   */
230
  static void combineAttributes(Element templateElement, Element resultElement, BiFunction<Attr, Attr, String> attributeMerger) {
231

232
    NamedNodeMap attributes = templateElement.getAttributes();
3✔
233
    int length = attributes.getLength();
3✔
234
    for (int i = 0; i < length; i++) {
7✔
235
      Attr attribute = (Attr) attributes.item(i);
5✔
236
      if (!XmlMergeSupport.hasMergeNamespace(attribute)) {
3✔
237
        String namespaceUri = attribute.getNamespaceURI();
3✔
238
        String attrName = attribute.getName();
3✔
239
        String attrValue = attribute.getValue();
3✔
240
        if ((namespaceUri != null) && !namespaceUri.isEmpty()) {
5!
241
          String prefix = attribute.getPrefix();
3✔
242
          if ((prefix != null) && !prefix.isEmpty()) {
5!
243
            if (resultElement.getAttributeNodeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, prefix) == null) {
5✔
244
              resultElement.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + prefix, namespaceUri);
6✔
245
            }
246
          }
247
          resultElement.setAttributeNS(namespaceUri, attrName, attrValue);
5✔
248
        } else {
1✔
249
          if (attributeMerger != null) {
2!
250
            Attr workspaceAttr = resultElement.getAttributeNode(attrName);
×
251
            if (workspaceAttr != null) {
×
252
              if (!Objects.equals(attrValue, workspaceAttr.getValue())) {
×
253
                // attribute already exists in workspaceElement but has a different value - let attributeMerger decide how to merge
254
                attrValue = attributeMerger.apply(attribute, workspaceAttr);
×
255
              }
256
            }
257
          }
258
          resultElement.setAttribute(attrName, attrValue);
4✔
259
        }
260

261
      }
262
    }
263
  }
1✔
264

265
  /**
266
   * @param element the {@link Element} to get the "merge:id" for.
267
   * @return the merge ID ({@link javax.xml.xpath.XPath} expression). Will fall back following convention over configuration if "merge:id" is not explicitly
268
   *     configured. Will be {@code null} if no fallback could be found.
269
   */
270
  static String getMergeId(Element element) {
271

272
    String id = element.getAttributeNS(MERGE_NS_URI, ATTR_ID);
5✔
273
    if (!id.isEmpty()) {
3✔
274
      return id;
2✔
275
    }
276
    if (element.hasAttribute(ATTR_ID)) {
4✔
277
      return XPATH_ATTR_ID;
2✔
278
    } else if (element.hasAttribute(ATTR_NAME)) {
4✔
279
      return XPATH_ATTR_NAME;
2✔
280
    }
281
    NamedNodeMap attributes = element.getAttributes();
3✔
282
    int attributeCount = attributes.getLength();
3✔
283
    if (attributeCount == 0) {
2✔
284
      return XPATH_ELEMENT_NAME;
2✔
285
    }
286
    id = XPATH_ELEMENT_NAME;
2✔
287
    for (int i = 0; i < attributeCount; i++) {
7✔
288
      Attr attribute = (Attr) attributes.item(i);
5✔
289
      if (!hasMergeNamespace(attribute)) {
3✔
290
        if (id == XPATH_ELEMENT_NAME) {
3✔
291
          id = "@" + attribute.getName();
5✔
292
        } else {
293
          id = null;
2✔
294
          break;
1✔
295
        }
296
      }
297
    }
298
    return id;
2✔
299
  }
300

301
  /**
302
   * @param element the {@link Element} to get the "merge:strategy" for.
303
   * @return the {@link XmlMergeStrategy} of this element or {@code null} if undefined.
304
   */
305
  static XmlMergeStrategy getMergeStrategy(Element element) {
306

307
    String strategy = element.getAttributeNS(MERGE_NS_URI, ATTR_STRATEGY);
5✔
308
    if (!strategy.isEmpty()) {
3✔
309
      return XmlMergeStrategy.of(strategy);
3✔
310
    }
311
    return null;
2✔
312
  }
313

314
  /**
315
   * @param node the {@link Node}.
316
   * @return the XML of the given {@link Node} for debugging (
317
   */
318
  static String getXml(Node node) {
319

320
    try {
321
      StringWriter writer = new StringWriter();
×
322
      Transformer transformer = TransformerFactory.newInstance().newTransformer();
×
323
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
×
324
      transformer.transform(new DOMSource(node), new StreamResult(writer));
×
325
      return writer.toString();
×
326
    } catch (Exception e) {
×
327
      return e.toString();
×
328
    }
329
  }
330

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