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.api.xml;
20
21 import javax.xml.stream.XMLStreamException;
22 import javax.xml.stream.XMLStreamReader;
23
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.Reader;
27 import java.io.Writer;
28 import java.util.ServiceLoader;
29
30 import org.apache.maven.api.annotations.Nonnull;
31 import org.apache.maven.api.annotations.Nullable;
32
33 /**
34 * Comprehensive service interface for XML operations including node creation,
35 * merging, reading, and writing.
36 *
37 * <p>This class provides XML merging functionality for Maven's XML handling
38 * and specifies the combination modes that control how XML elements are merged.</p>
39 *
40 * <p>The merger supports two main types of combinations:</p>
41 * <ul>
42 * <li>Children combination: Controls how child elements are combined</li>
43 * <li>Self combination: Controls how the element itself is combined</li>
44 * </ul>
45 *
46 * <p>Children combination modes (specified by {@code combine.children} attribute):</p>
47 * <ul>
48 * <li>{@code merge} (default): Merges elements with matching names</li>
49 * <li>{@code append}: Adds elements as siblings</li>
50 * </ul>
51 *
52 * <p>Self combination modes (specified by {@code combine.self} attribute):</p>
53 * <ul>
54 * <li>{@code merge} (default): Merges attributes and values</li>
55 * <li>{@code override}: Completely replaces the element</li>
56 * <li>{@code remove}: Removes the element</li>
57 * </ul>
58 *
59 * <p>For complex XML structures, combining can also be done based on:</p>
60 * <ul>
61 * <li>ID: Using the {@code combine.id} attribute</li>
62 * <li>Keys: Using the {@code combine.keys} attribute with comma-separated key names</li>
63 * </ul>
64 *
65 * @since 4.0.0
66 */
67 public abstract class XmlService {
68
69 /** Attribute name controlling how child elements are combined */
70 public static final String CHILDREN_COMBINATION_MODE_ATTRIBUTE = "combine.children";
71 /** Value indicating children should be merged based on element names */
72 public static final String CHILDREN_COMBINATION_MERGE = "merge";
73 /** Value indicating children should be appended as siblings */
74 public static final String CHILDREN_COMBINATION_APPEND = "append";
75 /**
76 * Default mode for combining children DOMs during merge.
77 * When element names match, the process will try to merge the element data,
78 * rather than putting the dominant and recessive elements as siblings.
79 */
80 public static final String DEFAULT_CHILDREN_COMBINATION_MODE = CHILDREN_COMBINATION_MERGE;
81
82 /** Attribute name controlling how the element itself is combined */
83 public static final String SELF_COMBINATION_MODE_ATTRIBUTE = "combine.self";
84 /** Value indicating the element should be completely overridden */
85 public static final String SELF_COMBINATION_OVERRIDE = "override";
86 /** Value indicating the element should be merged */
87 public static final String SELF_COMBINATION_MERGE = "merge";
88 /** Value indicating the element should be removed */
89 public static final String SELF_COMBINATION_REMOVE = "remove";
90 /**
91 * Default mode for combining a DOM node during merge.
92 * When element names match, the process will try to merge element attributes
93 * and values, rather than overriding the recessive element completely.
94 */
95 public static final String DEFAULT_SELF_COMBINATION_MODE = SELF_COMBINATION_MERGE;
96
97 /** Attribute name for ID-based combination mode */
98 public static final String ID_COMBINATION_MODE_ATTRIBUTE = "combine.id";
99 /**
100 * Attribute name for key-based combination mode.
101 * Value should be a comma-separated list of attribute names.
102 */
103 public static final String KEYS_COMBINATION_MODE_ATTRIBUTE = "combine.keys";
104
105 /**
106 * Convenience method to merge two XML nodes using default settings.
107 */
108 @Nullable
109 public static XmlNode merge(XmlNode dominant, XmlNode recessive) {
110 return merge(dominant, recessive, null);
111 }
112
113 /**
114 * Merges two XML nodes.
115 */
116 @Nullable
117 public static XmlNode merge(
118 @Nullable XmlNode dominant, @Nullable XmlNode recessive, @Nullable Boolean childMergeOverride) {
119 return getService().doMerge(dominant, recessive, childMergeOverride);
120 }
121
122 /**
123 * Reads an XML node from an input stream.
124 */
125 @Nonnull
126 public static XmlNode read(InputStream input, @Nullable InputLocationBuilder locationBuilder)
127 throws XMLStreamException {
128 return getService().doRead(input, locationBuilder);
129 }
130
131 /**
132 * Reads an XML node from a reader.
133 */
134 @Nonnull
135 public static XmlNode read(Reader reader) throws XMLStreamException {
136 return read(reader, null);
137 }
138
139 /**
140 * Reads an XML node from a reader.
141 */
142 @Nonnull
143 public static XmlNode read(Reader reader, @Nullable InputLocationBuilder locationBuilder)
144 throws XMLStreamException {
145 return getService().doRead(reader, locationBuilder);
146 }
147
148 /**
149 * Reads an XML node from an XMLStreamReader.
150 */
151 @Nonnull
152 public static XmlNode read(XMLStreamReader reader) throws XMLStreamException {
153 return read(reader, null);
154 }
155
156 /**
157 * Reads an XML node from an XMLStreamReader.
158 */
159 @Nonnull
160 public static XmlNode read(XMLStreamReader reader, @Nullable InputLocationBuilder locationBuilder)
161 throws XMLStreamException {
162 return getService().doRead(reader, locationBuilder);
163 }
164
165 /**
166 * Writes an XML node to a writer.
167 */
168 public static void write(XmlNode node, Writer writer) throws IOException {
169 getService().doWrite(node, writer);
170 }
171
172 /**
173 * Interface for building input locations during XML parsing.
174 */
175 public interface InputLocationBuilder {
176 Object toInputLocation(XMLStreamReader parser);
177 }
178
179 /**
180 * Implementation method for reading an XML node from an input stream.
181 *
182 * @param input the input stream to read from
183 * @param locationBuilder optional builder for creating input location objects
184 * @return the parsed XML node
185 * @throws XMLStreamException if there is an error parsing the XML
186 */
187 protected abstract XmlNode doRead(InputStream input, InputLocationBuilder locationBuilder)
188 throws XMLStreamException;
189
190 /**
191 * Implementation method for reading an XML node from a reader.
192 *
193 * @param reader the reader to read from
194 * @param locationBuilder optional builder for creating input location objects
195 * @return the parsed XML node
196 * @throws XMLStreamException if there is an error parsing the XML
197 */
198 protected abstract XmlNode doRead(Reader reader, InputLocationBuilder locationBuilder) throws XMLStreamException;
199
200 /**
201 * Implementation method for reading an XML node from an XMLStreamReader.
202 *
203 * @param reader the XML stream reader to read from
204 * @param locationBuilder optional builder for creating input location objects
205 * @return the parsed XML node
206 * @throws XMLStreamException if there is an error parsing the XML
207 */
208 protected abstract XmlNode doRead(XMLStreamReader reader, InputLocationBuilder locationBuilder)
209 throws XMLStreamException;
210
211 /**
212 * Implementation method for writing an XML node to a writer.
213 *
214 * @param node the XML node to write
215 * @param writer the writer to write to
216 * @throws IOException if there is an error writing the XML
217 */
218 protected abstract void doWrite(XmlNode node, Writer writer) throws IOException;
219
220 /**
221 * Implementation method for merging two XML nodes.
222 *
223 * @param dominant the dominant (higher priority) XML node
224 * @param recessive the recessive (lower priority) XML node
225 * @param childMergeOverride optional override for the child merge mode
226 * @return the merged XML node, or null if both inputs are null
227 */
228 protected abstract XmlNode doMerge(XmlNode dominant, XmlNode recessive, Boolean childMergeOverride);
229
230 /**
231 * Gets the singleton instance of the XmlService.
232 *
233 * @return the XmlService instance
234 * @throws IllegalStateException if no implementation is found
235 */
236 private static XmlService getService() {
237 return Holder.INSTANCE;
238 }
239
240 /** Holder class for lazy initialization of the default instance */
241 private static final class Holder {
242 static final XmlService INSTANCE = ServiceLoader.load(XmlService.class)
243 .findFirst()
244 .orElseThrow(() -> new IllegalStateException("No XmlService implementation found"));
245
246 private Holder() {}
247 }
248 }