View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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      * Merges one DOM into another, given a specific algorithm and possible override points for that algorithm.<p>
183      * The algorithm is as follows:
184      * <ol>
185      * <li> if the recessive DOM is null, there is nothing to do... return.</li>
186      * <li> Determine whether the dominant node will suppress the recessive one (flag=mergeSelf).
187      *   <ol type="A">
188      *   <li> retrieve the 'combine.self' attribute on the dominant node, and try to match against 'override'...
189      *        if it matches 'override', then set mergeSelf == false...the dominant node suppresses the recessive one
190      *        completely.</li>
191      *   <li> otherwise, use the default value for mergeSelf, which is true...this is the same as specifying
192      *        'combine.self' == 'merge' as an attribute of the dominant root node.</li>
193      *   </ol></li>
194      * <li> If mergeSelf == true
195      *   <ol type="A">
196      *   <li> Determine whether children from the recessive DOM will be merged or appended to the dominant DOM as
197      *        siblings (flag=mergeChildren).
198      *     <ol type="i">
199      *     <li> if childMergeOverride is set (non-null), use that value (true/false)</li>
200      *     <li> retrieve the 'combine.children' attribute on the dominant node, and try to match against
201      *          'append'...</li>
202      *     <li> if it matches 'append', then set mergeChildren == false...the recessive children will be appended as
203      *          siblings of the dominant children.</li>
204      *     <li> otherwise, use the default value for mergeChildren, which is true...this is the same as specifying
205      *         'combine.children' == 'merge' as an attribute on the dominant root node.</li>
206      *     </ol></li>
207      *   <li> Iterate through the recessive children, and:
208      *     <ol type="i">
209      *     <li> if mergeChildren == true and there is a corresponding dominant child (matched by element name),
210      *          merge the two.</li>
211      *     <li> otherwise, add the recessive child as a new child on the dominant root node.</li>
212      *     </ol></li>
213      *   </ol></li>
214      * </ol>
215      */
216     @SuppressWarnings("checkstyle:MethodLength")
217     public XmlNode doMerge(XmlNode dominant, XmlNode recessive, Boolean childMergeOverride) {
218         // TODO: share this as some sort of assembler, implement a walk interface?
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                                 // we have a match, so don't append but merge
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                                 // we have a match, so don't append but merge
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 }