View Javadoc
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 }