View Javadoc

1   /*
2    * Copyright 2012 smartics, Kronseder & Reiner GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package de.smartics.ci.config.utils;
17  
18  import java.io.ByteArrayOutputStream;
19  import java.util.Iterator;
20  
21  import org.apache.commons.lang.StringUtils;
22  import org.jdom.Content;
23  import org.jdom.Document;
24  import org.jdom.Element;
25  import org.jdom.JDOMException;
26  import org.jdom.output.XMLOutputter;
27  
28  import de.smartics.ci.config.hudson.HudsonConfigElement;
29  import de.smartics.util.lang.Arguments;
30  import de.smartics.util.lang.NullArgumentException;
31  
32  /**
33   * Helper functions to deal with JDom instances.
34   */
35  public final class JDomUtils
36  { // NOPMD
37    // ********************************* Fields *********************************
38  
39    // --- constants ------------------------------------------------------------
40  
41    // --- members --------------------------------------------------------------
42  
43    // ****************************** Initializer *******************************
44  
45    // ****************************** Constructors ******************************
46  
47    /**
48     * Utility class pattern.
49     */
50    private JDomUtils()
51    {
52    }
53  
54    // ****************************** Inner Classes *****************************
55  
56    /**
57     * Cursor to traverse XML elements for merging.
58     */
59    private static final class Cursor
60    {
61      /**
62       * The reference to the current parent element to add new elements to.
63       */
64      private Element parent; // NOPMD
65  
66      /**
67       * The iterator on the 'to be merged into target' structure.
68       */
69      private final Iterator<?> toBeMergedIterator;
70  
71      /**
72       * The root element of the target structure.
73       */
74      private final Element targetRootElement; // NOPMD
75  
76      /**
77       * The root element of the 'to be merged into target' structure.
78       */
79      private final Element rootElement;
80  
81      private Cursor(final Element target, final Element toBeMerged)
82      {
83        checkRootElementsMatch(target, toBeMerged);
84  
85        targetRootElement = target;
86        rootElement = toBeMerged;
87        parent = target;
88        toBeMergedIterator = toBeMerged.getChildren().iterator();
89      }
90  
91      private static void checkRootElementsMatch(final Element target,
92          final Element toBeMerged)
93      {
94        Arguments.checkNotNull("target", target);
95        Arguments.checkNotNull("toBeMerged", toBeMerged);
96        final String targetName = target.getName();
97        final String toBeMergedName = toBeMerged.getName();
98        if (!targetName.equals(toBeMergedName))
99        {
100         throw new IllegalArgumentException(
101             "The root element names of the XML subtrees to be merged do not"
102                 + " match. Target is '" + targetName + "' and toBeMerged is '"
103                 + toBeMergedName + "'.");
104       }
105     }
106 
107     private boolean hasNextToMerge()
108     {
109       return toBeMergedIterator.hasNext();
110     }
111 
112     private Content nextSiblingMergeContent()
113     {
114       return (Content) toBeMergedIterator.next();
115     }
116   }
117 
118   // ********************************* Methods ********************************
119 
120   // --- init -----------------------------------------------------------------
121 
122   // --- get&set --------------------------------------------------------------
123 
124   // --- business -------------------------------------------------------------
125 
126   /**
127    * Merges the distinct information from the two documents.
128    * <p>
129    * Note that not the contents of the element is checked to determine if the
130    * contents are equal. The merging algorithm simply checks the element names
131    * and assumes that two elements are the same, if they have the same name and
132    * are on the same level.
133    * </p>
134    *
135    * @param target the document to add information, if missing.
136    * @param toBeMerged the information to add to the {@code target}, if missing.
137    * @throws NullArgumentException if either {@code target} or
138    *           {@code toBeMerged} is <code>null</code>.
139    * @throws JDOMException on any problem encountered while merging.
140    */
141   public static void merge(final Document target, final Document toBeMerged)
142     throws NullArgumentException, JDOMException
143   {
144     Arguments.checkNotNull("target", target);
145     Arguments.checkNotNull("toBeMerged", toBeMerged);
146 
147     merge(target.getRootElement(), toBeMerged.getRootElement());
148   }
149 
150   /**
151    * Merges the distinct information from the two elements.
152    * <p>
153    * Note that not the contents of the element is checked to determine if the
154    * contents are equal. The merging algorithm simply checks the element names
155    * and assumes that two elements are the same, if they have the same name and
156    * are on the same level.
157    * </p>
158    *
159    * @param target the element to add information, if missing.
160    * @param toBeMerged the information to add to the {@code target}, if missing.
161    * @throws JDOMException on any problem encountered while merging.
162    */
163   public static void merge(final Element target, final Element toBeMerged)
164     throws JDOMException
165   {
166     final Cursor cursor = new Cursor(target, toBeMerged);
167 
168     if (target.getChildren().isEmpty())
169     {
170       if (cursor.hasNextToMerge())
171       {
172         fillWithMergeElements(cursor);
173         return;
174       }
175       else
176       {
177         overwriteTextContent(target, toBeMerged);
178       }
179     }
180 
181     while (cursor.hasNextToMerge())
182     {
183       final Content content = cursor.nextSiblingMergeContent();
184       if (content instanceof Element)
185       {
186         handleAsElement(cursor, content);
187       }
188     }
189   }
190 
191   private static void fillWithMergeElements(final Cursor cursor)
192   {
193     while (cursor.hasNextToMerge())
194     {
195       final Content content = cursor.nextSiblingMergeContent();
196       cursor.parent.addContent((Content) content.clone());
197     }
198   }
199 
200   private static void overwriteTextContent(final Element target,
201       final Element toBeMerged)
202   {
203     final String textContent = toBeMerged.getText();
204     if (StringUtils.isNotBlank(textContent))
205     {
206       target.setText(textContent);
207     }
208   }
209 
210   private static void handleAsElement(final Cursor cursor, final Content content)
211     throws JDOMException
212   {
213     final Element element = (Element) content;
214 
215     final Element targetElement = findElementInTarget(cursor, element);
216     final boolean exists = targetElement != null;
217     if (exists)
218     {
219       merge(targetElement, element);
220     }
221     else
222     {
223       cursor.parent.addContent((Content) element.clone());
224     }
225   }
226 
227   private static Element findElementInTarget(final Cursor cursor,
228       final Element element) throws JDOMException
229   {
230     final Element targetElement =
231         HudsonConfigElement.findElement(cursor.rootElement, element,
232             cursor.targetRootElement);
233     return targetElement;
234   }
235 
236   /**
237    * Calculates the XPath for the given {@code element}.
238    *
239    * @param rootElement the root element to the element whose path is to be
240    *          calculated. The returned XPath is relative to the element. May be
241    *          <code>null</code> if a path from the document root is requested.
242    * @param element the element relative to the {@code rootElement}.
243    * @return the XPath relative to {@code rootElement} to the {@code element}.
244    */
245   public static String calcXPath(final Element rootElement,
246       final Element element)
247   {
248     Arguments.checkNotNull("element", element);
249 
250     final StringBuilder buffer = new StringBuilder(32);
251 
252     buffer.append(element.getName());
253     Element current = element;
254     while ((current = current.getParentElement()) != rootElement) // NOPMD
255     {
256       buffer.insert(0, '/').insert(0, current.getName());
257     }
258 
259     if (rootElement == null)
260     {
261       buffer.insert(0, '/');
262     }
263     return buffer.toString();
264   }
265 
266   /**
267    * Returns the string representation of the XML document.
268    *
269    * @param document the XML document to create its string representation.
270    * @return the string representation of the XML document.
271    */
272   public static String toString(final Document document)
273   {
274     try
275     {
276       final ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
277 
278       final XMLOutputter outp = new XMLOutputter();
279       outp.output(document, out);
280       final String string = out.toString("UTF-8");
281       return string;
282     }
283     catch (final Exception e)
284     {
285       throw new IllegalStateException("Cannot stringify document.", e);
286     }
287   }
288 
289   // --- object basics --------------------------------------------------------
290 
291 }