replaced <code></code> with more concise {@code}
[idea/community.git] / platform / util / src / com / intellij / openapi / util / JDOMUtil.java
1 /*
2  * Copyright 2000-2017 JetBrains s.r.o.
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 com.intellij.openapi.util;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.io.FileUtil;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.vfs.CharsetToolkit;
22 import com.intellij.util.ArrayUtil;
23 import com.intellij.util.containers.ContainerUtil;
24 import com.intellij.util.containers.StringInterner;
25 import com.intellij.util.io.URLUtil;
26 import com.intellij.util.text.CharArrayUtil;
27 import com.intellij.util.text.CharSequenceReader;
28 import com.intellij.util.text.StringFactory;
29 import org.jdom.*;
30 import org.jdom.filter.Filter;
31 import org.jdom.input.SAXBuilder;
32 import org.jdom.input.SAXHandler;
33 import org.jdom.output.Format;
34 import org.jdom.output.XMLOutputter;
35 import org.jetbrains.annotations.Contract;
36 import org.jetbrains.annotations.NotNull;
37 import org.jetbrains.annotations.Nullable;
38 import org.xml.sax.EntityResolver;
39 import org.xml.sax.InputSource;
40 import org.xml.sax.XMLReader;
41
42 import javax.xml.XMLConstants;
43 import java.io.*;
44 import java.lang.ref.SoftReference;
45 import java.net.URL;
46 import java.util.ArrayList;
47 import java.util.Collections;
48 import java.util.Iterator;
49 import java.util.List;
50
51 /**
52  * @author mike
53  */
54 @SuppressWarnings({"HardCodedStringLiteral"})
55 public class JDOMUtil {
56   private static final ThreadLocal<SoftReference<SAXBuilder>> ourSaxBuilder = new ThreadLocal<SoftReference<SAXBuilder>>();
57   public static final Condition<Attribute> NOT_EMPTY_VALUE_CONDITION = new Condition<Attribute>() {
58     @Override
59     public boolean value(Attribute attribute) {
60       return !StringUtil.isEmpty(attribute.getValue());
61     }
62   };
63
64   private JDOMUtil() { }
65
66   @NotNull
67   public static List<Element> getChildren(@Nullable Element parent) {
68     if (parent == null) {
69       return Collections.emptyList();
70     }
71     else {
72       return parent.getChildren();
73     }
74   }
75
76   @NotNull
77   public static List<Element> getChildren(@Nullable Element parent, @NotNull String name) {
78     if (parent != null) {
79       return parent.getChildren(name);
80     }
81     return Collections.emptyList();
82   }
83
84   @SuppressWarnings("UtilityClassWithoutPrivateConstructor")
85   private static class LoggerHolder {
86     private static final Logger ourLogger = Logger.getInstance("#com.intellij.openapi.util.JDOMUtil");
87   }
88
89   private static Logger getLogger() {
90     return LoggerHolder.ourLogger;
91   }
92
93   public static boolean areElementsEqual(@Nullable Element e1, @Nullable Element e2) {
94     return areElementsEqual(e1, e2, false);
95   }
96
97   /**
98    *
99    * @param ignoreEmptyAttrValues defines if elements like <element foo="bar" skip_it=""/> and <element foo="bar"/> are 'equal'
100    * @return {@code true} if two elements are deep-equals by their content and attributes
101    */
102   public static boolean areElementsEqual(@Nullable Element e1, @Nullable Element e2, boolean ignoreEmptyAttrValues) {
103     if (e1 == null && e2 == null) return true;
104     if (e1 == null || e2 == null) return false;
105
106     return Comparing.equal(e1.getName(), e2.getName())
107            && attListsEqual(e1.getAttributes(), e2.getAttributes(), ignoreEmptyAttrValues)
108            && contentListsEqual(e1.getContent(CONTENT_FILTER), e2.getContent(CONTENT_FILTER), ignoreEmptyAttrValues);
109   }
110
111   private static final EmptyTextFilter CONTENT_FILTER = new EmptyTextFilter();
112
113   public static int getTreeHash(@NotNull Element root) {
114     return addToHash(0, root, true);
115   }
116
117   private static int addToHash(int i, @NotNull Element element, boolean skipEmptyText) {
118     i = addToHash(i, element.getName());
119
120     for (Attribute attribute : element.getAttributes()) {
121       i = addToHash(i, attribute.getName());
122       i = addToHash(i, attribute.getValue());
123     }
124
125     for (Content child : element.getContent()) {
126       if (child instanceof Element) {
127         i = addToHash(i, (Element)child, skipEmptyText);
128       }
129       else if (child instanceof Text) {
130         String text = ((Text)child).getText();
131         if (!skipEmptyText || !StringUtil.isEmptyOrSpaces(text)) {
132           i = addToHash(i, text);
133         }
134       }
135     }
136     return i;
137   }
138
139   private static int addToHash(int i, @NotNull String s) {
140     return i * 31 + s.hashCode();
141   }
142
143   /**
144    * @deprecated Use Element.getChildren() directly
145    */
146   @NotNull
147   @Deprecated
148   public static Element[] getElements(@NotNull Element m) {
149     List<Element> list = m.getChildren();
150     return list.toArray(new Element[list.size()]);
151   }
152
153   public static void internElement(@NotNull Element element, @NotNull StringInterner interner) {
154     element.setName(interner.intern(element.getName()));
155
156     for (Attribute attr : element.getAttributes()) {
157       attr.setName(interner.intern(attr.getName()));
158       attr.setValue(interner.intern(attr.getValue()));
159     }
160
161     for (Content o : element.getContent()) {
162       if (o instanceof Element) {
163         internElement((Element)o, interner);
164       }
165       else if (o instanceof Text) {
166         ((Text)o).setText(interner.intern(o.getValue()));
167       }
168     }
169   }
170
171   @NotNull
172   public static String legalizeText(@NotNull String str) {
173     return legalizeChars(str).toString();
174   }
175
176   @NotNull
177   public static CharSequence legalizeChars(@NotNull CharSequence str) {
178     StringBuilder result = new StringBuilder(str.length());
179     for (int i = 0, len = str.length(); i < len; i ++) {
180       appendLegalized(result, str.charAt(i));
181     }
182     return result;
183   }
184
185   private static void appendLegalized(@NotNull StringBuilder sb, char each) {
186     if (each == '<' || each == '>') {
187       sb.append(each == '<' ? "&lt;" : "&gt;");
188     }
189     else if (!Verifier.isXMLCharacter(each)) {
190       sb.append("0x").append(StringUtil.toUpperCase(Long.toHexString(each)));
191     }
192     else {
193       sb.append(each);
194     }
195   }
196
197   private static class EmptyTextFilter implements Filter {
198     @Override
199     public boolean matches(Object obj) {
200       return !(obj instanceof Text) || !CharArrayUtil.containsOnlyWhiteSpaces(((Text)obj).getText());
201     }
202   }
203
204   private static boolean contentListsEqual(final List c1, final List c2, boolean ignoreEmptyAttrValues) {
205     if (c1 == null && c2 == null) return true;
206     if (c1 == null || c2 == null) return false;
207
208     Iterator l1 = c1.listIterator();
209     Iterator l2 = c2.listIterator();
210     while (l1.hasNext() && l2.hasNext()) {
211       if (!contentsEqual((Content)l1.next(), (Content)l2.next(), ignoreEmptyAttrValues)) {
212         return false;
213       }
214     }
215
216     return l1.hasNext() == l2.hasNext();
217   }
218
219   private static boolean contentsEqual(Content c1, Content c2, boolean ignoreEmptyAttrValues) {
220     if (!(c1 instanceof Element) && !(c2 instanceof Element)) {
221       return c1.getValue().equals(c2.getValue());
222     }
223
224     return c1 instanceof Element && c2 instanceof Element && areElementsEqual((Element)c1, (Element)c2, ignoreEmptyAttrValues);
225   }
226
227   private static boolean attListsEqual(@NotNull List<Attribute> l1, @NotNull List<Attribute> l2, boolean ignoreEmptyAttrValues) {
228     if (ignoreEmptyAttrValues) {
229       l1 = ContainerUtil.filter(l1, NOT_EMPTY_VALUE_CONDITION);
230       l2 = ContainerUtil.filter(l2, NOT_EMPTY_VALUE_CONDITION);
231     }
232     if (l1.size() != l2.size()) return false;
233     for (int i = 0; i < l1.size(); i++) {
234       if (!attEqual(l1.get(i), l2.get(i))) return false;
235     }
236     return true;
237   }
238
239   private static boolean attEqual(@NotNull Attribute a1, @NotNull Attribute a2) {
240     return a1.getName().equals(a2.getName()) && a1.getValue().equals(a2.getValue());
241   }
242
243   private static SAXBuilder getSaxBuilder() {
244     SoftReference<SAXBuilder> reference = ourSaxBuilder.get();
245     SAXBuilder saxBuilder = com.intellij.reference.SoftReference.dereference(reference);
246     if (saxBuilder == null) {
247       saxBuilder = new SAXBuilder() {
248         @Override
249         protected void configureParser(XMLReader parser, SAXHandler contentHandler) throws JDOMException {
250           super.configureParser(parser, contentHandler);
251           try {
252             parser.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
253           }
254           catch (Exception ignore) {
255           }
256         }
257       };
258       saxBuilder.setEntityResolver(new EntityResolver() {
259         @Override
260         @NotNull
261         public InputSource resolveEntity(String publicId, String systemId) {
262           return new InputSource(new CharArrayReader(ArrayUtil.EMPTY_CHAR_ARRAY));
263         }
264       });
265       ourSaxBuilder.set(new SoftReference<SAXBuilder>(saxBuilder));
266     }
267     return saxBuilder;
268   }
269
270   /**
271    * @deprecated Use {@link #load(CharSequence)}
272    *
273    * Direct usage of element allows to get rid of {@link Document#getRootElement()} because only Element is required in mostly all cases.
274    */
275   @NotNull
276   @Deprecated
277   public static Document loadDocument(@NotNull CharSequence seq) throws IOException, JDOMException {
278     return loadDocument(new CharSequenceReader(seq));
279   }
280
281   public static Element load(@NotNull CharSequence seq) throws IOException, JDOMException {
282     return load(new CharSequenceReader(seq));
283   }
284
285   @NotNull
286   private static Document loadDocument(@NotNull Reader reader) throws IOException, JDOMException {
287     try {
288       return getSaxBuilder().build(reader);
289     }
290     finally {
291       reader.close();
292     }
293   }
294
295   @NotNull
296   public static Document loadDocument(File file) throws JDOMException, IOException {
297     return loadDocument(new BufferedInputStream(new FileInputStream(file)));
298   }
299
300   @NotNull
301   public static Element load(@NotNull File file) throws JDOMException, IOException {
302     return load(new BufferedInputStream(new FileInputStream(file)));
303   }
304
305   @NotNull
306   public static Document loadDocument(@NotNull InputStream stream) throws JDOMException, IOException {
307     return loadDocument(new InputStreamReader(stream, CharsetToolkit.UTF8_CHARSET));
308   }
309
310   public static Element load(Reader reader) throws JDOMException, IOException {
311     return reader == null ? null : loadDocument(reader).detachRootElement();
312   }
313
314   /**
315    * Consider to use `loadElement` (JdomKt.loadElement from java) due to more efficient whitespace handling (cannot be changed here due to backward compatibility).
316    */
317   @Contract("null -> null; !null -> !null")
318   public static Element load(InputStream stream) throws JDOMException, IOException {
319     return stream == null ? null : loadDocument(stream).detachRootElement();
320   }
321
322   @NotNull
323   public static Document loadDocument(@NotNull Class clazz, String resource) throws JDOMException, IOException {
324     InputStream stream = clazz.getResourceAsStream(resource);
325     if (stream == null) {
326       throw new FileNotFoundException(resource);
327     }
328     return loadDocument(stream);
329   }
330
331   @NotNull
332   public static Document loadDocument(@NotNull URL url) throws JDOMException, IOException {
333     return loadDocument(URLUtil.openStream(url));
334   }
335
336   @NotNull
337   public static Document loadResourceDocument(URL url) throws JDOMException, IOException {
338     return loadDocument(URLUtil.openResourceStream(url));
339   }
340
341   public static void writeDocument(@NotNull Document document, @NotNull String filePath, String lineSeparator) throws IOException {
342     OutputStream stream = new BufferedOutputStream(new FileOutputStream(filePath));
343     try {
344       writeDocument(document, stream, lineSeparator);
345     }
346     finally {
347       stream.close();
348     }
349   }
350
351   public static void writeDocument(@NotNull Document document, @NotNull File file, String lineSeparator) throws IOException {
352     write(document, file, lineSeparator);
353   }
354
355   public static void write(@NotNull Parent element, @NotNull File file) throws IOException {
356     write(element, file, "\n");
357   }
358
359   public static void write(@NotNull Parent element, @NotNull File file, @NotNull String lineSeparator) throws IOException {
360     FileUtil.createParentDirs(file);
361
362     OutputStream stream = new BufferedOutputStream(new FileOutputStream(file));
363     try {
364       write(element, stream, lineSeparator);
365     }
366     finally {
367       stream.close();
368     }
369   }
370
371   public static void writeDocument(@NotNull Document document, @NotNull OutputStream stream, String lineSeparator) throws IOException {
372     write(document, stream, lineSeparator);
373   }
374
375   public static void write(@NotNull Parent element, @NotNull OutputStream stream, @NotNull String lineSeparator) throws IOException {
376     OutputStreamWriter writer = new OutputStreamWriter(stream, CharsetToolkit.UTF8_CHARSET);
377     try {
378       if (element instanceof Document) {
379         writeDocument((Document)element, writer, lineSeparator);
380       }
381       else {
382         writeElement((Element) element, writer, lineSeparator);
383       }
384     }
385     finally {
386       writer.close();
387     }
388   }
389
390   /**
391    * @deprecated Use {@link #writeDocument(Document, String)} or {@link #writeElement(Element)}}
392    */
393   @NotNull
394   @Deprecated
395   public static byte[] printDocument(@NotNull Document document, String lineSeparator) throws IOException {
396     CharArrayWriter writer = new CharArrayWriter();
397     writeDocument(document, writer, lineSeparator);
398
399     return StringFactory.createShared(writer.toCharArray()).getBytes(CharsetToolkit.UTF8_CHARSET);
400   }
401
402   @NotNull
403   public static String writeDocument(@NotNull Document document, String lineSeparator) {
404     try {
405       final StringWriter writer = new StringWriter();
406       writeDocument(document, writer, lineSeparator);
407       return writer.toString();
408     }
409     catch (IOException ignored) {
410       // Can't be
411       return "";
412     }
413   }
414
415   @NotNull
416   public static String write(Parent element, String lineSeparator) {
417     try {
418       final StringWriter writer = new StringWriter();
419       write(element, writer, lineSeparator);
420       return writer.toString();
421     }
422     catch (IOException e) {
423       throw new RuntimeException(e);
424     }
425   }
426
427   public static void write(Parent element, Writer writer, String lineSeparator) throws IOException {
428     if (element instanceof Element) {
429       writeElement((Element) element, writer, lineSeparator);
430     } else if (element instanceof Document) {
431       writeDocument((Document) element, writer, lineSeparator);
432     }
433   }
434
435   public static void writeElement(@NotNull Element element, Writer writer, String lineSeparator) throws IOException {
436     writeElement(element, writer, createOutputter(lineSeparator));
437   }
438
439   public static void writeElement(@NotNull Element element, @NotNull Writer writer, @NotNull XMLOutputter xmlOutputter) throws IOException {
440     try {
441       xmlOutputter.output(element, writer);
442     }
443     catch (NullPointerException ex) {
444       getLogger().error(ex);
445       printDiagnostics(element, "");
446     }
447   }
448
449   @NotNull
450   public static String writeElement(@NotNull Element element) {
451     return writeElement(element, "\n");
452   }
453
454   @NotNull
455   public static String writeElement(@NotNull Element element, String lineSeparator) {
456     try {
457       final StringWriter writer = new StringWriter();
458       writeElement(element, writer, lineSeparator);
459       return writer.toString();
460     }
461     catch (IOException e) {
462       throw new RuntimeException(e);
463     }
464   }
465
466   @NotNull
467   public static String writeChildren(@NotNull final Element element, @NotNull final String lineSeparator) throws IOException {
468     final StringWriter writer = new StringWriter();
469     for (Element child : element.getChildren()) {
470       writeElement(child, writer, lineSeparator);
471       writer.append(lineSeparator);
472     }
473     return writer.toString();
474   }
475
476   public static void writeDocument(@NotNull Document document, @NotNull Writer writer, String lineSeparator) throws IOException {
477     XMLOutputter xmlOutputter = createOutputter(lineSeparator);
478     try {
479       xmlOutputter.output(document, writer);
480     }
481     catch (NullPointerException ex) {
482       getLogger().error(ex);
483       printDiagnostics(document.getRootElement(), "");
484     }
485   }
486
487   @NotNull
488   public static XMLOutputter createOutputter(String lineSeparator) {
489     return createOutputter(lineSeparator, null);
490   }
491
492   @NotNull
493   public static XMLOutputter createOutputter(String lineSeparator, @Nullable ElementOutputFilter elementOutputFilter) {
494     XMLOutputter xmlOutputter = new MyXMLOutputter(elementOutputFilter);
495     Format format = Format.getCompactFormat().
496       setIndent("  ").
497       setTextMode(Format.TextMode.TRIM).
498       setEncoding(CharsetToolkit.UTF8).
499       setOmitEncoding(false).
500       setOmitDeclaration(false).
501       setLineSeparator(lineSeparator);
502     xmlOutputter.setFormat(format);
503     return xmlOutputter;
504   }
505
506   /**
507    * Returns null if no escapement necessary.
508    */
509   @Nullable
510   private static String escapeChar(char c, boolean escapeApostrophes, boolean escapeSpaces, boolean escapeLineEnds) {
511     switch (c) {
512       case '\n': return escapeLineEnds ? "&#10;" : null;
513       case '\r': return escapeLineEnds ? "&#13;" : null;
514       case '\t': return escapeLineEnds ? "&#9;" : null;
515       case ' ' : return escapeSpaces  ? "&#20" : null;
516       case '<':  return "&lt;";
517       case '>':  return "&gt;";
518       case '\"': return "&quot;";
519       case '\'': return escapeApostrophes ? "&apos;": null;
520       case '&':  return "&amp;";
521     }
522     return null;
523   }
524
525   @NotNull
526   public static String escapeText(@NotNull String text) {
527     return escapeText(text, false, false);
528   }
529
530   @NotNull
531   public static String escapeText(@NotNull String text, boolean escapeSpaces, boolean escapeLineEnds) {
532     return escapeText(text, false, escapeSpaces, escapeLineEnds);
533   }
534
535   @NotNull
536   public static String escapeText(@NotNull String text, boolean escapeApostrophes, boolean escapeSpaces, boolean escapeLineEnds) {
537     StringBuilder buffer = null;
538     for (int i = 0; i < text.length(); i++) {
539       final char ch = text.charAt(i);
540       final String quotation = escapeChar(ch, escapeApostrophes, escapeSpaces, escapeLineEnds);
541       if (buffer == null) {
542         if (quotation != null) {
543           // An quotation occurred, so we'll have to use StringBuffer
544           // (allocate room for it plus a few more entities).
545           buffer = new StringBuilder(text.length() + 20);
546           // Copy previous skipped characters and fall through
547           // to pickup current character
548           buffer.append(text, 0, i);
549           buffer.append(quotation);
550         }
551       }
552       else if (quotation == null) {
553         buffer.append(ch);
554       }
555       else {
556         buffer.append(quotation);
557       }
558     }
559     // If there were any entities, return the escaped characters
560     // that we put in the StringBuffer. Otherwise, just return
561     // the unmodified input string.
562     return buffer == null ? text : buffer.toString();
563   }
564
565   public static class MyXMLOutputter extends XMLOutputter {
566     private final ElementOutputFilter myElementOutputFilter;
567
568     public MyXMLOutputter(@Nullable ElementOutputFilter filter) {
569       myElementOutputFilter = filter;
570     }
571
572     public MyXMLOutputter() {
573       this(null);
574     }
575
576     @Override
577     @NotNull
578     public String escapeAttributeEntities(@NotNull String str) {
579       return escapeText(str, false, true);
580     }
581
582     @Override
583     @NotNull
584     public String escapeElementEntities(@NotNull String str) {
585       return escapeText(str, false, false);
586     }
587
588     @Override
589     protected void printElement(Writer out, Element element, int level, NamespaceStack namespaces) throws IOException {
590       if (myElementOutputFilter == null || myElementOutputFilter.accept(element, level)) {
591         super.printElement(out, element, level, namespaces);
592       }
593     }
594   }
595
596   public interface ElementOutputFilter {
597     boolean accept(@NotNull Element element, int level);
598   }
599
600   private static void printDiagnostics(@NotNull Element element, String prefix) {
601     ElementInfo info = getElementInfo(element);
602     prefix += "/" + info.name;
603     if (info.hasNullAttributes) {
604       //noinspection UseOfSystemOutOrSystemErr
605       System.err.println(prefix);
606     }
607
608     for (final Element child : element.getChildren()) {
609       printDiagnostics(child, prefix);
610     }
611   }
612
613   @NotNull
614   private static ElementInfo getElementInfo(@NotNull Element element) {
615     ElementInfo info = new ElementInfo();
616     StringBuilder buf = new StringBuilder(element.getName());
617     List attributes = element.getAttributes();
618     if (attributes != null) {
619       int length = attributes.size();
620       if (length > 0) {
621         buf.append("[");
622         for (int idx = 0; idx < length; idx++) {
623           Attribute attr = (Attribute)attributes.get(idx);
624           if (idx != 0) {
625             buf.append(";");
626           }
627           buf.append(attr.getName());
628           buf.append("=");
629           buf.append(attr.getValue());
630           if (attr.getValue() == null) {
631             info.hasNullAttributes = true;
632           }
633         }
634         buf.append("]");
635       }
636     }
637     info.name = buf.toString();
638     return info;
639   }
640
641   public static void updateFileSet(@NotNull File[] oldFiles, @NotNull String[] newFilePaths, @NotNull Document[] newFileDocuments, String lineSeparator)
642     throws IOException {
643     getLogger().assertTrue(newFilePaths.length == newFileDocuments.length);
644
645     ArrayList<String> writtenFilesPaths = new ArrayList<String>();
646
647     // check if files are writable
648     for (String newFilePath : newFilePaths) {
649       File file = new File(newFilePath);
650       if (file.exists() && !file.canWrite()) {
651         throw new IOException("File \"" + newFilePath + "\" is not writeable");
652       }
653     }
654     for (File file : oldFiles) {
655       if (file.exists() && !file.canWrite()) {
656         throw new IOException("File \"" + file.getAbsolutePath() + "\" is not writeable");
657       }
658     }
659
660     for (int i = 0; i < newFilePaths.length; i++) {
661       String newFilePath = newFilePaths[i];
662
663       writeDocument(newFileDocuments[i], newFilePath, lineSeparator);
664       writtenFilesPaths.add(newFilePath);
665     }
666
667     // delete files if necessary
668
669     outer:
670     for (File oldFile : oldFiles) {
671       String oldFilePath = oldFile.getAbsolutePath();
672       for (final String writtenFilesPath : writtenFilesPaths) {
673         if (oldFilePath.equals(writtenFilesPath)) {
674           continue outer;
675         }
676       }
677       boolean result = oldFile.delete();
678       if (!result) {
679         throw new IOException("File \"" + oldFilePath + "\" was not deleted");
680       }
681     }
682   }
683
684   private static class ElementInfo {
685     @NotNull public String name = "";
686     public boolean hasNullAttributes = false;
687   }
688
689   public static String getValue(Object node) {
690     if (node instanceof Content) {
691       Content content = (Content)node;
692       return content.getValue();
693     }
694     else if (node instanceof Attribute) {
695       Attribute attribute = (Attribute)node;
696       return attribute.getValue();
697     }
698     else {
699       throw new IllegalArgumentException("Wrong node: " + node);
700     }
701   }
702
703   public static boolean isEmpty(@Nullable Element element) {
704     return element == null || (element.getAttributes().isEmpty() && element.getContent().isEmpty());
705   }
706
707   public static boolean isEmpty(@Nullable Element element, int attributeCount) {
708     return element == null || (element.getAttributes().size() == attributeCount && element.getContent().isEmpty());
709   }
710
711   public static void merge(@NotNull Element to, @NotNull Element from) {
712     for (Iterator<Element> iterator = from.getChildren().iterator(); iterator.hasNext(); ) {
713       Element configuration = iterator.next();
714       iterator.remove();
715       to.addContent(configuration);
716     }
717     for (Iterator<Attribute> iterator = from.getAttributes().iterator(); iterator.hasNext(); ) {
718       Attribute attribute = iterator.next();
719       iterator.remove();
720       to.setAttribute(attribute);
721     }
722   }
723 }