/******************************************************************************* * Copyright (c) 2014, 2016 École Polytechnique de Montréal * * 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: * Geneviève Bastien - Initial API and implementation *******************************************************************************/ package org.eclipse.tracecompass.internal.tmf.analysis.xml.core.module; import static org.eclipse.tracecompass.common.core.NonNullUtils.nullToEmptyString; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.net.URL; import java.nio.channels.FileChannel; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; import javax.xml.validation.SchemaFactory; import javax.xml.validation.Validator; import org.eclipse.core.runtime.FileLocator; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.ISafeRunnable; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.Status; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.osgi.util.NLS; import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.Activator; import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.stateprovider.TmfXmlStrings; import org.osgi.framework.Bundle; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; /** * Class containing some utilities for the XML plug-in packages: for example, it * manages the XML files and validates them * * @author Geneviève Bastien */ public class XmlUtils { /** Sub-directory of the plug-in where XML files are stored */ private static final String XML_DIRECTORY = "xml_files"; //$NON-NLS-1$ /** Name of the XSD schema file */ private static final String XSD = "xmlDefinition.xsd"; //$NON-NLS-1$ /** Extension point ID and attributes */ private static final String TMF_XML_BUILTIN_ID = "org.eclipse.linuxtools.tmf.analysis.xml.core.files"; //$NON-NLS-1$ private static final String XML_FILE_ELEMENT = "xmlfile"; //$NON-NLS-1$ private static final String XML_FILE_ATTRIB = "file"; //$NON-NLS-1$ /** * Extension for XML files */ public static final String XML_EXTENSION = "xml"; //$NON-NLS-1$ /** Make this class non-instantiable */ private XmlUtils() { } /** * Get the path where the XML files are stored. Create it if it does not * exist * * @return path to XML files */ public static IPath getXmlFilesPath() { IPath path = Activator.getDefault().getStateLocation(); path = path.addTrailingSeparator().append(XML_DIRECTORY); /* Check if directory exists, otherwise create it */ File dir = path.toFile(); if (!dir.exists() || !dir.isDirectory()) { dir.mkdirs(); } return path; } /** * Validate the XML file input with the XSD schema * * @param xmlFile * XML file to validate * @return True if the XML validates */ public static IStatus xmlValidate(File xmlFile) { URL url = XmlUtils.class.getResource(XSD); SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); Source xmlSource = new StreamSource(xmlFile); try { Schema schema = schemaFactory.newSchema(url); Validator validator = schema.newValidator(); validator.validate(xmlSource); } catch (SAXParseException e) { String error = NLS.bind(Messages.XmlUtils_XmlParseError, e.getLineNumber(), e.getLocalizedMessage()); Activator.logError(error); return new Status(IStatus.ERROR, Activator.PLUGIN_ID, error, e); } catch (SAXException e) { String error = NLS.bind(Messages.XmlUtils_XmlValidationError, e.getLocalizedMessage()); Activator.logError(error); return new Status(IStatus.ERROR, Activator.PLUGIN_ID, error, e); } catch (IOException e) { String error = Messages.XmlUtils_XmlValidateError; Activator.logError("IO exception occurred", e); //$NON-NLS-1$ return new Status(IStatus.ERROR, Activator.PLUGIN_ID, error, e); } return Status.OK_STATUS; } /** * Adds an XML file to the plugin's path. The XML file should have been * validated using the {@link XmlUtils#xmlValidate(File)} method before * calling this method. * * @param fromFile * The XML file to add * @return Whether the file was successfully added */ public static IStatus addXmlFile(File fromFile) { /* Copy file to path */ File toFile = getXmlFilesPath().addTrailingSeparator().append(fromFile.getName()).toFile(); return copyXmlFile(fromFile, toFile); } /** * List all files under the XML analysis files path. It returns a map where * the key is the file name. * * @return A map with all the XML analysis files */ public static synchronized @NonNull Map<String, File> listFiles() { IPath pathToFiles = XmlUtils.getXmlFilesPath(); File folder = pathToFiles.toFile(); Map<String, File> fileMap = new HashMap<>(); if ((folder.isDirectory() && folder.exists())) { File[] listOfFiles = folder.listFiles(); if (listOfFiles != null) { for (File file : listOfFiles) { IPath path = new Path(file.getName()); if (path.getFileExtension().equals(XML_EXTENSION)) { fileMap.put(file.getName(), file); } } } else { Activator.logError("I/O error occured while accessing files in folder " + folder.getPath()); //$NON-NLS-1$ } } return Collections.unmodifiableMap(fileMap); } /** * List all files advertised through the builtin extension point. It returns a map where the key is the file name. * * @return A map with all the XMl analysis builtin files */ public static synchronized @NonNull Map<String, IPath> listBuiltinFiles() { /* Get the XML files advertised through the extension point */ IConfigurationElement[] elements = Platform.getExtensionRegistry().getConfigurationElementsFor(TMF_XML_BUILTIN_ID); Map<String, IPath> map = new HashMap<>(); for (IConfigurationElement element : elements) { if (element.getName().equals(XML_FILE_ELEMENT)) { final String filename = element.getAttribute(XML_FILE_ATTRIB); final String name = element.getContributor().getName(); // Run this in a safe runner in case there is an exception // (IOException, FileNotFoundException, NPE, etc). // This makes sure other extensions are not prevented from // working if one is faulty. SafeRunner.run(new ISafeRunnable() { @Override public void run() throws IOException { if (name != null) { Bundle bundle = Platform.getBundle(name); if (bundle != null) { URL xmlUrl = bundle.getResource(filename); if (xmlUrl == null) { throw new FileNotFoundException(filename); } URL locatedURL = FileLocator.toFileURL(xmlUrl); map.put(filename, new Path(locatedURL.getPath())); } } } @Override public void handleException(Throwable exception) { // Handled sufficiently in SafeRunner } }); } } return map; } /** * Delete an XML analysis file * * @param name * The XML file to delete */ public static void deleteFile(String name) { Map<String, File> files = listFiles(); File file = files.get(name); if (file == null) { return; } file.delete(); } /** * Export an XML analysis file to an external path * * @param from * The name of the file to export * @param to * The full path of the file to write to * @return Whether the file was successfully exported */ public static IStatus exportXmlFile(String from, String to) { /* Copy file to path */ File fromFile = getXmlFilesPath().addTrailingSeparator().append(from).toFile(); if (!fromFile.exists()) { Activator.logError("Failed to find XML analysis file " + fromFile.getName()); //$NON-NLS-1$ return Status.CANCEL_STATUS; } File toFile = new File(to); return copyXmlFile(fromFile, toFile); } private static IStatus copyXmlFile(File fromFile, File toFile) { try { if (!toFile.exists()) { toFile.createNewFile(); } } catch (IOException e) { String error = Messages.XmlUtils_ErrorCopyingFile; Activator.logError(error, e); return new Status(IStatus.ERROR, Activator.PLUGIN_ID, error, e); } try (FileInputStream fis = new FileInputStream(fromFile); FileOutputStream fos = new FileOutputStream(toFile); FileChannel source = fis.getChannel(); FileChannel destination = fos.getChannel();) { destination.transferFrom(source, 0, source.size()); } catch (IOException e) { String error = Messages.XmlUtils_ErrorCopyingFile; Activator.logError(error, e); return new Status(IStatus.ERROR, Activator.PLUGIN_ID, error, e); } return Status.OK_STATUS; } /** * Get the IDs of all the analysis described in a single file * * @param fileName * The file name * @return The list of IDs */ public static List<String> getAnalysisIdsFromFile(String fileName) { List<String> ids = new ArrayList<>(); File file = getXmlFilesPath().addTrailingSeparator().append(fileName).toFile(); if (file.exists()) { try { Document doc = getDocumentFromFile(file); /* get State Providers modules */ NodeList stateproviderNodes = doc.getElementsByTagName(TmfXmlStrings.STATE_PROVIDER); for (int i = 0; i < stateproviderNodes.getLength(); i++) { ids.add(nullToEmptyString(((Element) stateproviderNodes.item(i)).getAttribute(TmfXmlStrings.ID))); } /* get patterns modules */ NodeList patternNodes = doc.getElementsByTagName(TmfXmlStrings.PATTERN); for (int i = 0; i < patternNodes.getLength(); i++) { ids.add(nullToEmptyString(((Element) patternNodes.item(i)).getAttribute(TmfXmlStrings.ID))); } } catch (ParserConfigurationException | SAXException | IOException e) { Activator.logError("Failed to get analyses IDs from " + fileName); //$NON-NLS-1$ } } return ids; } /** * Load the XML File * * @param file * The XML file * @return The document representing the XML file * @throws ParserConfigurationException * if a DocumentBuilder cannot be created * @throws SAXException * If any parse errors occur. * @throws IOException * If any IO errors occur. */ public static Document getDocumentFromFile(File file) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); Document doc = dbFactory.newDocumentBuilder().parse(file); doc.getDocumentElement().normalize(); return doc; } /** * Get only the XML element children of an XML element. * * @param parent * The parent element to get children from * @return The list of children Element of the parent */ public static @NonNull List<@Nullable Element> getChildElements(Element parent) { NodeList childNodes = parent.getChildNodes(); List<@Nullable Element> childElements = new ArrayList<>(); for (int index = 0; index < childNodes.getLength(); index++) { if (childNodes.item(index).getNodeType() == Node.ELEMENT_NODE) { childElements.add((Element) childNodes.item(index)); } } return childElements; } /** * Get the XML children element of an XML element, but only those of a * certain type * * @param parent * The parent element to get the children from * @param elementTag * The tag of the elements to return * @return The list of children {@link Element} of the parent */ public static List<@NonNull Element> getChildElements(Element parent, String elementTag) { /* get the state providers and find the corresponding one */ NodeList nodes = parent.getElementsByTagName(elementTag); List<@NonNull Element> childElements = new ArrayList<>(); for (int i = 0; i < nodes.getLength(); i++) { Element node = (Element) nodes.item(i); if (node.getParentNode().equals(parent)) { childElements.add(node); } } return childElements; } /** * Return the node element corresponding to the requested type in the file. * * TODO: Nothing prevents from having duplicate type -> id in a same file. * That should not be allowed. If you want an element with the same ID as * another one, it should be in a different file and we should check it at * validation time. * * @param filePath * The absolute path to the XML file * @param elementType * The type of top level element to search for * @param elementId * The ID of the desired element * @return The XML element or <code>null</code> if not found */ public static Element getElementInFile(String filePath, @NonNull String elementType, @NonNull String elementId) { if (filePath == null) { return null; } IPath path = new Path(filePath); File file = path.toFile(); if (file == null || !file.exists() || !file.isFile() || !xmlValidate(file).isOK()) { return null; } try { Document doc = getDocumentFromFile(file); /* get the state providers and find the corresponding one */ NodeList nodes = doc.getElementsByTagName(elementType); Element foundNode = null; for (int i = 0; i < nodes.getLength(); i++) { Element node = (Element) nodes.item(i); String id = node.getAttribute(TmfXmlStrings.ID); if (id.equals(elementId)) { foundNode = node; } } return foundNode; } catch (ParserConfigurationException | SAXException | IOException e) { return null; } } }