/******************************************************************************* * Copyright (c) 2009, 2013 Andrew Gvozdev (Quoin Inc.). * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Andrew Gvozdev (Quoin Inc.) *******************************************************************************/ package org.eclipse.cdt.internal.core; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.eclipse.cdt.core.CCorePlugin; import org.eclipse.cdt.core.resources.ResourcesUtil; import org.eclipse.cdt.internal.core.model.Util; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * XML utilities. * */ public class XmlUtil { private static final String ENCODING_UTF_8 = "UTF-8"; //$NON-NLS-1$ private static final String EOL_XML = "\n"; //$NON-NLS-1$ private static final String DEFAULT_IDENT = "\t"; //$NON-NLS-1$ private static String LINE_SEPARATOR = System.getProperty("line.separator"); //$NON-NLS-1$ /** * Convenience method to create new XML DOM Document. * * @return a new instance of a DOM {@link Document}. * @throws ParserConfigurationException in case of a problem retrieving {@link DocumentBuilder}. */ public static Document newDocument() throws ParserConfigurationException { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); return builder.newDocument(); } /** * Convenience method to retrieve value of a node. * @return node value or {@code null} */ public static String determineNodeValue(Node node) { return node!=null ? node.getNodeValue() : null; } /** * Convenience method to retrieve an attribute of an element. * Note that calling element.getAttributes() once may be more efficient when pulling several attributes. * * @param element - element to retrieve the attribute from. * @param attr - attribute to get value. * @return attribute value or {@code null} */ public static String determineAttributeValue(Node element, String attr) { NamedNodeMap attributes = element.getAttributes(); return attributes!=null ? determineNodeValue(attributes.getNamedItem(attr)) : null; } /** * The method creates an element with specified name and attributes and appends it to the parent element. * This is a convenience method for often used sequence of calls. * * @param parent - the node where to append the new element. * @param name - the name of the element type being created. * @param attributes - string array of pairs attributes and their values. * Each attribute must have a value, so the array must have even number of elements. * @return the newly created element. * * @throws ArrayIndexOutOfBoundsException in case of odd number of elements of the attribute array * (i.e. the last attribute is missing a value). */ public static Element appendElement(Node parent, String name, String[] attributes) { Document doc = parent instanceof Document ? (Document)parent : parent.getOwnerDocument(); Element element = doc.createElement(name); if (attributes!=null) { int attrLen = attributes.length; for (int i=0;i<attrLen;i+=2) { String attrName = attributes[i]; String attrValue = attributes[i+1]; element.setAttribute(attrName, attrValue); } } parent.appendChild(element); return element; } /** * The method creates an element with specified name and appends it to the parent element. * This is a shortcut for {@link #appendElement(Node, String, String[])} with no attributes specified. * * @param parent - the node where to append the new element. * @param name - the name of the element type being created. * @return the newly created element. */ public static Element appendElement(Node parent, String name) { return appendElement(parent, name, null); } /** * As a workaround for {@code javax.xml.transform.Transformer} not being able * to pretty print XML. This method prepares DOM {@code Document} for the transformer * to be pretty printed, i.e. providing proper indentations for enclosed tags. * * @param doc - DOM document to be pretty printed */ public static void prettyFormat(Document doc) { prettyFormat(doc, DEFAULT_IDENT); } /** * As a workaround for {@code javax.xml.transform.Transformer} not being able * to pretty print XML. This method prepares DOM {@code Document} for the transformer * to be pretty printed, i.e. providing proper indentations for enclosed tags. * * @param doc - DOM document to be pretty printed * @param ident - custom indentation as a string of white spaces */ public static void prettyFormat(Document doc, String ident) { doc.normalize(); Element documentElement = doc.getDocumentElement(); if (documentElement!=null) { prettyFormat(documentElement, "", ident); //$NON-NLS-1$ } } /** * The method inserts end-of-line+indentation Text nodes where indentation is necessary. * * @param node - node to be pretty formatted * @param identLevel - initial indentation level of the node * @param ident - additional indentation inside the node */ private static void prettyFormat(Node node, String identLevel, String ident) { NodeList nodelist = node.getChildNodes(); int iStart=0; Node item = nodelist.item(0); if (item!=null) { short type = item.getNodeType(); if (type==Node.ELEMENT_NODE || type==Node.COMMENT_NODE) { Node newChild = node.getOwnerDocument().createTextNode(EOL_XML + identLevel + ident); node.insertBefore(newChild, item); iStart=1; } } for (int i=iStart;i<nodelist.getLength();i++) { item = nodelist.item(i); if (item!=null) { short type = item.getNodeType(); if (type==Node.TEXT_NODE && item.getNodeValue().trim().length()==0) { if (i+1<nodelist.getLength()) { item.setNodeValue(EOL_XML + identLevel + ident); } else { item.setNodeValue(EOL_XML + identLevel); } } else if (type==Node.ELEMENT_NODE) { prettyFormat(item, identLevel + ident, ident); if (i+1<nodelist.getLength()) { Node nextItem = nodelist.item(i+1); if (nextItem!=null) { short nextType = nextItem.getNodeType(); if (nextType==Node.ELEMENT_NODE || nextType==Node.COMMENT_NODE) { Node newChild = node.getOwnerDocument().createTextNode(EOL_XML + identLevel + ident); node.insertBefore(newChild, nextItem); i++; continue; } } } else { Node newChild = node.getOwnerDocument().createTextNode(EOL_XML + identLevel); node.appendChild(newChild); i++; continue; } } } } } /** * Load XML from input stream to DOM Document. * * @param xmlStream - XML stream. * @return new loaded DOM Document. * @throws CoreException if something goes wrong. */ private static Document loadXml(InputStream xmlStream) throws CoreException { try { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); return builder.parse(xmlStream); } catch (Exception e) { throw new CoreException(CCorePlugin.createStatus(Messages.XmlUtil_InternalErrorLoading, e)); } } /** * Load XML from file to DOM Document. * * @param uriLocation - location of XML file. * @return new loaded XML Document or {@code null} if file does not exist. * @throws CoreException if something goes wrong. */ public static Document loadXml(URI uriLocation) throws CoreException { java.io.File xmlFile = new java.io.File(uriLocation); if (!xmlFile.exists()) { return null; } try { InputStream xmlStream = new FileInputStream(xmlFile); try { return loadXml(xmlStream); } finally { xmlStream.close(); } } catch (Exception e) { throw new CoreException(CCorePlugin.createStatus(Messages.XmlUtil_InternalErrorLoading, e)); } } /** * Load XML from file to DOM Document. * * @param xmlFile - XML file * @return new loaded XML Document. * @throws CoreException if something goes wrong. */ public static Document loadXml(IFile xmlFile) throws CoreException { try { InputStream xmlStream = xmlFile.getContents(); try { return loadXml(xmlStream); } finally { xmlStream.close(); } } catch (Exception e) { throw new CoreException(CCorePlugin.createStatus(Messages.XmlUtil_InternalErrorLoading, e)); } } /** * Serialize XML Document into a file.<br/> * Note: clients should synchronize access to this method. * * @param doc - DOM Document to serialize. * @param uriLocation - URI of the file. * @param lineSeparator - line separator. * * @throws IOException in case of problems with file I/O * @throws TransformerException in case of problems with XML output */ public static void serializeXml(Document doc, URI uriLocation, String lineSeparator) throws IOException, TransformerException, CoreException { XmlUtil.prettyFormat(doc); java.io.File storeFile = new java.io.File(uriLocation); if (!storeFile.exists()) { storeFile.createNewFile(); } String utfString = new String(toByteArray(doc), ENCODING_UTF_8); utfString = XmlUtil.replaceLineSeparatorInternal(utfString, lineSeparator); FileOutputStream output = getFileOutputStreamWorkaround(storeFile); output.write(utfString.getBytes(ENCODING_UTF_8)); output.close(); ResourcesUtil.refreshWorkspaceFiles(uriLocation); } /** * Workaround for Java problem on Windows with releasing buffers for memory-mapped files. * * @see "http://stackoverflow.com/questions/3602783/file-access-synchronized-on-java-object" * @see "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6354433" * @see "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4715154" * @see "http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4469299" */ private static FileOutputStream getFileOutputStreamWorkaround(java.io.File storeFile) throws FileNotFoundException { final int maxCount = 10; for (int i = 0; i <= maxCount; i++) { try { // there is no sleep on first round Thread.sleep(10 * i); } catch (InterruptedException e) { // restore interrupted status Thread.currentThread().interrupt(); } try { return new FileOutputStream(storeFile); } catch (FileNotFoundException e) { // only apply workaround for the very specific exception if (i >= maxCount || !e.getMessage().contains("The requested operation cannot be performed on a file with a user-mapped section open")) { //$NON-NLS-1$ throw e; } // CCorePlugin.log(new Status(IStatus.INFO, CCorePlugin.PLUGIN_ID, "Workaround for concurrent access to memory-mapped files applied, attempt " + (i + 1), e)); //$NON-NLS-1$ } } // will never get here return null; } /** * Serialize XML Document into a byte array. * @param doc - DOM Document to serialize. * @return XML as a byte array. * @throws CoreException if something goes wrong. */ private static byte[] toByteArray(Document doc) throws CoreException { XmlUtil.prettyFormat(doc); try { ByteArrayOutputStream stream = new ByteArrayOutputStream(); Transformer transformer = TransformerFactory.newInstance().newTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.ENCODING, ENCODING_UTF_8); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ DOMSource source = new DOMSource(doc); StreamResult result = new StreamResult(stream); transformer.transform(source, result); return stream.toByteArray(); } catch (Exception e) { throw new CoreException(CCorePlugin.createStatus(Messages.XmlUtil_InternalErrorSerializing, e)); } } /** * <b>Do not use outside of CDT.</b> This method is a workaround for {@link javax.xml.transform.Transformer} * not being able to specify the line separator. This method replaces a string generated by * {@link javax.xml.transform.Transformer} which contains the system line.separator with the line separators * from an existing file or the preferences if it's a new file. * * @param string - the string to be replaced * @param lineSeparator - line separator to be used in the string * * @noreference This method is not intended to be referenced by clients. * This is an internal method which ideally should be made private. */ public static String replaceLineSeparatorInternal(String string, String lineSeparator) { string = string.replace(LINE_SEPARATOR, lineSeparator); return string; } /** * Serialize XML Document into a workspace file.<br/> * Note: clients should synchronize access to this method. * * @param doc - DOM Document to serialize. * @param file - file where to write the XML. * @throws CoreException if something goes wrong. */ public static void serializeXml(Document doc, IFile file) throws CoreException { XmlUtil.prettyFormat(doc); try { String utfString = new String(toByteArray(doc), ENCODING_UTF_8); String lineSeparator = Util.getLineSeparator(file); utfString = XmlUtil.replaceLineSeparatorInternal(utfString, lineSeparator); InputStream input = new ByteArrayInputStream(utfString.getBytes(ENCODING_UTF_8)); if (file.exists()) { file.setContents(input, IResource.FORCE, null); } else { file.create(input, IResource.FORCE, null); } } catch (UnsupportedEncodingException e) { } } /** * Serialize XML Document into a string. * * @param doc - DOM Document to serialize. * @return XML as a String. * @throws CoreException if something goes wrong. */ public static String toString(Document doc) throws CoreException { return new String(toByteArray(doc)); } }