/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.dev.util; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.dev.util.log.speedtracer.CompilerEventType; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger; import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event; import com.google.gwt.util.tools.Utility; import com.google.gwt.util.tools.shared.StringUtils; import org.w3c.dom.Attr; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.Text; import java.io.BufferedWriter; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.Reader; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; import java.net.JarURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Collection; /** * A smattering of useful methods. Methods in this class are candidates for * being moved to {@link com.google.gwt.util.tools.Utility} if they would be * generally useful to tool writers, and don't involve TreeLogger. */ // TODO: remove stream functions and replace with Guava. public final class Util { public static String DEFAULT_ENCODING = "UTF-8"; private static final String FILE_PROTOCOL = "file"; private static final String JAR_PROTOCOL = "jar"; /** * The size of a {@link #threadLocalBuf}, which should be large enough for * efficient data transfer but small enough to fit easily into the L2 cache of * most modern processors. */ private static final int THREAD_LOCAL_BUF_SIZE = 16 * 1024; /** * Stores reusable thread local buffers for efficient data transfer. */ private static final ThreadLocal<byte[]> threadLocalBuf = new ThreadLocal<byte[]>(); /** * Computes the MD5 hash for the specified byte array. * * @return a big fat string encoding of the MD5 for the content, suitably * formatted for use as a file name */ public static String computeStrongName(byte[] content) { return computeStrongName(new byte[][] {content}); } /** * Computes the MD5 hash of the specified byte arrays. * * @return a big fat string encoding of the MD5 for the content, suitably * formatted for use as a file name */ public static String computeStrongName(byte[][] contents) { MessageDigest md5; try { md5 = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Error initializing MD5", e); } /* * Include the lengths of the contents components in the hash, so that the * hashed sequence of bytes is in a one-to-one correspondence with the * possible arguments to this method. */ ByteBuffer b = ByteBuffer.allocate((contents.length + 1) * 4); b.putInt(contents.length); for (int i = 0; i < contents.length; i++) { b.putInt(contents[i].length); } b.flip(); md5.update(b); // Now hash the actual contents of the arrays for (int i = 0; i < contents.length; i++) { md5.update(contents[i]); } return StringUtils.toHexString(md5.digest()); } public static void copy(InputStream is, OutputStream os) throws IOException { try { copyNoClose(is, os); } finally { Utility.close(is); Utility.close(os); } } /** * Copies an input stream out to an output stream. Closes the input steam and * output stream. */ public static void copy(TreeLogger logger, InputStream is, OutputStream os) throws UnableToCompleteException { try { copy(is, os); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Error during copy", e); throw new UnableToCompleteException(); } } /** * Copies all of the bytes from the input stream to the output stream until * the input stream is EOF. Does not close either stream. */ public static void copyNoClose(InputStream is, OutputStream os) throws IOException { byte[] buf = takeThreadLocalBuf(); try { int i; while ((i = is.read(buf)) != -1) { os.write(buf, 0, i); } } finally { releaseThreadLocalBuf(buf); } } public static Reader createReader(TreeLogger logger, URL url) throws UnableToCompleteException { try { return new InputStreamReader(url.openStream()); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to open resource: " + url, e); throw new UnableToCompleteException(); } } /** * Equality check through equals() that is also satisfied if both objects are null. */ public static boolean equalsNullCheck(Object thisObject, Object thatObject) { if (thisObject == null) { return thatObject == null; } return thisObject.equals(thatObject); } /** * Escapes '&', '<', '>', '"', and '\'' to their XML entity equivalents. */ public static String escapeXml(String unescaped) { StringBuilder builder = new StringBuilder(); escapeXml(unescaped, 0, unescaped.length(), true, builder); return builder.toString(); } /** * Escapes '&', '<', '>', '"', and optionally ''' to their XML entity * equivalents. The portion of the input string between start (inclusive) and * end (exclusive) is scanned. The output is appended to the given * StringBuilder. * * @param code the input String * @param start the first character position to scan. * @param end the character position following the last character to scan. * @param quoteApostrophe if true, the ' character is quoted as * &apos; * @param builder a StringBuilder to be appended with the output. */ public static void escapeXml(String code, int start, int end, boolean quoteApostrophe, StringBuilder builder) { int lastIndex = 0; int len = end - start; char[] c = new char[len]; code.getChars(start, end, c, 0); for (int i = 0; i < len; i++) { switch (c[i]) { case '&': builder.append(c, lastIndex, i - lastIndex); builder.append("&"); lastIndex = i + 1; break; case '>': builder.append(c, lastIndex, i - lastIndex); builder.append(">"); lastIndex = i + 1; break; case '<': builder.append(c, lastIndex, i - lastIndex); builder.append("<"); lastIndex = i + 1; break; case '\"': builder.append(c, lastIndex, i - lastIndex); builder.append("""); lastIndex = i + 1; break; case '\'': if (quoteApostrophe) { builder.append(c, lastIndex, i - lastIndex); builder.append("'"); lastIndex = i + 1; } break; default: break; } } builder.append(c, lastIndex, len - lastIndex); } public static URL findSourceInClassPath(ClassLoader cl, String sourceTypeName) { String toTry = sourceTypeName.replace('.', '/') + ".java"; URL foundURL = cl.getResource(toTry); if (foundURL != null) { return foundURL; } int i = sourceTypeName.lastIndexOf('.'); if (i != -1) { return findSourceInClassPath(cl, sourceTypeName.substring(0, i)); } else { return null; } } /** * Returns a byte-array representing the default encoding for a String. */ public static byte[] getBytes(String s) { try { return s.getBytes(DEFAULT_ENCODING); } catch (UnsupportedEncodingException e) { throw new RuntimeException( "The JVM does not support the compiler's default encoding.", e); } } /** * @param className A fully-qualified class name whose name you want. * @return The base name for the specified class. */ public static String getClassName(String className) { return className.substring(className.lastIndexOf('.') + 1); } /** * Gets the contents of a file. * * @param relativePath relative path within the install directory * @return the contents of the file, or null if an error occurred */ public static String getFileFromInstallPath(String relativePath) { String installPath = Utility.getInstallPath(); File file = new File(installPath + '/' + relativePath); return readFileAsString(file); } /** * @param qualifiedName A fully-qualified class name whose package name you want. * @return The package name for the specified class, empty string if default package. */ public static String getPackageName(String qualifiedName) { int idx = qualifiedName.lastIndexOf('.'); if (idx > 0) { return qualifiedName.substring(0, idx); } return ""; } /** * Retrieves the last modified time of a provided URL. * * @return a positive value indicating milliseconds since the epoch (00:00:00 * Jan 1, 1970), or 0L on failure, such as a SecurityException or * IOException. */ public static long getResourceModifiedTime(URL url) { long lastModified = 0L; try { if (url.getProtocol().equals(JAR_PROTOCOL)) { /* * If this resource is contained inside a jar file, such as can happen * if it's bundled in a 3rd-party library, we use the jar file itself to * test whether it's up to date. We don't want to call * JarURLConnection.getLastModified(), as this is much slower than using * the jar File resource directly. */ JarURLConnection jarConn = (JarURLConnection) url.openConnection(); url = jarConn.getJarFileURL(); } if (url.getProtocol().equals(FILE_PROTOCOL)) { /* * Need to handle possibly wonky syntax in a file URL resource. Modeled * after suggestion in this blog entry: * http://weblogs.java.net/blog/2007 * /04/25/how-convert-javaneturl-javaiofile */ File file; try { file = new File(url.toURI()); } catch (URISyntaxException uriEx) { file = new File(url.getPath()); } lastModified = file.lastModified(); } } catch (IOException ignored) { } catch (RuntimeException ignored) { } return lastModified; } public static boolean isValidJavaIdent(String token) { if (token.length() == 0) { return false; } if (!Character.isJavaIdentifierStart(token.charAt(0))) { return false; } for (int i = 1, n = token.length(); i < n; i++) { if (!Character.isJavaIdentifierPart(token.charAt(i))) { return false; } } return true; } /** * Attempts to make a path relative to a particular directory. * * @param from the directory from which 'to' should be relative * @param to an absolute path which will be returned so that it is relative to * 'from' * @return the relative path, if possible; null otherwise */ public static File makeRelativeFile(File from, File to) { // Keep ripping off directories from the 'from' path until the 'from' path // is a prefix of the 'to' path. // String toPath = tryMakeCanonical(to).getAbsolutePath(); File currentFrom = tryMakeCanonical(from.isDirectory() ? from : from.getParentFile()); int numberOfBackups = 0; while (currentFrom != null) { String currentFromPath = currentFrom.getPath(); if (toPath.startsWith(currentFromPath)) { // Found a prefix! // break; } else { ++numberOfBackups; currentFrom = currentFrom.getParentFile(); } } if (currentFrom == null) { // Cannot make it relative. // return null; } // Find everything to the right of the common prefix. // String trailingToPath = toPath.substring(currentFrom.getAbsolutePath().length()); if (currentFrom.getParentFile() != null && trailingToPath.length() > 0) { trailingToPath = trailingToPath.substring(1); } File relativeFile = new File(trailingToPath); for (int i = 0; i < numberOfBackups; ++i) { relativeFile = new File("..", relativeFile.getPath()); } return relativeFile; } public static String makeRelativePath(File from, File to) { File f = makeRelativeFile(from, to); return (f != null ? f.getPath() : null); } public static byte[] readFileAsBytes(File file) { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(file); int length = (int) file.length(); return readBytesFromInputStream(fileInputStream, length); } catch (IOException e) { return null; } finally { Utility.close(fileInputStream); } } public static <T extends Serializable> T readFileAsObject(File file, Class<T> type) throws ClassNotFoundException, IOException { FileInputStream fileInputStream = null; try { fileInputStream = new FileInputStream(file); return readStreamAsObject(fileInputStream, type); } finally { Utility.close(fileInputStream); } } public static String readFileAsString(File file) { byte[] bytes = readFileAsBytes(file); if (bytes != null) { return toString(bytes, DEFAULT_ENCODING); } return null; } /** * Reads an entire input stream as bytes. Closes the input stream. */ public static byte[] readStreamAsBytes(InputStream in) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); copy(in, out); return out.toByteArray(); } catch (IOException e) { return null; } } public static <T> T readStreamAsObject(InputStream inputStream, Class<T> type) throws ClassNotFoundException, IOException { ObjectInputStream objectInputStream = null; try { objectInputStream = new StringInterningObjectInputStream(inputStream); return type.cast(objectInputStream.readObject()); } finally { Utility.close(objectInputStream); } } /** * Reads an entire input stream as String. Closes the input stream. */ public static String readStreamAsString(InputStream in) { try { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); copy(in, out); return out.toString(DEFAULT_ENCODING); } catch (UnsupportedEncodingException e) { throw new RuntimeException( "The JVM does not support the compiler's default encoding.", e); } catch (IOException e) { // TODO(zundel): Consider allowing this exception out. The pattern in this // file is to convert IOException to null, but in references to this // method, there are few places that check for null and do something sane, // the rest just throw an NPE and obscure the root cause. return null; } } /** * @return null if the file could not be read */ public static byte[] readURLAsBytes(URL url) { try { URLConnection conn = url.openConnection(); conn.setUseCaches(false); return readURLConnectionAsBytes(conn); } catch (IOException e) { return null; } } /** * @return null if the file could not be read */ public static char[] readURLAsChars(URL url) { byte[] bytes = readURLAsBytes(url); if (bytes != null) { return toString(bytes, DEFAULT_ENCODING).toCharArray(); } return null; } /** * @return null if the file could not be read */ public static String readURLAsString(URL url) { byte[] bytes = readURLAsBytes(url); if (bytes != null) { return toString(bytes, DEFAULT_ENCODING); } return null; } public static byte[] readURLConnectionAsBytes(URLConnection connection) { // ENH: add a weak cache that has an additional check against the file date InputStream input = null; try { input = connection.getInputStream(); int contentLength = connection.getContentLength(); if (contentLength < 0) { return null; } return readBytesFromInputStream(input, contentLength); } catch (IOException e) { return null; } finally { Utility.close(input); } } /** * Deletes a file or recursively deletes a directory. * * @param file the file to delete, or if this is a directory, the directory * that serves as the root of a recursive deletion * @param childrenOnly if <code>true</code>, only the children of a * directory are recursively deleted but the specified directory * itself is spared; if <code>false</code>, the specified * directory is also deleted; ignored if <code>file</code> is not a * directory */ public static void recursiveDelete(File file, boolean childrenOnly) { recursiveDelete(file, childrenOnly, null); } /** * Selectively deletes a file or recursively deletes a directory. Note that * it is possible that files remain if file.delete() fails. * * @param file the file to delete, or if this is a directory, the directory * that serves as the root of a recursive deletion * @param childrenOnly if <code>true</code>, only the children of a * directory are recursively deleted but the specified directory * itself is spared; if <code>false</code>, the specified * directory is also deleted; ignored if <code>file</code> is not a * directory * @param filter only files matching this filter will be deleted */ public static void recursiveDelete(File file, boolean childrenOnly, FileFilter filter) { if (file.isDirectory()) { File[] children = file.listFiles(); if (children != null) { for (int i = 0; i < children.length; i++) { recursiveDelete(children[i], false, filter); } } if (childrenOnly) { // Do not delete the specified directory itself. return; } } if (filter == null || filter.accept(file)) { file.delete(); } } /** * Release a buffer previously returned from {@link #takeThreadLocalBuf()}. * The released buffer may then be reused. */ public static void releaseThreadLocalBuf(byte[] buf) { assert buf.length == THREAD_LOCAL_BUF_SIZE; threadLocalBuf.set(buf); } /** * Remove leading file:jar:...!/ prefix from source paths for source located in jars. * @param absolutePath an absolute JAR file URL path * @return the location of the file within the JAR */ public static String stripJarPathPrefix(String absolutePath) { if (absolutePath != null) { int bang = absolutePath.lastIndexOf('!'); if (bang != -1) { return absolutePath.substring(bang + 2); } } return absolutePath; } /** * Get a large byte buffer local to this thread. Currently this is set to a * 16k buffer, which is small enough to fit into the L2 cache on modern * processors. The contents of the returned buffer are undefined. Calling * {@link #releaseThreadLocalBuf(byte[])} on the returned buffer allows * subsequent callers to reuse the buffer later, avoiding unncessary * allocations and GC. */ public static byte[] takeThreadLocalBuf() { byte[] buf = threadLocalBuf.get(); if (buf == null) { buf = new byte[THREAD_LOCAL_BUF_SIZE]; } else { threadLocalBuf.set(null); } return buf; } /** * Creates an array from a collection of the specified component type and * size. You can definitely downcast the result to T[] if T is the specified * component type. * * Class<? super T> is used to allow creation of generic types, such as * Map.Entry<K,V> since we can only pass in Map.Entry.class. */ @SuppressWarnings("unchecked") public static <T> T[] toArray(Class<? super T> componentType, Collection<? extends T> coll) { int n = coll.size(); T[] a = (T[]) Array.newInstance(componentType, n); return coll.toArray(a); } /** * Returns a String representing the character content of the bytes; the bytes * must be encoded using the compiler's default encoding. */ public static String toString(byte[] bytes) { return toString(bytes, DEFAULT_ENCODING); } /** * Attempts to find the canonical form of a file path. * * @return the canonical version of the file path, if it could be computed; * otherwise, the original file is returned unmodified */ public static File tryMakeCanonical(File file) { try { return file.getCanonicalFile(); } catch (IOException e) { return file; } } public static void writeBytesToFile(TreeLogger logger, File where, byte[] what) throws UnableToCompleteException { writeBytesToFile(logger, where, new byte[][] {what}); } /** * Gathering write. */ public static void writeBytesToFile(TreeLogger logger, File where, byte[][] what) throws UnableToCompleteException { FileOutputStream f = null; Throwable caught; try { // No need to check mkdirs result because an IOException will occur anyway where.getParentFile().mkdirs(); f = new FileOutputStream(where); for (int i = 0; i < what.length; i++) { f.write(what[i]); } return; } catch (FileNotFoundException e) { caught = e; } catch (IOException e) { caught = e; } finally { Utility.close(f); } String msg = "Unable to write file '" + where + "'"; logger.log(TreeLogger.ERROR, msg, caught); throw new UnableToCompleteException(); } /** * Serializes an object and writes it to a file. */ public static void writeObjectAsFile(TreeLogger logger, File file, Object... objects) throws UnableToCompleteException { Event writeObjectAsFileEvent = SpeedTracerLogger.start(CompilerEventType.WRITE_OBJECT_AS_FILE); FileOutputStream stream = null; try { // No need to check mkdirs result because an IOException will occur anyway file.getParentFile().mkdirs(); stream = new FileOutputStream(file); writeObjectToStream(stream, objects); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to write file: " + file.getAbsolutePath(), e); throw new UnableToCompleteException(); } finally { Utility.close(stream); writeObjectAsFileEvent.end(); } } /** * Serializes an object and writes it to a stream. */ public static void writeObjectToStream(OutputStream stream, Object... objects) throws IOException { ObjectOutputStream objectStream = null; objectStream = new ObjectOutputStream(stream); for (Object object : objects) { objectStream.writeObject(object); } objectStream.flush(); } public static boolean writeStringAsFile(File file, String string) { FileOutputStream stream = null; OutputStreamWriter writer = null; BufferedWriter buffered = null; try { // No need to check mkdirs result because an IOException will occur anyway file.getParentFile().mkdirs(); stream = new FileOutputStream(file); writer = new OutputStreamWriter(stream, DEFAULT_ENCODING); buffered = new BufferedWriter(writer); buffered.write(string); } catch (IOException e) { return false; } finally { Utility.close(buffered); Utility.close(writer); Utility.close(stream); } return true; } public static void writeStringAsFile(TreeLogger logger, File file, String string) throws UnableToCompleteException { FileOutputStream stream = null; OutputStreamWriter writer = null; BufferedWriter buffered = null; try { stream = new FileOutputStream(file); writer = new OutputStreamWriter(stream, DEFAULT_ENCODING); buffered = new BufferedWriter(writer); // No need to check mkdirs result because an IOException will occur anyway file.getParentFile().mkdirs(); buffered.write(string); } catch (IOException e) { logger.log(TreeLogger.ERROR, "Unable to write file: " + file.getAbsolutePath(), e); throw new UnableToCompleteException(); } finally { Utility.close(buffered); Utility.close(writer); Utility.close(stream); } } /** * Writes the contents of a StringBuilder to an OutputStream, encoding * each character using the UTF-* encoding. Unicode characters between * U+0000 and U+10FFFF are supported. */ public static void writeUtf8(StringBuilder builder, OutputStream out) throws IOException { // Rolling our own converter avoids the following: // // o Instantiating the entire builder as a String // o Creating CharEncoders and NIO buffer // o Passing through an OutputStreamWriter int buflen = 1024; char[] inBuf = new char[buflen]; byte[] outBuf = new byte[4 * buflen]; int length = builder.length(); int start = 0; while (start < length) { int end = Math.min(start + buflen, length); builder.getChars(start, end, inBuf, 0); int index = 0; int len = end - start; for (int i = 0; i < len; i++) { int c = inBuf[i] & 0xffff; if (c < 0x80) { outBuf[index++] = (byte) c; } else if (c < 0x800) { int y = c >> 8; int x = c & 0xff; outBuf[index++] = (byte) (0xc0 | (y << 2) | (x >> 6)); // 110yyyxx outBuf[index++] = (byte) (0x80 | (x & 0x3f)); // 10xxxxxx } else if (c < 0xD800 || c > 0xDFFF) { int y = (c >> 8) & 0xff; int x = c & 0xff; outBuf[index++] = (byte) (0xe0 | (y >> 4)); // 1110yyyy outBuf[index++] = (byte) (0x80 | ((y << 2) & 0x3c) | (x >> 6)); // 10yyyyxx outBuf[index++] = (byte) (0x80 | (x & 0x3f)); // 10xxxxxx } else { // Ignore if no second character (which is not be legal unicode) if (i + 1 < len) { int hi = c & 0x3ff; int lo = inBuf[i + 1] & 0x3ff; int full = 0x10000 + ((hi << 10) | lo); int z = (full >> 16) & 0xff; int y = (full >> 8) & 0xff; int x = full & 0xff; outBuf[index++] = (byte) (0xf0 | (z >> 5)); outBuf[index++] = (byte) (0x80 | ((z << 4) & 0x30) | (y >> 4)); outBuf[index++] = (byte) (0x80 | ((y << 2) & 0x3c) | (x >> 6)); outBuf[index++] = (byte) (0x80 | (x & 0x3f)); i++; // char has been consumed } } } out.write(outBuf, 0, index); start = end; } } /** * Reads the specified number of bytes from the {@link InputStream}. * * @param byteLength number of bytes to read * @return byte array containing the bytes read or <code>null</code> if * there is an {@link IOException} or if the requested number of bytes * cannot be read from the {@link InputStream} */ private static byte[] readBytesFromInputStream(InputStream input, int byteLength) { try { byte[] bytes = new byte[byteLength]; int byteOffset = 0; while (byteOffset < byteLength) { int bytesReadCount = input.read(bytes, byteOffset, byteLength - byteOffset); if (bytesReadCount == -1) { return null; } byteOffset += bytesReadCount; } return bytes; } catch (IOException e) { // Ignored. } return null; } /** * Creates a string from the bytes using the specified character set name. * * @param bytes bytes to convert * @param charsetName the name of the character set to use * * @return String for the given bytes and character set or <code>null</code> * if the character set is not supported */ private static String toString(byte[] bytes, String charsetName) { try { return new String(bytes, charsetName); } catch (UnsupportedEncodingException e) { // Ignored. } return null; } private static void writeAttribute(PrintWriter w, Attr attr, int depth) throws IOException { w.write(attr.getName()); w.write('='); Node c = attr.getFirstChild(); while (c != null) { w.write('"'); writeNode(w, c, depth); w.write('"'); c = c.getNextSibling(); } } private static void writeDocument(PrintWriter w, Document d) throws IOException { w.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); Node c = d.getFirstChild(); while (c != null) { writeNode(w, c, 0); c = c.getNextSibling(); } } private static void writeElement(PrintWriter w, Element el, int depth) throws IOException { String tagName = el.getTagName(); writeIndent(w, depth); w.write('<'); w.write(tagName); NamedNodeMap attrs = el.getAttributes(); for (int i = 0, n = attrs.getLength(); i < n; ++i) { w.write(' '); writeNode(w, attrs.item(i), depth); } Node c = el.getFirstChild(); if (c != null) { // There is at least one child. // w.println('>'); // Write the children. // while (c != null) { writeNode(w, c, depth + 1); w.println(); c = c.getNextSibling(); } // Write the closing tag. // writeIndent(w, depth); w.write("</"); w.write(tagName); w.print('>'); } else { // There are no children, so just write the short form close. // w.print("/>"); } } private static void writeIndent(PrintWriter w, int depth) { for (int i = 0; i < depth; ++i) { w.write('\t'); } } private static void writeNode(PrintWriter w, Node node, int depth) throws IOException { short nodeType = node.getNodeType(); switch (nodeType) { case Node.ELEMENT_NODE: writeElement(w, (Element) node, depth); break; case Node.ATTRIBUTE_NODE: writeAttribute(w, (Attr) node, depth); break; case Node.DOCUMENT_NODE: writeDocument(w, (Document) node); break; case Node.TEXT_NODE: writeText(w, (Text) node); break; case Node.COMMENT_NODE: case Node.CDATA_SECTION_NODE: case Node.ENTITY_REFERENCE_NODE: case Node.ENTITY_NODE: case Node.PROCESSING_INSTRUCTION_NODE: default: throw new RuntimeException("Unsupported DOM node type: " + nodeType); } } private static void writeText(PrintWriter w, Text text) throws DOMException { String nodeValue = text.getNodeValue(); String escaped = escapeXml(nodeValue); w.write(escaped); } /** * Not instantiable. */ private Util() { } }