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 }