/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.jdt.core.launching; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; 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 java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; /** * @author Evgen Vidolob */ public class Launching { private static final Logger LOG = LoggerFactory.getLogger(Launching.class); /** * The id of the JDT launching plug-in (value <code>"org.eclipse.jdt.launching"</code>). */ public static final String ID_PLUGIN = "org.eclipse.jdt.launching"; //$NON-NLS-1$ public static boolean DEBUG_JRE_CONTAINER = false; /** * Mapping of top-level VM installation directories to library info for that * VM. */ private static Map<String, LibraryInfo> fgLibraryInfoMap = null; private static Launching fgLaunching; /** * Mutex for checking the time stamp of an install location */ private static Object installLock = new Object(); /** * List of install locations that have been detected to have changed */ private static HashSet<String> fgHasChanged = new HashSet<>(); /** * Mapping of the last time the directory of a given SDK was modified. * <br><br> * Mapping: <code>Map<String,Long></code> */ private static Map<String, Long> fgInstallTimeMap = null; /** * Status code indicating an unexpected error. * * @since 3.4 */ public static final int ERROR = 125; /** * Returns the library info that corresponds to the specified JRE install * path, or <code>null</code> if none. * * @param javaInstallPath the absolute path to the java executable * @return the library info that corresponds to the specified JRE install * path, or <code>null</code> if none */ public static LibraryInfo getLibraryInfo(String javaInstallPath) { if (fgLibraryInfoMap == null) { restoreLibraryInfo(); } return fgLibraryInfoMap.get(javaInstallPath); } /** * Restores library information for VMs */ private static void restoreLibraryInfo() { fgLibraryInfoMap = new HashMap<String, LibraryInfo>(10); IPath libPath = getDefault().getStateLocation(); libPath = libPath.append("libraryInfos.xml"); //$NON-NLS-1$ File file = libPath.toFile(); if (file.exists()) { try { InputStream stream = new BufferedInputStream(new FileInputStream(file)); DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); parser.setErrorHandler(new DefaultHandler()); Element root = parser.parse(new InputSource(stream)).getDocumentElement(); if (!root.getNodeName().equals("libraryInfos")) { //$NON-NLS-1$ return; } NodeList list = root.getChildNodes(); int length = list.getLength(); for (int i = 0; i < length; ++i) { Node node = list.item(i); short type = node.getNodeType(); if (type == Node.ELEMENT_NODE) { Element element = (Element) node; String nodeName = element.getNodeName(); if (nodeName.equalsIgnoreCase("libraryInfo")) { //$NON-NLS-1$ String version = element.getAttribute("version"); //$NON-NLS-1$ String location = element.getAttribute("home"); //$NON-NLS-1$ String[] bootpath = getPathsFromXML(element, "bootpath"); //$NON-NLS-1$ String[] extDirs = getPathsFromXML(element, "extensionDirs"); //$NON-NLS-1$ String[] endDirs = getPathsFromXML(element, "endorsedDirs"); //$NON-NLS-1$ if (location != null) { LibraryInfo info = new LibraryInfo(version, bootpath, extDirs, endDirs); fgLibraryInfoMap.put(location, info); } } } } } catch (IOException | SAXException | ParserConfigurationException e) { log(e); } } } /** * Returns the location in the local file system of the * plug-in state area for this plug-in. * If the plug-in state area did not exist prior to this call, * it is created. * @throws IllegalStateException, when the system is running with no data area (-data @none), * or when a data area has not been set yet. * @return a local file system path */ public final IPath getStateLocation() throws IllegalStateException { Path path = new Path("/tmp/codenvy/"); File file = path.toFile(); if (!file.exists()){ file.mkdirs(); } return path; } /** * Returns the singleton instance of <code>LaunchingPlugin</code> * @return the singleton instance of <code>LaunchingPlugin</code> */ public static Launching getDefault() { if(fgLaunching == null) { fgLaunching = new Launching(); } return fgLaunching; } /** * Logs the specified status * @param status the status to log */ public static void log(IStatus status) { // getDefault().getLog().log(status); LOG.error(status.getMessage(), status.getException()); } /** * Logs the specified message, by creating a new <code>Status</code> * @param message the message to log as an error status */ public static void log(String message) { log(new Status(IStatus.ERROR, getUniqueIdentifier(), IStatus.ERROR, message, null)); } /** * Logs the specified exception by creating a new <code>Status</code> * @param e the {@link Throwable} to log as an error */ public static void log(Throwable e) { log(new Status(IStatus.ERROR, getUniqueIdentifier(), IStatus.ERROR, e.getMessage(), e)); } /** * Convenience method which returns the unique identifier of this plug-in. * * @return the id of the {@link Launching} */ public static String getUniqueIdentifier() { return ID_PLUGIN; } /** * Returns paths stored in XML * @param lib the library path in {@link Element} form * @param pathType the type of the path * @return paths stored in XML */ private static String[] getPathsFromXML(Element lib, String pathType) { List<String> paths = new ArrayList<String>(); NodeList list = lib.getChildNodes(); int length = list.getLength(); for (int i = 0; i < length; ++i) { Node node = list.item(i); short type = node.getNodeType(); if (type == Node.ELEMENT_NODE) { Element element = (Element) node; String nodeName = element.getNodeName(); if (nodeName.equalsIgnoreCase(pathType)) { NodeList entries = element.getChildNodes(); int numEntries = entries.getLength(); for (int j = 0; j < numEntries; j++) { Node n = entries.item(j); short t = n.getNodeType(); if (t == Node.ELEMENT_NODE) { Element entryElement = (Element)n; String name = entryElement.getNodeName(); if (name.equals("entry")) { //$NON-NLS-1$ String path = entryElement.getAttribute("path"); //$NON-NLS-1$ if (path != null && path.length() > 0) { paths.add(path); } } } } } } } return paths.toArray(new String[paths.size()]); } /** * Checks to see if the time stamp of the file describe by the given location string * has been modified since the last recorded time stamp. If there is no last recorded * time stamp we assume it has changed. See https://bugs.eclipse.org/bugs/show_bug.cgi?id=266651 for more information * * @param location the location of the SDK we want to check the time stamp for * @return <code>true</code> if the time stamp has changed compared to the cached one or if there is * no recorded time stamp, <code>false</code> otherwise. */ public static boolean timeStampChanged(String location) { synchronized (installLock) { if(fgHasChanged.contains(location)) { return true; } File file = new File(location); if(file.exists()) { if(fgInstallTimeMap == null) { readInstallInfo(); } Long stamp = fgInstallTimeMap.get(location); long fstamp = file.lastModified(); if(stamp != null) { if(stamp.longValue() == fstamp) { return false; } } //if there is no recorded stamp we have to assume it is new stamp = new Long(fstamp); fgInstallTimeMap.put(location, stamp); writeInstallInfo(); fgHasChanged.add(location); return true; } } return false; } /** * Reads the file of saved time stamps and populates the {@link #fgInstallTimeMap}. * See https://bugs.eclipse.org/bugs/show_bug.cgi?id=266651 for more information * * @since 3.7 */ private static void readInstallInfo() { fgInstallTimeMap = new HashMap<String, Long>(); IPath libPath = getDefault().getStateLocation(); libPath = libPath.append(".install.xml"); //$NON-NLS-1$ File file = libPath.toFile(); if (file.exists()) { try { InputStream stream = new BufferedInputStream(new FileInputStream(file)); DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); parser.setErrorHandler(new DefaultHandler()); Element root = parser.parse(new InputSource(stream)).getDocumentElement(); if(root.getNodeName().equalsIgnoreCase("dirs")) { //$NON-NLS-1$ NodeList nodes = root.getChildNodes(); Node node = null; Element element = null; for (int i = 0; i < nodes.getLength(); i++) { node = nodes.item(i); if(node.getNodeType() == Node.ELEMENT_NODE) { element = (Element) node; if(element.getNodeName().equalsIgnoreCase("entry")) { //$NON-NLS-1$ String loc = element.getAttribute("loc"); //$NON-NLS-1$ String stamp = element.getAttribute("stamp"); //$NON-NLS-1$ try { Long l = new Long(stamp); fgInstallTimeMap.put(loc, l); } catch(NumberFormatException nfe) { //do nothing } } } } } } catch (IOException e) { log(e); } catch (ParserConfigurationException e) { log(e); } catch (SAXException e) { log(e); } } } /** * Sets the library info that corresponds to the specified JRE install * path. * * @param javaInstallPath home location for a JRE * @param info the library information, or <code>null</code> to remove */ public static void setLibraryInfo(String javaInstallPath, LibraryInfo info) { if (fgLibraryInfoMap == null) { restoreLibraryInfo(); } if (info == null) { fgLibraryInfoMap.remove(javaInstallPath); if(fgInstallTimeMap != null) { fgInstallTimeMap.remove(javaInstallPath); writeInstallInfo(); } } else { fgLibraryInfoMap.put(javaInstallPath, info); } //once the library info has been set we can forget it has changed fgHasChanged.remove(javaInstallPath); saveLibraryInfo(); } /** * Saves the library info in a local workspace state location */ private static void saveLibraryInfo() { OutputStream stream= null; try { String xml = getLibraryInfoAsXML(); IPath libPath = getDefault().getStateLocation(); libPath = libPath.append("libraryInfos.xml"); //$NON-NLS-1$ File file = libPath.toFile(); if (!file.exists()) { file.createNewFile(); } stream = new BufferedOutputStream(new FileOutputStream(file)); stream.write(xml.getBytes("UTF8")); //$NON-NLS-1$ } catch (IOException e) { log(e); } catch (CoreException e) { log(e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e1) { } } } } /** * Return the VM definitions contained in this object as a String of XML. The String * is suitable for storing in the workbench preferences. * <p> * The resulting XML is compatible with the static method <code>parseXMLIntoContainer</code>. * </p> * @return String the results of flattening this object into XML * @throws CoreException if this method fails. Reasons include:<ul> * <li>serialization of the XML document failed</li> * </ul> */ private static String getLibraryInfoAsXML() throws CoreException { Document doc = newDocument(); Element config = doc.createElement("libraryInfos"); //$NON-NLS-1$ doc.appendChild(config); // Create a node for each info in the table Iterator<String> locations = fgLibraryInfoMap.keySet().iterator(); while (locations.hasNext()) { String home = locations.next(); LibraryInfo info = fgLibraryInfoMap.get(home); Element locationElemnet = infoAsElement(doc, info); locationElemnet.setAttribute("home", home); //$NON-NLS-1$ config.appendChild(locationElemnet); } // Serialize the Document and return the resulting String return serializeDocument(doc); } /** * Creates an XML element for the given info. * * @param doc the backing {@link Document} * @param info the {@link LibraryInfo} to add to the {@link Document} * @return Element */ private static Element infoAsElement(Document doc, LibraryInfo info) { Element libraryElement = doc.createElement("libraryInfo"); //$NON-NLS-1$ libraryElement.setAttribute("version", info.getVersion()); //$NON-NLS-1$ appendPathElements(doc, "bootpath", libraryElement, info.getBootpath()); //$NON-NLS-1$ appendPathElements(doc, "extensionDirs", libraryElement, info.getExtensionDirs()); //$NON-NLS-1$ appendPathElements(doc, "endorsedDirs", libraryElement, info.getEndorsedDirs()); //$NON-NLS-1$ return libraryElement; } /** * Appends path elements to the given library element, rooted by an * element of the given type. * * @param doc the backing {@link Document} * @param elementType the kind of {@link Element} to create * @param libraryElement the {@link Element} describing a given {@link LibraryInfo} object * @param paths the paths to add */ private static void appendPathElements(Document doc, String elementType, Element libraryElement, String[] paths) { if (paths.length > 0) { Element child = doc.createElement(elementType); libraryElement.appendChild(child); for (int i = 0; i < paths.length; i++) { String path = paths[i]; Element entry = doc.createElement("entry"); //$NON-NLS-1$ child.appendChild(entry); entry.setAttribute("path", path); //$NON-NLS-1$ } } } /** * Returns a Document that can be used to build a DOM tree * @return the Document * @throws ParserConfigurationException if an exception occurs creating the document builder */ public static Document getDocument() throws ParserConfigurationException { DocumentBuilderFactory dfactory= DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder= dfactory.newDocumentBuilder(); Document doc= docBuilder.newDocument(); return doc; } /** * Creates and returns a new XML document. * * @return a new XML document * @throws CoreException if unable to create a new document */ public static Document newDocument()throws CoreException { try { return getDocument(); } catch (ParserConfigurationException e) { abort("Unable to create new XML document.", e); //$NON-NLS-1$ } return null; } /** * Serializes a XML document into a string - encoded in UTF8 format, * with platform line separators. * * @param doc document to serialize * @return the document as a string * @throws TransformerException if an unrecoverable error occurs during the serialization * @throws IOException if the encoding attempted to be used is not supported */ private static String serializeDocumentInt(Document doc) throws TransformerException, IOException { ByteArrayOutputStream s = new ByteArrayOutputStream(); TransformerFactory factory = TransformerFactory.newInstance(); Transformer transformer = factory.newTransformer(); transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$ transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ DOMSource source = new DOMSource(doc); StreamResult outputTarget = new StreamResult(s); transformer.transform(source, outputTarget); return s.toString("UTF8"); //$NON-NLS-1$ } /** * Serializes the given XML document into a string. * * @param document XML document to serialize * @return a string representing the given document * @throws CoreException if unable to serialize the document */ public static String serializeDocument(Document document) throws CoreException { try { return serializeDocumentInt(document); } catch (TransformerException e) { abort("Unable to serialize XML document.", e); //$NON-NLS-1$ } catch (IOException e) { abort("Unable to serialize XML document.",e); //$NON-NLS-1$ } return null; } /** * Throws an exception with the given message and underlying exception. * * @param message error message * @param exception underlying exception, or <code>null</code> * @throws CoreException if a problem is encountered */ private static void abort(String message, Throwable exception) throws CoreException { IStatus status = new Status(IStatus.ERROR, getUniqueIdentifier(), ERROR, message, exception); throw new CoreException(status); } /** * Writes out the mappings of SDK install time stamps to disk. See * https://bugs.eclipse.org/bugs/show_bug.cgi?id=266651 for more information. */ private static void writeInstallInfo() { if(fgInstallTimeMap != null) { OutputStream stream= null; try { Document doc = newDocument(); Element root = doc.createElement("dirs"); //$NON-NLS-1$ doc.appendChild(root); Map.Entry<String, Long> entry = null; Element e = null; String key = null; for(Iterator<Map.Entry<String, Long>> i = fgInstallTimeMap.entrySet().iterator(); i.hasNext();) { entry = i.next(); key = entry.getKey(); if(fgLibraryInfoMap == null || fgLibraryInfoMap.containsKey(key)) { //only persist the info if the library map also has info OR is null - prevent persisting deleted JRE information e = doc.createElement("entry"); //$NON-NLS-1$ root.appendChild(e); e.setAttribute("loc", key); //$NON-NLS-1$ e.setAttribute("stamp", entry.getValue().toString()); //$NON-NLS-1$ } } String xml = serializeDocument(doc); IPath libPath = getDefault().getStateLocation(); libPath = libPath.append(".install.xml"); //$NON-NLS-1$ File file = libPath.toFile(); if (!file.exists()) { file.createNewFile(); } stream = new BufferedOutputStream(new FileOutputStream(file)); stream.write(xml.getBytes("UTF8")); //$NON-NLS-1$ } catch (IOException e) { log(e); } catch (CoreException e) { log(e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e1) { } } } } } public static File getFileInPlugin(IPath path) { try { return new File(Launching.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath() + path.toString()); } catch (URISyntaxException e) { return null; } } /** * Compares two URL for equality, but do not connect to do DNS resolution * * @param url1 * a given URL * @param url2 * another given URL to compare to url1 * @return <code>true</code> if the URLs are equal, <code>false</code> otherwise * @since 3.5 */ public static boolean sameURL(URL url1, URL url2) { if (url1 == url2) { return true; } if (url1 == null ^ url2 == null) { return false; } // check if URL are file: URL as we may have two URL pointing to the same doc location // but with different representation - (i.e. file:/C;/ and file:C:/) final boolean isFile1 = "file".equalsIgnoreCase(url1.getProtocol());//$NON-NLS-1$ final boolean isFile2 = "file".equalsIgnoreCase(url2.getProtocol());//$NON-NLS-1$ if (isFile1 && isFile2) { File file1 = new File(url1.getFile()); File file2 = new File(url2.getFile()); return file1.equals(file2); } // URL1 XOR URL2 is a file, return false. (They either both need to be files, or neither) if (isFile1 ^ isFile2) { return false; } return getExternalForm(url1).equals(getExternalForm(url2)); } /** * Gets the external form of this URL. In particular, it trims any white space, * removes a trailing slash and creates a lower case string. * * @param url * the URL to get the {@link String} value of * @return the lower-case {@link String} form of the given URL */ private static String getExternalForm(URL url) { String externalForm = url.toExternalForm(); if (externalForm == null) { return ""; //$NON-NLS-1$ } externalForm = externalForm.trim(); if (externalForm.endsWith("/")) { //$NON-NLS-1$ // Remove the trailing slash externalForm = externalForm.substring(0, externalForm.length() - 1); } return externalForm.toLowerCase(); } }