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

devonfw / IDEasy / 11560364436

28 Oct 2024 06:31PM UTC coverage: 66.705% (-0.04%) from 66.746%
11560364436

push

github

web-flow
#710: make workspace configuration robust (#719)

2406 of 3950 branches covered (60.91%)

Branch coverage included in aggregate %.

6275 of 9064 relevant lines covered (69.23%)

3.05 hits per line

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

73.05
cli/src/main/java/com/devonfw/tools/ide/merge/xmlmerger/XmlMerger.java
1
package com.devonfw.tools.ide.merge.xmlmerger;
2

3
import java.io.InputStream;
4
import java.nio.file.Files;
5
import java.nio.file.Path;
6
import java.util.Objects;
7
import javax.xml.parsers.DocumentBuilder;
8
import javax.xml.parsers.DocumentBuilderFactory;
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.Document;
17
import org.w3c.dom.Element;
18
import org.w3c.dom.NamedNodeMap;
19
import org.w3c.dom.Node;
20
import org.w3c.dom.NodeList;
21
import org.w3c.dom.Text;
22

23
import com.devonfw.tools.ide.context.IdeContext;
24
import com.devonfw.tools.ide.environment.EnvironmentVariables;
25
import com.devonfw.tools.ide.merge.FileMerger;
26
import com.devonfw.tools.ide.merge.xmlmerger.matcher.ElementMatcher;
27
import com.devonfw.tools.ide.merge.xmlmerger.model.MergeElement;
28

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

34
  private static final DocumentBuilder DOCUMENT_BUILDER;
35

36
  private static final TransformerFactory TRANSFORMER_FACTORY;
37

38
  public static final String MERGE_NS_URI = "https://github.com/devonfw/IDEasy/merge";
39

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

51
  public XmlMerger(IdeContext context) {
52

53
    super(context);
3✔
54
  }
1✔
55

56
  @Override
57
  protected void doMerge(Path setup, Path update, EnvironmentVariables resolver, Path workspace) {
58

59
    Document document = null;
2✔
60
    Path template = setup;
2✔
61
    Path target = workspace;
2✔
62
    boolean updateFileExists = Files.exists(update);
5✔
63
    if (Files.exists(workspace)) {
5✔
64
      if (!updateFileExists) {
2✔
65
        return; // nothing to do ...
1✔
66
      }
67
      document = load(workspace);
5✔
68
    } else if (Files.exists(setup)) {
5!
69
      document = load(setup);
4✔
70
      target = setup;
2✔
71
    }
72
    if (updateFileExists) {
2✔
73
      template = update;
2✔
74
      if (document == null) {
2!
75
        document = load(update);
×
76
      } else {
77
        Document updateDocument = load(update);
4✔
78
        merge(updateDocument, document, template, target);
6✔
79
      }
80
    }
81
    if (document != null) {
2!
82
      resolve(document, resolver, false, template);
6✔
83
      save(document, workspace);
4✔
84
    }
85
    return;
1✔
86
  }
87

88
  /**
89
   * Merges the source document with the target document.
90
   *
91
   * @param sourceDocument The {@link Document} representing the source xml file.
92
   * @param targetDocument The {@link Document} representing the target xml file.
93
   * @param source {@link Path} to the source document.
94
   * @param target {@link Path} to the target document.
95
   */
96
  public void merge(Document sourceDocument, Document targetDocument, Path source, Path target) {
97

98
    this.context.debug("Merging {} with {} ...", source.getFileName().toString(), target.getFileName().toString());
18✔
99
    MergeElement sourceRoot = new MergeElement(sourceDocument.getDocumentElement(), source);
7✔
100
    MergeElement targetRoot = new MergeElement(targetDocument.getDocumentElement(), target);
7✔
101

102
    if (areRootsCompatible(sourceRoot, targetRoot)) {
5!
103
      MergeStrategy strategy = sourceRoot.getMergingStrategy();
3✔
104
      if (strategy == null) {
2✔
105
        strategy = MergeStrategy.KEEP; // default strategy used as fallback
2✔
106
      }
107
      strategy.merge(sourceRoot, targetRoot, new ElementMatcher(this.context));
9✔
108
    } else {
1✔
109
      this.context.warning("Root elements do not match. Skipping merge operation.");
×
110
    }
111
  }
1✔
112

113
  /**
114
   * Checks the compatibility (tagname and namespaceURI) of the given root elements to be merged.
115
   *
116
   * @param sourceRoot the {@link MergeElement} representing the root element of the source document.
117
   * @param targetRoot the {@link MergeElement} representing the root element of the target document.
118
   * @return {@code true} when the roots are compatible, otherwise {@code false}.
119
   */
120
  private boolean areRootsCompatible(MergeElement sourceRoot, MergeElement targetRoot) {
121

122
    Element sourceElement = sourceRoot.getElement();
3✔
123
    Element targetElement = targetRoot.getElement();
3✔
124

125
    // Check if tag names match
126
    if (!sourceElement.getTagName().equals(targetElement.getTagName())) {
6!
127
      this.context.warning("Names of root elements of {} and {} don't match. Found {} and {}",
×
128
          sourceRoot.getDocumentPath(), targetRoot.getDocumentPath(),
×
129
          sourceElement.getTagName(), targetElement.getTagName());
×
130
      return false;
×
131
    }
132

133
    // Check if namespace URIs match (if they exist)
134
    String sourceNs = sourceElement.getNamespaceURI();
3✔
135
    String targetNs = targetElement.getNamespaceURI();
3✔
136
    if (sourceNs != null || targetNs != null) {
4!
137
      if (!Objects.equals(sourceNs, targetNs)) {
×
138
        this.context.warning("URI of root elements of {} and {} don't match. Found {} and {}",
×
139
            sourceRoot.getDocumentPath(), targetRoot.getDocumentPath(),
×
140
            sourceNs, targetNs);
141
        return false;
×
142
      }
143
    }
144

145
    return true;
2✔
146
  }
