1 /* 2 * This file is part of *** M y C o R e *** 3 * See http://www.mycore.de/ for details. 4 * 5 * MyCoRe is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * 10 * MyCoRe is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with MyCoRe. If not, see <http://www.gnu.org/licenses/>. 17 */ 18 19 package org.mycore.mods.merger; 20 21 import java.util.ArrayList; 22 import java.util.Collections; 23 import java.util.List; 24 import java.util.Objects; 25 26 import org.jdom2.Attribute; 27 import org.jdom2.Element; 28 import org.jdom2.Namespace; 29 import org.jdom2.filter.Filters; 30 import org.jdom2.xpath.XPathExpression; 31 import org.jdom2.xpath.XPathFactory; 32 import org.mycore.common.MCRConstants; 33 import org.mycore.common.xml.MCRXMLHelper; 34 35 /** 36 * MCRMerger is the main and default implementation for comparing 37 * and merging MODS elements that are semantically the same. 38 * Each MODS element is wrapped by an instance of MCRMerger or one of its subclasses. 39 * It contains methods to decide whether the two MODS elements are equal, or probably represent the information 40 * maybe with different granularity. 41 * If so, the text, elements and attributes are merged so that the "better" information/representation wins. 42 * This is done recursively for all child elements, too. 43 * 44 * @author Frank L\u00FCtzenkirchen 45 */ 46 public class MCRMerger { 47 48 /** Holds the MODS namespace */ 49 private static List<Namespace> NS = Collections.singletonList(MCRConstants.MODS_NAMESPACE); 50 51 /** The MODS element wrapped and compared by this merger */ 52 protected Element element; 53 54 /** Sets the MODS element wrapped and compared by this merger */ 55 public void setElement(Element element) { 56 Objects.requireNonNull(element); 57 this.element = element; 58 } 59 60 /** 61 * Returns true, if the element wrapped by this merger probably represents the same information as the other. 62 * The default implementation returns false and may be overwritten by subclasses implementing logic 63 * for specific MODS elements. 64 */ 65 public boolean isProbablySameAs(MCRMerger other) { 66 return false; 67 } 68 69 /** 70 * Two mergers are equal if they wrap elements that are deep equals. 71 */ 72 @Override 73 public boolean equals(Object obj) { 74 if (obj instanceof MCRMerger) { 75 return MCRXMLHelper.deepEqual(this.element, ((MCRMerger) obj).element); 76 } else { 77 return super.equals(obj); 78 } 79 } 80 81 /** 82 * Merges the contents of the element wrapped by the other merger into the contents of the element wrapped 83 * by this merger. 84 * Should only be called if this.isProbablySameAs(other). 85 * 86 * The default implementation copies all attributes from the other into this if they do not exist in this element. 87 * Afterwards it recursively builds mergers for all child elements and compares and eventually merges them too. 88 */ 89 public void mergeFrom(MCRMerger other) { 90 mergeAttributes(other); 91 mergeElements(other); 92 } 93 94 /** 95 * Copies those attributes from the other's element into this' element that do not exist in this' element. 96 */ 97 protected void mergeAttributes(MCRMerger other) { 98 for (Attribute attribute : other.element.getAttributes()) { 99 if (this.element.getAttribute(attribute.getName(), attribute.getNamespace()) == null) { 100 this.element.setAttribute(attribute.clone()); 101 } 102 } 103 } 104 105 /** 106 * Merges all child elements of this with that from other. 107 * This is done by building MCRMerger instances for each child element and comparing them. 108 */ 109 protected void mergeElements(MCRMerger other) { 110 List<MCRMerger> entries = new ArrayList<>(); 111 for (Element child : this.element.getChildren()) { 112 entries.add(MCRMergerFactory.buildFrom(child)); 113 } 114 115 for (Element child : other.element.getChildren()) { 116 mergeIntoExistingEntries(entries, MCRMergerFactory.buildFrom(child)); 117 } 118 } 119 120 /** 121 * Given a list of MCRMergers which represent the current content, merges a new entry into it. 122 * 123 * @param entries a list of existing mergers, each wrapping an element 124 * @param newEntry the merger for the new element that should be merged into the existing entries 125 */ 126 private void mergeIntoExistingEntries(List<MCRMerger> entries, MCRMerger newEntry) { 127 for (MCRMerger existingEntry : entries) { 128 if (newEntry.equals(existingEntry)) { 129 return; 130 } else if (newEntry.isProbablySameAs(existingEntry)) { 131 existingEntry.mergeFrom(newEntry); 132 return; 133 } 134 } 135 entries.add(newEntry); 136 element.addContent(newEntry.element.clone()); 137 } 138 139 /** 140 * Helper method to lookup child elements by XPath. 141 * 142 * @param xPath XPath expression relative to the element wrapped by this merger. 143 * @return a list of elements matching the given XPath 144 */ 145 protected List<Element> getNodes(String xPath) { 146 XPathExpression<Element> xPathExpr = XPathFactory.instance().compile(xPath, Filters.element(), null, NS); 147 return xPathExpr.evaluate(element); 148 } 149 }