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 }