1 /*
2  * Copyright 2010 the original author or authors.
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 
17 package androidx.build.testutils;
18 
19 import org.gradle.api.Action;
20 import org.gradle.api.XmlProvider;
21 import org.gradle.api.internal.DomNode;
22 import org.gradle.internal.SystemProperties;
23 import org.gradle.internal.UncheckedException;
24 import org.gradle.util.internal.GUtil;
25 import org.gradle.util.internal.TextUtil;
26 import org.w3c.dom.Document;
27 import org.w3c.dom.Element;
28 import org.xml.sax.InputSource;
29 
30 import java.io.BufferedOutputStream;
31 import java.io.BufferedWriter;
32 import java.io.File;
33 import java.io.IOException;
34 import java.io.OutputStream;
35 import java.io.OutputStreamWriter;
36 import java.io.PrintWriter;
37 import java.io.StringReader;
38 import java.io.StringWriter;
39 import java.io.Writer;
40 import java.nio.charset.StandardCharsets;
41 import java.nio.file.Files;
42 
43 import javax.xml.parsers.DocumentBuilderFactory;
44 import javax.xml.transform.OutputKeys;
45 import javax.xml.transform.TransformerException;
46 import javax.xml.transform.TransformerFactory;
47 import javax.xml.transform.dom.DOMSource;
48 import javax.xml.transform.stream.StreamResult;
49 
50 import groovy.util.IndentPrinter;
51 import groovy.util.Node;
52 import groovy.xml.XmlNodePrinter;
53 import groovy.xml.XmlParser;
54 
55 /**
56  * Test fixture for Gradle's XmlProvider.
57  * <p>
58  * Adapted from org.gradle.internal.xml.XmlTransformer.java in the Android Studio repo.
59  */
60 @SuppressWarnings({"NullableProblems", "unused"})
61 public class XmlProviderImpl implements XmlProvider {
62     private final String indentation = "  ";
63 
64     private StringBuilder builder;
65     private Node node;
66     private String stringValue;
67     private Element element;
68     private String publicId;
69     private String systemId;
70 
XmlProviderImpl(String original)71     public XmlProviderImpl(String original) {
72         this.stringValue = original;
73     }
74 
XmlProviderImpl(Node original)75     public XmlProviderImpl(Node original) {
76         this.node = original;
77     }
78 
XmlProviderImpl(DomNode original)79     public XmlProviderImpl(DomNode original) {
80         this.node = original;
81         publicId = original.getPublicId();
82         systemId = original.getSystemId();
83     }
84 
apply(Iterable<Action<? super XmlProvider>> actions)85     public void apply(Iterable<Action<? super XmlProvider>> actions) {
86         for (Action<? super XmlProvider> action : actions) {
87             action.execute(this);
88         }
89     }
90 
91     @Override
toString()92     public String toString() {
93         StringWriter writer = new StringWriter();
94         writeTo(writer);
95         return writer.toString();
96     }
97 
writeTo(Writer writer)98     public void writeTo(Writer writer) {
99         doWriteTo(writer, null);
100     }
101 
writeTo(Writer writer, String encoding)102     public void writeTo(Writer writer, String encoding) {
103         doWriteTo(writer, encoding);
104     }
105 
writeTo(File file)106     public void writeTo(File file) {
107         try (OutputStream outputStream = new BufferedOutputStream(
108                 Files.newOutputStream(file.toPath()))) {
109             writeTo(outputStream);
110         } catch (IOException e) {
111             throw UncheckedException.throwAsUncheckedException(e);
112         }
113     }
114 
writeTo(OutputStream stream)115     public void writeTo(OutputStream stream) {
116         try(Writer writer = new BufferedWriter(new OutputStreamWriter(
117                 stream, StandardCharsets.UTF_8))) {
118             doWriteTo(writer, "UTF-8");
119             writer.flush();
120         } catch (IOException e) {
121             throw UncheckedException.throwAsUncheckedException(e);
122         }
123     }
124 
125     @Override
asString()126     public StringBuilder asString() {
127         if (builder == null) {
128             builder = new StringBuilder(toString());
129             node = null;
130             element = null;
131         }
132         return builder;
133     }
134 
135     @Override
asNode()136     public Node asNode() {
137         if (node == null) {
138             try {
139                 node = new XmlParser().parseText(toString());
140             } catch (Exception e) {
141                 throw UncheckedException.throwAsUncheckedException(e);
142             }
143             builder = null;
144             element = null;
145         }
146         return node;
147     }
148 
149     @Override
asElement()150     public Element asElement() {
151         if (element == null) {
152             Document document;
153             try {
154                 document = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(
155                         new InputSource(new StringReader(toString())));
156             } catch (Exception e) {
157                 throw UncheckedException.throwAsUncheckedException(e);
158             }
159             element = document.getDocumentElement();
160             builder = null;
161             node = null;
162         }
163         return element;
164     }
165 
doWriteTo(Writer writer, String encoding)166     private void doWriteTo(Writer writer, String encoding) {
167         writeXmlDeclaration(writer, encoding);
168 
169         try {
170             if (node != null) {
171                 printNode(node, writer);
172             } else if (element != null) {
173                 printDomNode(element, writer);
174             } else if (builder != null) {
175                 writer.append(TextUtil.toPlatformLineSeparators(stripXmlDeclaration(builder)));
176             } else {
177                 writer.append(TextUtil.toPlatformLineSeparators(stripXmlDeclaration(stringValue)));
178             }
179         } catch (IOException e) {
180             throw UncheckedException.throwAsUncheckedException(e);
181         }
182     }
183 
printNode(Node node, Writer writer)184     private void printNode(Node node, Writer writer) {
185         final PrintWriter printWriter = new PrintWriter(writer);
186         if (GUtil.isTrue(publicId)) {
187             printWriter.format("<!DOCTYPE %s PUBLIC \"%s\" \"%s\">%n", node.name(), publicId,
188                     systemId);
189         }
190         IndentPrinter indentPrinter = new IndentPrinter(printWriter, indentation) {
191             @Override
192             public void println() {
193                 printWriter.println();
194             }
195 
196             @Override
197             public void flush() {
198                 // for performance, ignore flushes
199             }
200         };
201         XmlNodePrinter nodePrinter = new XmlNodePrinter(indentPrinter);
202         nodePrinter.setPreserveWhitespace(true);
203         nodePrinter.print(node);
204         printWriter.flush();
205     }
206 
printDomNode(org.w3c.dom.Node node, Writer destination)207     private void printDomNode(org.w3c.dom.Node node, Writer destination) {
208         removeEmptyTextNodes(node); // empty text nodes hinder subsequent formatting
209         int indentAmount = determineIndentAmount();
210 
211         try {
212             TransformerFactory factory = TransformerFactory.newInstance();
213             try {
214                 factory.setAttribute("indent-number", indentAmount);
215             } catch (IllegalArgumentException ignored) {
216                 /* unsupported by this transformer */
217             }
218 
219             javax.xml.transform.Transformer transformer = factory.newTransformer();
220             transformer.setOutputProperty(OutputKeys.METHOD, "xml");
221             transformer.setOutputProperty(OutputKeys.INDENT, "yes");
222             transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
223             if (GUtil.isTrue(publicId)) {
224                 transformer.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, publicId);
225                 transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemId);
226             }
227             try {
228                 // some impls support this but not factory.setAttribute("indent-number")
229                 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount",
230                         String.valueOf(indentAmount));
231             } catch (IllegalArgumentException ignored) {
232                 /* unsupported by this transformer */
233             }
234 
235             transformer.transform(new DOMSource(node), new StreamResult(destination));
236         } catch (TransformerException e) {
237             throw UncheckedException.throwAsUncheckedException(e);
238         }
239     }
240 
determineIndentAmount()241     private int determineIndentAmount() {
242         return indentation.length(); // assume indentation uses spaces
243     }
244 
removeEmptyTextNodes(org.w3c.dom.Node node)245     private void removeEmptyTextNodes(org.w3c.dom.Node node) {
246         org.w3c.dom.NodeList children = node.getChildNodes();
247 
248         for (int i = 0; i < children.getLength(); i++) {
249             org.w3c.dom.Node child = children.item(i);
250             if (child.getNodeType() == org.w3c.dom.Node.TEXT_NODE
251                     && child.getNodeValue().trim().length() == 0) {
252                 node.removeChild(child);
253                 i--;
254             } else {
255                 removeEmptyTextNodes(child);
256             }
257         }
258     }
259 
writeXmlDeclaration(Writer writer, String encoding)260     private void writeXmlDeclaration(Writer writer, String encoding) {
261         try {
262             writer.write("<?xml version=\"1.0\"");
263             if (encoding != null) {
264                 writer.write(" encoding=\"");
265                 writer.write(encoding);
266                 writer.write("\"");
267             }
268             writer.write("?>");
269             writer.write(SystemProperties.getInstance().getLineSeparator());
270         } catch (IOException e) {
271             throw UncheckedException.throwAsUncheckedException(e);
272         }
273     }
hasXmlDeclaration(String xml)274     private boolean hasXmlDeclaration(String xml) {
275         // XML declarations must be located at first position of first line
276         return xml.startsWith("<?xml");
277     }
278 
stripXmlDeclaration(CharSequence sequence)279     private String stripXmlDeclaration(CharSequence sequence) {
280         String str = sequence.toString();
281         if (hasXmlDeclaration(str)) {
282             str = str.substring(str.indexOf("?>") + 2);
283             str = str.stripLeading();
284         }
285         return str;
286     }
287 }
288