1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.internal.xml;
20
21 import javax.xml.stream.XMLInputFactory;
22 import javax.xml.stream.XMLOutputFactory;
23 import javax.xml.stream.XMLStreamException;
24 import javax.xml.stream.XMLStreamReader;
25 import javax.xml.stream.XMLStreamWriter;
26
27 import java.io.IOException;
28 import java.io.InputStream;
29 import java.io.Reader;
30 import java.io.Writer;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.Iterator;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Objects;
37 import java.util.Optional;
38 import java.util.Set;
39 import java.util.stream.Collectors;
40 import java.util.stream.Stream;
41
42 import org.apache.maven.api.annotations.Nonnull;
43 import org.apache.maven.api.annotations.Nullable;
44 import org.apache.maven.api.xml.XmlNode;
45 import org.apache.maven.api.xml.XmlService;
46 import org.codehaus.stax2.util.StreamWriterDelegate;
47
48 public class DefaultXmlService extends XmlService {
49 private static final boolean DEFAULT_TRIM = true;
50
51 @Nonnull
52 @Override
53 public XmlNode doRead(InputStream input, @Nullable XmlService.InputLocationBuilder locationBuilder)
54 throws XMLStreamException {
55 XMLStreamReader parser = XMLInputFactory.newFactory().createXMLStreamReader(input);
56 return doRead(parser, locationBuilder);
57 }
58
59 @Nonnull
60 @Override
61 public XmlNode doRead(Reader reader, @Nullable XmlService.InputLocationBuilder locationBuilder)
62 throws XMLStreamException {
63 XMLStreamReader parser = XMLInputFactory.newFactory().createXMLStreamReader(reader);
64 return doRead(parser, locationBuilder);
65 }
66
67 @Nonnull
68 @Override
69 public XmlNode doRead(XMLStreamReader parser, @Nullable XmlService.InputLocationBuilder locationBuilder)
70 throws XMLStreamException {
71 return doBuild(parser, DEFAULT_TRIM, locationBuilder);
72 }
73
74 private XmlNode doBuild(XMLStreamReader parser, boolean trim, InputLocationBuilder locationBuilder)
75 throws XMLStreamException {
76 boolean spacePreserve = false;
77 String lPrefix = null;
78 String lNamespaceUri = null;
79 String lName = null;
80 String lValue = null;
81 Object location = null;
82 Map<String, String> attrs = null;
83 List<XmlNode> children = null;
84 int eventType = parser.getEventType();
85 int lastStartTag = -1;
86 while (eventType != XMLStreamReader.END_DOCUMENT) {
87 if (eventType == XMLStreamReader.START_ELEMENT) {
88 lastStartTag = parser.getLocation().getLineNumber() * 1000
89 + parser.getLocation().getColumnNumber();
90 if (lName == null) {
91 int namespacesSize = parser.getNamespaceCount();
92 lPrefix = parser.getPrefix();
93 lNamespaceUri = parser.getNamespaceURI();
94 lName = parser.getLocalName();
95 location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null;
96 int attributesSize = parser.getAttributeCount();
97 if (attributesSize > 0 || namespacesSize > 0) {
98 attrs = new HashMap<>();
99 for (int i = 0; i < namespacesSize; i++) {
100 String nsPrefix = parser.getNamespacePrefix(i);
101 String nsUri = parser.getNamespaceURI(i);
102 attrs.put(nsPrefix != null && !nsPrefix.isEmpty() ? "xmlns:" + nsPrefix : "xmlns", nsUri);
103 }
104 for (int i = 0; i < attributesSize; i++) {
105 String aName = parser.getAttributeLocalName(i);
106 String aValue = parser.getAttributeValue(i);
107 String aPrefix = parser.getAttributePrefix(i);
108 if (aPrefix != null && !aPrefix.isEmpty()) {
109 aName = aPrefix + ":" + aName;
110 }
111 attrs.put(aName, aValue);
112 spacePreserve = spacePreserve || ("xml:space".equals(aName) && "preserve".equals(aValue));
113 }
114 }
115 } else {
116 if (children == null) {
117 children = new ArrayList<>();
118 }
119 XmlNode child = doBuild(parser, trim, locationBuilder);
120 children.add(child);
121 }
122 } else if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) {
123 String text = parser.getText();
124 lValue = lValue != null ? lValue + text : text;
125 } else if (eventType == XMLStreamReader.END_ELEMENT) {
126 boolean emptyTag = lastStartTag
127 == parser.getLocation().getLineNumber() * 1000
128 + parser.getLocation().getColumnNumber();
129 if (lValue != null && trim && !spacePreserve) {
130 lValue = lValue.trim();
131 }
132 return XmlNode.newBuilder()
133 .prefix(lPrefix)
134 .namespaceUri(lNamespaceUri)
135 .name(lName)
136 .value(children == null ? (lValue != null ? lValue : emptyTag ? null : "") : null)
137 .attributes(attrs)
138 .children(children)
139 .inputLocation(location)
140 .build();
141 }
142 eventType = parser.next();
143 }
144 throw new IllegalStateException("End of document found before returning to 0 depth");
145 }
146
147 @Override
148 public void doWrite(XmlNode node, Writer writer) throws IOException {
149 try {
150 XMLOutputFactory factory = new com.ctc.wstx.stax.WstxOutputFactory();
151 factory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, false);
152 factory.setProperty(com.ctc.wstx.api.WstxOutputProperties.P_USE_DOUBLE_QUOTES_IN_XML_DECL, true);
153 factory.setProperty(com.ctc.wstx.api.WstxOutputProperties.P_ADD_SPACE_AFTER_EMPTY_ELEM, true);
154 XMLStreamWriter serializer = new IndentingXMLStreamWriter(factory.createXMLStreamWriter(writer));
155 writeNode(serializer, node);
156 serializer.close();
157 } catch (XMLStreamException e) {
158 throw new IOException(e);
159 }
160 }
161
162 private void writeNode(XMLStreamWriter xmlWriter, XmlNode node) throws XMLStreamException {
163 xmlWriter.writeStartElement(node.prefix(), node.name(), node.namespaceUri());
164
165 for (Map.Entry<String, String> attr : node.attributes().entrySet()) {
166 xmlWriter.writeAttribute(attr.getKey(), attr.getValue());
167 }
168
169 for (XmlNode child : node.children()) {
170 writeNode(xmlWriter, child);
171 }
172
173 String value = node.value();
174 if (value != null) {
175 xmlWriter.writeCharacters(value);
176 }
177
178 xmlWriter.writeEndElement();
179 }
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216 @SuppressWarnings("checkstyle:MethodLength")
217 public XmlNode doMerge(XmlNode dominant, XmlNode recessive, Boolean childMergeOverride) {
218
219 if (recessive == null) {
220 return dominant;
221 }
222 if (dominant == null) {
223 return recessive;
224 }
225
226 boolean mergeSelf = true;
227
228 String selfMergeMode = getSelfCombinationMode(dominant);
229
230 if (SELF_COMBINATION_OVERRIDE.equals(selfMergeMode)) {
231 mergeSelf = false;
232 }
233
234 if (mergeSelf) {
235
236 String value = dominant.value();
237 Object location = dominant.inputLocation();
238 Map<String, String> attrs = dominant.attributes();
239 List<XmlNode> children = null;
240
241 for (Map.Entry<String, String> attr : recessive.attributes().entrySet()) {
242 String key = attr.getKey();
243 if (isEmpty(attrs.get(key))) {
244 if (attrs == dominant.attributes()) {
245 attrs = new HashMap<>(attrs);
246 }
247 attrs.put(key, attr.getValue());
248 }
249 }
250
251 if (!recessive.children().isEmpty()) {
252 boolean mergeChildren = true;
253 if (childMergeOverride != null) {
254 mergeChildren = childMergeOverride;
255 } else {
256 String childCombinationMode = getChildCombinationMode(attrs);
257 if (CHILDREN_COMBINATION_APPEND.equals(childCombinationMode)) {
258 mergeChildren = false;
259 }
260 }
261
262 Map<String, Iterator<XmlNode>> commonChildren = new HashMap<>();
263 Set<String> names =
264 recessive.children().stream().map(XmlNode::name).collect(Collectors.toSet());
265 for (String name : names) {
266 List<XmlNode> dominantChildren = dominant.children().stream()
267 .filter(n -> n.name().equals(name))
268 .toList();
269 if (!dominantChildren.isEmpty()) {
270 commonChildren.put(name, dominantChildren.iterator());
271 }
272 }
273
274 String keysValue = recessive.attribute(KEYS_COMBINATION_MODE_ATTRIBUTE);
275
276 int recessiveChildIndex = 0;
277 for (XmlNode recessiveChild : recessive.children()) {
278 String idValue = recessiveChild.attribute(ID_COMBINATION_MODE_ATTRIBUTE);
279
280 XmlNode childDom = null;
281 if (!isEmpty(idValue)) {
282 for (XmlNode dominantChild : dominant.children()) {
283 if (idValue.equals(dominantChild.attribute(ID_COMBINATION_MODE_ATTRIBUTE))) {
284 childDom = dominantChild;
285
286 mergeChildren = true;
287 }
288 }
289 } else if (!isEmpty(keysValue)) {
290 String[] keys = keysValue.split(",");
291 Map<String, Optional<String>> recessiveKeyValues = Stream.of(keys)
292 .collect(Collectors.toMap(
293 k -> k, k -> Optional.ofNullable(recessiveChild.attribute(k))));
294
295 for (XmlNode dominantChild : dominant.children()) {
296 Map<String, Optional<String>> dominantKeyValues = Stream.of(keys)
297 .collect(Collectors.toMap(
298 k -> k, k -> Optional.ofNullable(dominantChild.attribute(k))));
299
300 if (recessiveKeyValues.equals(dominantKeyValues)) {
301 childDom = dominantChild;
302
303 mergeChildren = true;
304 }
305 }
306 } else {
307 childDom = dominant.child(recessiveChild.name());
308 }
309
310 if (mergeChildren && childDom != null) {
311 String name = recessiveChild.name();
312 Iterator<XmlNode> it =
313 commonChildren.computeIfAbsent(name, n1 -> Stream.of(dominant.children().stream()
314 .filter(n2 -> n2.name().equals(n1))
315 .collect(Collectors.toList()))
316 .filter(l -> !l.isEmpty())
317 .map(List::iterator)
318 .findFirst()
319 .orElse(null));
320 if (it == null) {
321 if (children == null) {
322 children = new ArrayList<>(dominant.children());
323 }
324 children.add(recessiveChild);
325 } else if (it.hasNext()) {
326 XmlNode dominantChild = it.next();
327
328 String dominantChildCombinationMode = getSelfCombinationMode(dominantChild);
329 if (SELF_COMBINATION_REMOVE.equals(dominantChildCombinationMode)) {
330 if (children == null) {
331 children = new ArrayList<>(dominant.children());
332 }
333 children.remove(dominantChild);
334 } else {
335 int idx = dominant.children().indexOf(dominantChild);
336 XmlNode merged = merge(dominantChild, recessiveChild, childMergeOverride);
337 if (merged != dominantChild) {
338 if (children == null) {
339 children = new ArrayList<>(dominant.children());
340 }
341 children.set(idx, merged);
342 }
343 }
344 }
345 } else {
346 if (children == null) {
347 children = new ArrayList<>(dominant.children());
348 }
349 int idx = mergeChildren ? children.size() : recessiveChildIndex;
350 children.add(idx, recessiveChild);
351 }
352 recessiveChildIndex++;
353 }
354 }
355
356 if (value != null || attrs != dominant.attributes() || children != null) {
357 if (children == null) {
358 children = dominant.children();
359 }
360 if (!Objects.equals(value, dominant.value())
361 || !Objects.equals(attrs, dominant.attributes())
362 || !Objects.equals(children, dominant.children())
363 || !Objects.equals(location, dominant.inputLocation())) {
364 return XmlNode.newBuilder()
365 .prefix(dominant.prefix())
366 .namespaceUri(dominant.namespaceUri())
367 .name(dominant.name())
368 .value(value != null ? value : dominant.value())
369 .attributes(attrs)
370 .children(children)
371 .inputLocation(location)
372 .build();
373 } else {
374 return dominant;
375 }
376 }
377 }
378 return dominant;
379 }
380
381 private static boolean isEmpty(String str) {
382 return str == null || str.isEmpty();
383 }
384
385 private static String getSelfCombinationMode(XmlNode node) {
386 String value = node.attribute(SELF_COMBINATION_MODE_ATTRIBUTE);
387 return !isEmpty(value) ? value : DEFAULT_SELF_COMBINATION_MODE;
388 }
389
390 private static String getChildCombinationMode(Map<String, String> attributes) {
391 String value = attributes.get(CHILDREN_COMBINATION_MODE_ATTRIBUTE);
392 return !isEmpty(value) ? value : DEFAULT_CHILDREN_COMBINATION_MODE;
393 }
394
395 @Nullable
396 private static XmlNode findNodeById(@Nonnull List<XmlNode> nodes, @Nonnull String id) {
397 return nodes.stream()
398 .filter(n -> id.equals(n.attribute(ID_COMBINATION_MODE_ATTRIBUTE)))
399 .findFirst()
400 .orElse(null);
401 }
402
403 @Nullable
404 private static XmlNode findNodeByKeys(
405 @Nonnull List<XmlNode> nodes, @Nonnull XmlNode target, @Nonnull String[] keys) {
406 return nodes.stream()
407 .filter(n -> matchesKeys(n, target, keys))
408 .findFirst()
409 .orElse(null);
410 }
411
412 private static boolean matchesKeys(@Nonnull XmlNode node1, @Nonnull XmlNode node2, @Nonnull String[] keys) {
413 for (String key : keys) {
414 String value1 = node1.attribute(key);
415 String value2 = node2.attribute(key);
416 if (!Objects.equals(value1, value2)) {
417 return false;
418 }
419 }
420 return true;
421 }
422
423 static class IndentingXMLStreamWriter extends StreamWriterDelegate {
424
425 int depth = 0;
426 boolean hasChildren = false;
427 boolean anew = true;
428
429 IndentingXMLStreamWriter(XMLStreamWriter parent) {
430 super(parent);
431 }
432
433 @Override
434 public void writeStartDocument() throws XMLStreamException {
435 super.writeStartDocument();
436 anew = false;
437 }
438
439 @Override
440 public void writeStartDocument(String version) throws XMLStreamException {
441 super.writeStartDocument(version);
442 anew = false;
443 }
444
445 @Override
446 public void writeStartDocument(String encoding, String version) throws XMLStreamException {
447 super.writeStartDocument(encoding, version);
448 anew = false;
449 }
450
451 @Override
452 public void writeEmptyElement(String localName) throws XMLStreamException {
453 indent();
454 super.writeEmptyElement(localName);
455 hasChildren = true;
456 anew = false;
457 }
458
459 @Override
460 public void writeEmptyElement(String namespaceURI, String localName) throws XMLStreamException {
461 indent();
462 super.writeEmptyElement(namespaceURI, localName);
463 hasChildren = true;
464 anew = false;
465 }
466
467 @Override
468 public void writeEmptyElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
469 indent();
470 super.writeEmptyElement(prefix, localName, namespaceURI);
471 hasChildren = true;
472 anew = false;
473 }
474
475 @Override
476 public void writeStartElement(String localName) throws XMLStreamException {
477 indent();
478 super.writeStartElement(localName);
479 depth++;
480 hasChildren = false;
481 anew = false;
482 }
483
484 @Override
485 public void writeStartElement(String namespaceURI, String localName) throws XMLStreamException {
486 indent();
487 super.writeStartElement(namespaceURI, localName);
488 depth++;
489 hasChildren = false;
490 anew = false;
491 }
492
493 @Override
494 public void writeStartElement(String prefix, String localName, String namespaceURI) throws XMLStreamException {
495 indent();
496 super.writeStartElement(prefix, localName, namespaceURI);
497 depth++;
498 hasChildren = false;
499 anew = false;
500 }
501
502 @Override
503 public void writeEndElement() throws XMLStreamException {
504 depth--;
505 if (hasChildren) {
506 indent();
507 }
508 super.writeEndElement();
509 hasChildren = true;
510 anew = false;
511 }
512
513 private void indent() throws XMLStreamException {
514 if (!anew) {
515 super.writeCharacters("\n");
516 }
517 for (int i = 0; i < depth; i++) {
518 super.writeCharacters(" ");
519 }
520 }
521 }
522 }