001/* 002 * Licensed under the Apache License, Version 2.0 (the "License"); 003 * you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at 005 * 006 * http://www.apache.org/licenses/LICENSE-2.0 007 * 008 * Unless required by applicable law or agreed to in writing, software 009 * distributed under the License is distributed on an "AS IS" BASIS, 010 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 011 * See the License for the specific language governing permissions and 012 * limitations under the License. 013 */ 014package org.atteo.config.xmlmerge; 015 016import java.util.List; 017import java.util.Map.Entry; 018 019import javax.annotation.Nullable; 020import javax.xml.parsers.DocumentBuilder; 021import javax.xml.parsers.DocumentBuilderFactory; 022import javax.xml.parsers.ParserConfigurationException; 023 024import org.w3c.dom.Attr; 025import org.w3c.dom.Document; 026import org.w3c.dom.Element; 027import org.w3c.dom.NamedNodeMap; 028import org.w3c.dom.Node; 029import org.w3c.dom.NodeList; 030 031import com.google.common.collect.ListMultimap; 032 033/** 034 * Combines two or more XML DOM trees. 035 * 036 * <p> 037 * The merging algorithm is as follows:<br/> 038 * First direct subelements of selected node are examined. 039 * The elements from both trees with matching tag names and the value of 'id' attributes are paired. 040 * Based on selected behavior the content of the paired elements is then merged. 041 * Finally the paired elements are recursively combined. Any not paired elements are appended. 042 * </p> 043 * <p> 044 * You can control merging behavior using {@link CombineSelf 'combine.self'} 045 * and {@link CombineChildren 'combine.children'} attributes. 046 * </p> 047 * <p> 048 * The merging algorithm was inspired by similar functionality in Plexus Utils. 049 * </p> 050 * 051 * @see <a href="http://www.sonatype.com/people/2011/01/maven-how-to-merging-plugin-configuration-in-complex-projects/">merging in Maven</a> 052 * @see <a href="http://plexus.codehaus.org/plexus-utils/apidocs/org/codehaus/plexus/util/xml/Xpp3DomUtils.html">Plexus utils implementation of merging</a> 053 */ 054public class XmlCombiner { 055 056 private DocumentBuilder documentBuilder; 057 private Document document; 058 059 /** 060 * Creates XML combiner using default {@link DocumentBuilder}. 061 * @throws ParserConfigurationException when {@link DocumentBuilder} creation fails 062 */ 063 public XmlCombiner() throws ParserConfigurationException { 064 DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); 065 documentBuilder = factory.newDocumentBuilder(); 066 document = documentBuilder.newDocument(); 067 } 068 069 public XmlCombiner(DocumentBuilder documentBuilder) { 070 this.documentBuilder = documentBuilder; 071 document = documentBuilder.newDocument(); 072 } 073 074 /** 075 * Combine given document. 076 * @param document document to combine 077 */ 078 public void combine(Document document) { 079 combine(document.getDocumentElement()); 080 } 081 082 /** 083 * Combine given element. 084 * @param element element to combine 085 */ 086 public void combine(Element element) { 087 Element parent = document.getDocumentElement(); 088 if (parent != null) { 089 document.removeChild(parent); 090 } 091 Context result = combine(Context.fromElement(parent), Context.fromElement(element)); 092 result.addAsChildTo(document); 093 } 094 095 public Document buildDocument() { 096 filterOutDefaults(Context.fromElement(document.getDocumentElement())); 097 return document; 098 } 099 100 private Context combine(Context recessive, Context dominant) { 101 CombineSelf dominantCombineSelf = getCombineSelf(dominant.getElement()); 102 CombineSelf recessiveCombineSelf = getCombineSelf(recessive.getElement()); 103 104 if (dominantCombineSelf == CombineSelf.REMOVE) { 105 return null; 106 } else if (dominantCombineSelf == CombineSelf.OVERRIDE) { 107 Context result = copyRecursively(dominant); 108 result.getElement().removeAttribute(CombineSelf.ATTRIBUTE_NAME); 109 return result; 110 } 111 112 CombineChildren combineChildren = getCombineChildren(dominant.getElement()); 113 if (combineChildren == null) { 114 combineChildren = getCombineChildren(recessive.getElement()); 115 if (combineChildren == null) { 116 combineChildren = CombineChildren.MERGE; 117 } 118 } 119 120 if (combineChildren == CombineChildren.APPEND) { 121 if (recessive.getElement() != null) { 122 removeWhitespaceTail(recessive.getElement()); 123 appendRecursively(dominant, recessive); 124 return recessive; 125 } else { 126 return copyRecursively(dominant); 127 } 128 } 129 130 Element resultElement = document.createElement(dominant.getElement().getTagName()); 131 132 copyAttributes(recessive.getElement(), resultElement); 133 copyAttributes(dominant.getElement(), resultElement); 134 135 // when dominant combineSelf is null or DEFAULTS use combineSelf from recessive 136 CombineSelf combineSelf = dominantCombineSelf; 137 if ((combineSelf == null && recessiveCombineSelf != CombineSelf.DEFAULTS)) { 138 //|| (combineSelf == CombineSelf.DEFAULTS && recessive.getElement() != null)) { 139 combineSelf = recessiveCombineSelf; 140 } 141 if (combineSelf != null) { 142 resultElement.setAttribute(CombineSelf.ATTRIBUTE_NAME, combineSelf.name().toLowerCase()); 143 } else { 144 resultElement.removeAttribute(CombineSelf.ATTRIBUTE_NAME); 145 } 146 147 ListMultimap<Key, Context> recessiveContexts = recessive.mapChildContexts(); 148 ListMultimap<Key, Context> dominantContexts = dominant.mapChildContexts(); 149 150 // Execute only if there is at least one subelement in recessive 151 if (!recessiveContexts.isEmpty()) { 152 for (Entry<Key, Context> entry : recessiveContexts.entries()) { 153 Key key = entry.getKey(); 154 Context recessiveContext = entry.getValue(); 155 156 if (key == Key.BEFORE_END) { 157 continue; 158 } 159 160 if (dominantContexts.get(key).size() == 1 && recessiveContexts.get(key).size() == 1) { 161 Context dominantContext = dominantContexts.get(key).iterator().next(); 162 163 Context combined = combine(recessiveContext, dominantContext); 164 if (combined != null) { 165 combined.addAsChildTo(resultElement); 166 } 167 } else { 168 recessiveContext.addAsChildTo(resultElement); 169 } 170 } 171 } 172 173 for (Entry<Key, Context> entry : dominantContexts.entries()) { 174 Key key = entry.getKey(); 175 Context dominantContext = entry.getValue(); 176 177 if (key == Key.BEFORE_END) { 178 dominantContext.addAsChildTo(resultElement, document); 179 // break? this should be the last anyway... 180 continue; 181 } 182 183 if (dominantContexts.get(key).size() == 1 && recessiveContexts.get(key).size() == 1) { 184 // already added 185 } else { 186 Context combined = combine(Context.fromElement(null), dominantContext); 187 if (combined != null) { 188 combined.addAsChildTo(resultElement); 189 } 190 } 191 } 192 193 Context result = new Context(); 194 result.setElement(resultElement); 195 appendNeighbours(dominant, result); 196 197 return result; 198 } 199 200 /** 201 * Copy element recursively. 202 * @param context context to copy, it is assumed it is from unrelated document 203 * @return copied element in current document 204 */ 205 private Context copyRecursively(Context context) { 206 Context copy = new Context(); 207 208 appendNeighbours(context, copy); 209 210 Element element = (Element) document.importNode(context.getElement(), false); 211 copy.setElement(element); 212 213 appendRecursively(context, copy); 214 215 return copy; 216 } 217 218 /** 219 * Append neighbours from source to destination 220 * @param source source element, it is assumed it is from unrelated document 221 * @param destination destination element 222 */ 223 private void appendNeighbours(Context source, Context destination) { 224 for (Node neighbour : source.getNeighbours()) { 225 destination.addNeighbour(document.importNode(neighbour, true)); 226 } 227 } 228 229 /** 230 * Appends all attributes and subelements from source element do destination element. 231 * @param source source element, it is assumed it is from unrelated document 232 * @param destination destination element 233 */ 234 private void appendRecursively(Context source, Context destination) { 235 copyAttributes(source.getElement(), destination.getElement()); 236 237 List<Context> contexts = source.groupChildContexts(); 238 239 for (Context context : contexts) { 240 if (context.getElement() == null) { 241 context.addAsChildTo(destination.getElement(), document); 242 continue; 243 } 244 Context combined = combine(Context.fromElement(null), context); 245 if (combined != null) { 246 combined.addAsChildTo(destination.getElement()); 247 } 248 } 249 } 250 251 /** 252 * Copies attributes from one {@link Element} to the other. 253 * @param source source element 254 * @param destination destination element 255 */ 256 private void copyAttributes(@Nullable Element source, Element destination) { 257 if (source == null) { 258 return; 259 } 260 NamedNodeMap attributes = source.getAttributes(); 261 for (int i = 0; i < attributes.getLength(); i++) { 262 Attr attribute = (Attr) attributes.item(i); 263 Attr destAttribute = destination.getAttributeNodeNS(attribute.getNamespaceURI(), attribute.getName()); 264 265 if (destAttribute == null) { 266 destination.setAttributeNodeNS((Attr) document.importNode(attribute, true)); 267 } else { 268 destAttribute.setValue(attribute.getValue()); 269 } 270 } 271 } 272 273 private static CombineSelf getCombineSelf(@Nullable Element element) { 274 CombineSelf combine = null; 275 if (element == null) { 276 return null; 277 } 278 Attr combineAttribute = element.getAttributeNode(CombineSelf.ATTRIBUTE_NAME); 279 if (combineAttribute != null) { 280 try { 281 combine = CombineSelf.valueOf(combineAttribute.getValue().toUpperCase()); 282 } catch (IllegalArgumentException e) { 283 throw new RuntimeException("The attribute 'combine' of element '" 284 + element.getTagName() + "' has invalid value '" 285 + combineAttribute.getValue(), e); 286 } 287 } 288 return combine; 289 } 290 291 private static CombineChildren getCombineChildren(@Nullable Element element) { 292 CombineChildren combine = null; 293 if (element == null) { 294 return null; 295 } 296 Attr combineAttribute = element.getAttributeNode(CombineChildren.ATTRIBUTE_NAME); 297 if (combineAttribute != null) { 298 try { 299 combine = CombineChildren.valueOf(combineAttribute.getValue().toUpperCase()); 300 } catch (IllegalArgumentException e) { 301 throw new RuntimeException("The attribute 'combine' of element '" 302 + element.getTagName() + "' has invalid value '" 303 + combineAttribute.getValue(), e); 304 } 305 } 306 return combine; 307 } 308 309 private static void removeWhitespaceTail(Element element) { 310 NodeList list = element.getChildNodes(); 311 for (int i = list.getLength() - 1; i >= 0; i--) { 312 Node node = list.item(i); 313 if (node instanceof Element) { 314 break; 315 } 316 element.removeChild(node); 317 } 318 } 319 320 private static void filterOutDefaults(Context context) { 321 Element element = context.getElement(); 322 List<Context> childContexts = context.groupChildContexts(); 323 324 for (Context childContext : childContexts) { 325 if (childContext.getElement() == null) { 326 continue; 327 } 328 CombineSelf combineSelf = getCombineSelf(childContext.getElement()); 329 if (combineSelf == CombineSelf.DEFAULTS) { 330 for (Node neighbour : childContext.getNeighbours()) { 331 element.removeChild(neighbour); 332 } 333 element.removeChild(childContext.getElement()); 334 } else { 335 filterOutDefaults(childContext); 336 } 337 } 338 } 339}