147

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

151
    if (!Files.exists(workspace) || !Files.exists(update)) {
×
152
      return;
×
153
    }
154
    Document updateDocument = load(update);
×
155
    Document workspaceDocument = load(workspace);
×
156
    resolve(updateDocument, variables, true, workspace.getFileName());
×
157
    MergeStrategy strategy = MergeStrategy.OVERRIDE;
×
158
    MergeElement sourceRoot = new MergeElement(workspaceDocument.getDocumentElement(), workspace);
×
159
    MergeElement targetRoot = new MergeElement(updateDocument.getDocumentElement(), update);
×
160
    strategy.merge(sourceRoot, targetRoot, null);
×
161
    save(updateDocument, update);
×
162
    this.context.debug("Saved changes in {} to {}", workspace.getFileName(), update);
×
163
  }
×
164

165
  public Document load(Path file) {
166

167
    try (InputStream in = Files.newInputStream(file)) {
5✔
168
      return DOCUMENT_BUILDER.parse(in);
6✔
169
    } catch (Exception e) {
×
170
      throw new IllegalStateException("Failed to load XML from: " + file, e);
×
171
    }
172
  }
173

174
  public void save(Document document, Path file) {
175

176
    ensureParentDirectoryExists(file);
2✔
177
    try {
178
      Transformer transformer = TRANSFORMER_FACTORY.newTransformer();
3✔
179
      transformer.setOutputProperty(OutputKeys.INDENT, "yes");
4✔
180
      transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
4✔
181
      transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
4✔
182
      transformer.setOutputProperty(OutputKeys.STANDALONE, "no");
4✔
183
      transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no");
4✔
184

185
      // Workaround:
186
      // Remove whitespace from the target document before saving, because if target XML Document is already formatted
187
      // then indent 2 keeps adding empty lines for nothing, and if we don't use indentation then appending/ overriding
188
      // isn't properly formatted.
189
      removeWhitespace(document.getDocumentElement());
4✔
190

191
      DOMSource source = new DOMSource(document);
5✔
192
      StreamResult result = new StreamResult(file.toFile());
6✔
193
      transformer.transform(source, result);
4✔
194
    } catch (Exception e) {
×
195
      throw new IllegalStateException("Failed to save XML to file: " + file, e);
×
196
    }
1✔
197
  }
1✔
198

199
  private void removeWhitespace(Node node) {
200

201
    NodeList children = node.getChildNodes();
3✔
202
    for (int i = 0; i < children.getLength(); i++) {
8✔
203
      Node child = children.item(i);
4✔
204
      if (child.getNodeType() == Node.TEXT_NODE) {
4✔
205
        if (child.getTextContent().trim().isEmpty()) {
5✔
206
          node.removeChild(child);
4✔
207
          i--;
2✔
208
        }
209
      } else {
210
        removeWhitespace(child);
3✔
211
      }
212
    }
213
  }
1✔
214

215
  private void resolve(Document document, EnvironmentVariables resolver, boolean inverse, Object src) {
216

217
    NodeList nodeList = document.getElementsByTagName("*");
4✔
218
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
219
      Element element = (Element) nodeList.item(i);
5✔
220
      resolve(element, resolver, inverse, src);
6✔
221
    }
222
  }
1✔
223

224
  private void resolve(Element element, EnvironmentVariables variables, boolean inverse, Object src) {
225

226
    resolve(element.getAttributes(), variables, inverse, src);
7✔
227
    NodeList nodeList = element.getChildNodes();
3✔
228

229
    for (int i = 0; i < nodeList.getLength(); i++) {
8✔
230
      Node node = nodeList.item(i);
4✔
231
      if (node instanceof Text text) {
6✔
232
        String value = text.getNodeValue();
3✔
233
        String resolvedValue;
234
        if (inverse) {
2!
235
          resolvedValue = variables.inverseResolve(value, src);
×
236
        } else {
237
          resolvedValue = variables.resolve(value, src, this.legacySupport);
7✔
238
        }
239
        text.setNodeValue(resolvedValue);
3✔
240
      }
241
    }
242
  }
1✔
243

244
  private void resolve(NamedNodeMap attributes, EnvironmentVariables variables, boolean inverse, Object src) {
245

246
    for (int i = 0; i < attributes.getLength(); i++) {
8✔
247
      Attr attribute = (Attr) attributes.item(i);
5✔
248
      String value = attribute.getValue();
3✔
249
      String resolvedValue;
250
      if (inverse) {
2!
251
        resolvedValue = variables.inverseResolve(value, src);
×
252
      } else {
253
        resolvedValue = variables.resolve(value, src, this.legacySupport);
7✔
254
      }
255
      attribute.setValue(resolvedValue);
3✔
256
    }
257
  }
1✔
258
}
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