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