/******************************************************************************* * Copyright (c) 2005 RadRails.org and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Common Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package org.radrails.rails.core.railsplugins; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Reader; import java.io.StringReader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParserFactory; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.debug.core.ILaunch; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import org.eclipse.debug.core.ILaunchManager; import org.radrails.rails.core.RailsLog; import org.radrails.rails.internal.core.RailsPlugin; import org.rubypeople.rdt.launching.IRubyLaunchConfigurationConstants; import org.rubypeople.rdt.launching.RubyRuntime; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.XMLReader; /** * Singleton class that manages access to a list of available Rails plugins. The list is cached in the rails.ui plug-in * state location, and can be refreshed on-demand from the Rails Plugins web service. * * @author mkent */ public class RailsPluginsManager { public class RailsPluginException extends Exception { private static final long serialVersionUID = -7961625830842504591L; public RailsPluginException(Exception e) { super(e); } } private static final int DAY = 1000 * 60 * 60 * 24; private long lastUpdated = -1; private static final String SERVICE_URL = "http://agilewebdevelopment.com/plugins/top_rated.xml"; private static final String PLUGINS_FILE = "rails_plugins.xml"; private static RailsPluginsManager fInstance; private List<RailsPluginDescriptor> fPlugins; private static Set<IRailsPluginListener> listeners = new HashSet<IRailsPluginListener>(); private RailsPluginsManager() { } public static RailsPluginsManager getInstance() { if (fInstance == null) { fInstance = new RailsPluginsManager(); } return fInstance; } public static void addRailsPluginListener(IRailsPluginListener listener) { listeners.add(listener); } public static void removeRailsPluginListener(IRailsPluginListener listener) { listeners.remove(listener); } /** * Gets a list of currently available Rails plugins. The list contains Maps, each of which holds information about a * plugin in String to String mappings. * * @return a list of plugins * @throws Exception */ public List<RailsPluginDescriptor> getPlugins() throws RailsPluginException { if (fPlugins == null) { // Try to load from state file fPlugins = loadPlugins(); // No results, try to update from web service sync if (fPlugins.isEmpty()) { fPlugins = updatePlugins(new NullProgressMonitor()); } else if (haventUpdatedInADay()) // current results are stale, async update { scheduleLoadOfRemotePluginListing(); } } return fPlugins; } private boolean haventUpdatedInADay() { return lastUpdated < (System.currentTimeMillis() - DAY); } private void scheduleLoadOfRemotePluginListing() { Job job = new Job("Grabbing Rails Plugin listing from remote source") { protected IStatus run(IProgressMonitor monitor) { try { updatePlugins(monitor); } catch (Exception e) { RailsLog.log(e); return Status.CANCEL_STATUS; } return Status.OK_STATUS; } }; job.setSystem(true); job.schedule(10000); // schedules a 10-second delay } /** * Updates the current list of Rails plugins from the web service. * * @throws RailsPluginException * @throws Exception * if an error occurs */ public List<RailsPluginDescriptor> updatePlugins(IProgressMonitor monitor) throws RailsPluginException { monitor.beginTask("Updating plugin list", 3); // Get latest list from web service monitor.subTask("Accessing plugin directory"); HttpURLConnection conn; try { conn = getConnection(); } catch (MalformedURLException e1) { throw new RailsPluginException(e1); } catch (ProtocolException e1) { throw new RailsPluginException(e1); } catch (IOException e1) { throw new RailsPluginException(e1); } monitor.worked(1); monitor.subTask("Downloading plugin list"); String pxml = null; try { pxml = getPluginXMLFeed(monitor, conn); } catch (IOException e) { throw new RailsPluginException(e); } monitor.worked(1); monitor.subTask("Processing plugin information"); // Update the object list from the XML data List<RailsPluginDescriptor> plugins = new ArrayList<RailsPluginDescriptor>(); StringReader strIn = new StringReader(pxml); try { plugins = parsePluginsXML(strIn); } catch (SAXException e) { throw new RailsPluginException(e); } catch (ParserConfigurationException e) { throw new RailsPluginException(e); } catch (IOException e) { throw new RailsPluginException(e); } // Cache XML data to file writeOutPluginsXML(pxml); monitor.worked(1); lastUpdated = System.currentTimeMillis(); for (IRailsPluginListener listener : listeners) { listener.remotePluginsRefreshed(); } monitor.done(); return plugins; } private HttpURLConnection getConnection() throws MalformedURLException, IOException, ProtocolException { URL url = new URL(SERVICE_URL); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setRequestMethod("GET"); return conn; } private String getPluginXMLFeed(IProgressMonitor monitor, HttpURLConnection conn) throws IOException { BufferedReader bufIn = new BufferedReader(new InputStreamReader(conn.getInputStream())); StringBuffer pxml = new StringBuffer(); String l = null; while ((l = bufIn.readLine()) != null) { pxml.append(l); } return pxml.toString(); } private void writeOutPluginsXML(String pxml) { PrintWriter out = null; try { File f = new File(getLocalPluginXMLCache().toOSString()); out = new PrintWriter(new BufferedWriter(new FileWriter(f))); out.write(pxml); out.flush(); } catch (IllegalStateException e) { RailsLog.log(e); } catch (IOException e) { RailsLog.log(e); } finally { if (out != null) out.close(); } } /** * Loads the list of plugins from the state file. */ private List<RailsPluginDescriptor> loadPlugins() { File f = new File(getLocalPluginXMLCache().toOSString()); if (f.exists()) { try { FileReader fis = new FileReader(f); return parsePluginsXML(fis); } catch (SAXException e) { RailsLog.logError("Error parsing Rails plugins XML", e); } catch (ParserConfigurationException e) { RailsLog.logError("Error parsing Rails plugins XML: parser misconfigured.", e); } catch (IOException e) { RailsLog.logError("Error parsing Rails plugins XML: I/O problems.", e); } } return Collections.emptyList(); } private IPath getLocalPluginXMLCache() { return RailsPlugin.getInstance().getStateLocation().append(PLUGINS_FILE); } /** * Parses an XML file describing a list of Rails plugins and returns a List of Map objects, containing the plugin * attributes mapped to their corresponding values. * * @param rdr * the XML source * @return a list of plugin Maps * @throws ParserConfigurationException * @throws SAXException * @throws IOException */ private List<RailsPluginDescriptor> parsePluginsXML(Reader rdr) throws SAXException, ParserConfigurationException, IOException { XMLReader reader = SAXParserFactory.newInstance().newSAXParser().getXMLReader(); RailsPluginsContentHandler handler = new RailsPluginsContentHandler(); reader.setContentHandler(handler); reader.parse(new InputSource(rdr)); return handler.getRailsPlugins(); } /** * Install a Rails plugin from an SVN repository. Information about the plugin is provided in a * RailsPluginDescriptor. * * @param project * the project to install the plugin into * @param plugin * the plugin to install * @param externals * true if an svn:externals entry is to be added, false otherwise */ public static void installPlugin(IProject project, RailsPluginDescriptor plugin, boolean externals, boolean checkout) { // Prepare plugin info String repos = plugin.getRepository(); if (repos == null || repos.trim().length() == 0) { RailsLog.log("Plugin has no repository: " + plugin.toString()); repos = plugin.getName(); } // Prepare the command String command = "install "; if (externals || checkout) { String ext_str = externals ? "x" : ""; String check_str = checkout ? "o" : ""; command += "-" + ext_str + check_str + " " + repos; } else { command += repos; } run(project, command); for (IRailsPluginListener listener : listeners) { listener.pluginInstalled(project, plugin); } } public static ILaunch run(IProject project, String args) { try { ILaunchConfigurationWorkingCopy wc = makeWorkingCopy(getPluginScript(), args, project); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_USE_TERMINAL, "org.radrails.rails.shell"); wc.setAttribute(IRubyLaunchConfigurationConstants.ATTR_TERMINAL_COMMAND, "script/plugin " + args); ILaunchConfiguration config = wc.doSave(); return config.launch(ILaunchManager.RUN_MODE, null); } catch (CoreException e) { RailsLog.logError("Error running rake task", e); } return null; } private static String getPluginScript() { return new Path("script").append("plugin").toPortableString(); } /** * Removes a plugin from the given project. * * @param project * the project to remove the plugin from * @param plugin * the plugin to remove */ public static void removePlugin(IProject project, RailsPluginDescriptor plugin) { String name = (String) plugin.getProperty(RailsPluginDescriptor.NAME); if (name.indexOf(' ') > -1) { System.out.println("WTF!"); } run(project, "remove " + name + ""); for (IRailsPluginListener listener : listeners) { listener.pluginRemoved(project, plugin); } } private static ILaunchConfigurationWorkingCopy makeWorkingCopy(String file, String args, IProject project) throws CoreException { String workingDirectory = ""; IPath path = RailsPlugin.findRailsRoot(project); if (path.toPortableString().length() != 0) { workingDirectory = project.getFolder(path).getLocation().toOSString(); } else { workingDirectory = project.getLocation().toOSString(); } return RubyRuntime.createBasicLaunch(file, args, project, workingDirectory); } public static List<RailsPluginDescriptor> getInstalledPlugins(IProject project) { List<RailsPluginDescriptor> plugins = new ArrayList<RailsPluginDescriptor>(); if (project == null) return plugins; IPath railsRoot = RailsPlugin.findRailsRoot(project); IPath pluginsDir = railsRoot.append("vendor").append("plugins"); IFolder pluginsFolder = project.getFolder(pluginsDir); try { IResource[] members = pluginsFolder.members(); for (int i = 0; i < members.length; i++) { IResource member = members[i]; if (member.getType() != IResource.FOLDER) continue; RailsPluginDescriptor desc = new RailsPluginDescriptor(); desc.setProperty(RailsPluginDescriptor.NAME, member.getName()); plugins.add(desc); } } catch (CoreException e) { RailsLog.log(e); } return plugins; } public static boolean pluginInstalled(RailsPluginDescriptor plugin, IProject project) { List<RailsPluginDescriptor> plugins = getInstalledPlugins(project); for (RailsPluginDescriptor descriptor : plugins) { if (normalize(descriptor.getName()).equalsIgnoreCase(plugin.getName())) return true; } return false; } private static String normalize(String string) { return string.replace('_', ' '); } }