XML.java
001 /*
002  * Java GPX Library (jpx-3.1.0).
003  * Copyright (c) 2016-2023 Franz Wilhelmstötter
004  *
005  * Licensed under the Apache License, Version 2.0 (the "License");
006  * you may not use this file except in compliance with the License.
007  * You may obtain a copy of the License at
008  *
009  *      http://www.apache.org/licenses/LICENSE-2.0
010  *
011  * Unless required by applicable law or agreed to in writing, software
012  * distributed under the License is distributed on an "AS IS" BASIS,
013  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014  * See the License for the specific language governing permissions and
015  * limitations under the License.
016  *
017  * Author:
018  *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019  */
020 package io.jenetics.jpx;
021 
022 import static java.lang.String.format;
023 import static java.nio.charset.StandardCharsets.UTF_8;
024 import static javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION;
025 import static javax.xml.transform.OutputKeys.VERSION;
026 
027 import java.io.ByteArrayInputStream;
028 import java.io.ByteArrayOutputStream;
029 import java.io.IOException;
030 import java.io.OutputStream;
031 import java.io.UncheckedIOException;
032 import java.util.ArrayList;
033 import java.util.List;
034 import java.util.Objects;
035 
036 import javax.xml.parsers.DocumentBuilder;
037 import javax.xml.parsers.ParserConfigurationException;
038 import javax.xml.stream.XMLStreamException;
039 import javax.xml.stream.XMLStreamReader;
040 import javax.xml.stream.XMLStreamWriter;
041 import javax.xml.transform.Result;
042 import javax.xml.transform.Source;
043 import javax.xml.transform.Transformer;
044 import javax.xml.transform.TransformerException;
045 import javax.xml.transform.TransformerFactory;
046 import javax.xml.transform.dom.DOMResult;
047 import javax.xml.transform.dom.DOMSource;
048 import javax.xml.transform.stax.StAXResult;
049 import javax.xml.transform.stax.StAXSource;
050 import javax.xml.transform.stream.StreamResult;
051 import javax.xml.transform.stream.StreamSource;
052 
053 import org.w3c.dom.DOMException;
054 import org.w3c.dom.Document;
055 import org.w3c.dom.Element;
056 import org.w3c.dom.NamedNodeMap;
057 import org.w3c.dom.Node;
058 import org.w3c.dom.NodeList;
059 
060 /**
061  @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
062  @version 1.5
063  @since 1.5
064  */
065 final class XML {
066 
067     private static final class TFHolder {
068         private static final TFHolder INSTANCE = new TFHolder();
069 
070         final TransformerFactory factory;
071 
072         private TFHolder() {
073             factory = TransformerFactory.newInstance();
074         }
075     }
076 
077     private XML() {
078     }
079 
080 
081     private static void __copy(final Source source, final Result sink)
082         throws XMLStreamException
083     {
084         try {
085             final Transformer transformer = TFHolder.INSTANCE.factory
086                 .newTransformer();
087 
088             transformer.setOutputProperty(OMIT_XML_DECLARATION, "yes");
089             transformer.setOutputProperty(VERSION, "1.0");
090 
091             transformer.transform(source, sink);
092         catch (TransformerException e) {
093             throw new XMLStreamException(e);
094         }
095     }
096 
097     static void copy(final XMLStreamReader source, final Document sink)
098         throws XMLStreamException
099     {
100         __copy(new StAXSource(source)new DOMResult(sink));
101     }
102 
103     static void copy(final Node source, final XMLStreamWriter sink)
104         throws XMLStreamException
105     {
106         __copy(new DOMSource(source)new StAXResult(sink));
107     }
108 
109     static void copy(final Node source, final OutputStream sink)
110         throws IOException
111     {
112         try {
113             __copy(new DOMSource(source)new StreamResult(sink));
114         catch (XMLStreamException e) {
115             throw new IOException(e);
116         }
117     }
118 
119     static String toString(final Node source) {
120         final ByteArrayOutputStream out = new ByteArrayOutputStream();
121         try {
122             copy(source, out);
123         catch (IOException e) {
124             throw new UncheckedIOException(e);
125         }
126         return out.toString();
127     }
128 
129     static DocumentBuilder builder()
130         throws XMLStreamException
131     {
132         try {
133             return XMLProvider
134                 .provider()
135                 .documentBuilderFactory()
136                 .newDocumentBuilder();
137         catch (ParserConfigurationException e) {
138             throw new XMLStreamException(e);
139         }
140     }
141 
142     static Document parse(final String xml) {
143         try {
144             final Document doc = builder().newDocument();
145 
146             final ByteArrayInputStream in = new ByteArrayInputStream(xml.getBytes(UTF_8));
147             __copy(new StreamSource(in)new DOMResult(doc));
148             return clean(doc);
149         catch (Exception e) {
150             throw new IllegalArgumentException(e);
151         }
152     }
153 
154     static Document clone(final Document doc) {
155         if (doc == nullreturn null;
156 
157         try {
158             final Transformer transformer = TFHolder.INSTANCE.factory
159                 .newTransformer();
160 
161             final DOMSource source = new DOMSource(doc);
162             final DOMResult result = new DOMResult();
163             transformer.transform(source, result);
164             return (Document)result.getNode();
165         catch (TransformerException e) {
166             throw (DOMException)
167                 new DOMException(DOMException.NOT_SUPPORTED_ERR, e.getMessage())
168                     .initCause(e);
169         }
170     }
171 
172     static boolean equals(final Node n1, final Node n2) {
173         if (n1 == n2return true;
174         if (n1 == null || n2 == nullreturn false;
175         if (!Objects.equals(n1.getNodeValue(), n2.getNodeValue())) return false;
176         if (!equals(n1.getAttributes(), n2.getAttributes())) return false;
177 
178         final NodeList nl1 = n1.getChildNodes();
179         final NodeList nl2 = n2.getChildNodes();
180         if (nl1.getLength() != nl2.getLength()) return false;
181         for (int i = 0; i < nl1.getLength(); ++i) {
182             if (!equals(nl1.item(i), nl2.item(i))) return false;
183         }
184 
185         return true;
186     }
187 
188     private static boolean equals(final NamedNodeMap a1, final NamedNodeMap a2) {
189         if (a1 == null && a2 == nullreturn true;
190         if (a1 == null || a2 == nullreturn false;
191         if (a1.getLength() != a2.getLength()) return false;
192 
193         for (int i = 0; i < a1.getLength(); ++i) {
194             final String name = a1.item(i).getNodeName();
195             if (!"xmlns".equals(name)) {
196                 final String v1 = a1.item(i).getNodeValue();
197                 final String v2 = a2.getNamedItem(a1.item(i).getNodeName()).getNodeValue();
198 
199                 if (!Objects.equals(v1, v2)) return false;
200             }
201         }
202 
203         return true;
204     }
205 
206     private static boolean isEmpty(final Document doc) {
207         return doc == null ||
208             doc.getDocumentElement().getChildNodes().getLength() == 0;
209     }
210 
211     private static <T extends Node> T clean(final T node) {
212         if (node == nullreturn null;
213 
214         node.normalize();
215         final List<Node> remove = new ArrayList<>();
216         clean(node, remove);
217         for (Node n : remove) {
218             if (n.getParentNode() != null) {
219                 n.getParentNode().removeChild(n);
220             }
221         }
222 
223         return node;
224     }
225 
226     private static void clean(final Node node, final List<Node> remove) {
227         if (node.getNodeType() == Node.TEXT_NODE
228             && isEmpty(node.getTextContent()))
229         {
230             remove.add(node);
231         }
232 
233         final NodeList list = node.getChildNodes();
234         for (int i = 0; i < list.getLength(); i++) {
235             clean(list.item(i), remove);
236         }
237     }
238 
239     private static boolean isEmpty(final String text) {
240         if (text == nullreturn true;
241         if (text.isEmpty()) return true;
242         for (int i = 0; i < text.length(); ++i) {
243             if (!Character.isWhitespace(text.charAt(i))) {
244                 return false;
245             }
246         }
247         return true;
248     }
249 
250     static Document removeNS(final Document doc) {
251         if (doc == nullreturn null;
252 
253         final Node root = doc.getDocumentElement();
254         final Element newRoot = doc.createElement(root.getNodeName());
255 
256         final NodeList children = root.getChildNodes();
257         for (int i = 0; i < children.getLength(); ++i) {
258             newRoot.appendChild(children.item(i).cloneNode(true));
259         }
260 
261         doc.replaceChild(newRoot, root);
262         return doc;
263     }
264 
265     static Document extensions(final Document extensions) {
266         final Document doc = XML.clean(extensions);
267         return XML.isEmpty(docnull : doc;
268     }
269 
270     static Document checkExtensions(final Document extensions) {
271         if (extensions != null) {
272             final Element root = extensions.getDocumentElement();
273 
274             if (root == null) {
275                 throw new IllegalArgumentException(
276                     "'extensions' has no document element."
277                 );
278             }
279 
280             if (!"extensions".equals(root.getNodeName())) {
281                 throw new IllegalArgumentException(format(
282                     "Expected 'extensions' root element, but got '%s'.",
283                     root.getNodeName()
284                 ));
285             }
286 
287             if (root.getNamespaceURI() != null) {
288                 final String ns = root.getNamespaceURI();
289                 if (!ns.isEmpty() &&
290                     !ns.startsWith("http://www.topografix.com/GPX/1/1"&&
291                     !ns.startsWith("http://www.topografix.com/GPX/1/0"))
292                 {
293                     throw new IllegalArgumentException(format(
294                         "Invalid document namespace: '%s'.", ns
295                     ));
296                 }
297             }
298         }
299 
300         return extensions;
301     }
302 
303 }