package net.sf.jabref.plugin; import net.sf.jabref.plugin.*; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.EventList; import net.sf.jabref.net.URLDownload; import net.sf.jabref.JabRefFrame; import net.sf.jabref.Globals; import javax.swing.*; import java.net.URL; import java.net.MalformedURLException; import java.io.*; import java.util.regex.Pattern; import java.util.regex.Matcher; import java.util.*; import java.util.zip.ZipEntry; import java.util.jar.JarFile; import org.java.plugin.registry.PluginDescriptor; import org.java.plugin.registry.PluginRegistry; import org.java.plugin.registry.ManifestProcessingException; import org.java.plugin.registry.ManifestInfo; import org.java.plugin.registry.xml.PluginRegistryImpl; /** * */ public class PluginInstaller { public static final String PLUGIN_XML_FILE = "plugin.xml"; public static final int SUCCESS = 0, UNABLE_TO_CREATE_DIR = 1, UNABLE_TO_COPY_FILE = 2; public static final int NO_VERSIONS_INSTALLED = 0, NEWER_VERSION_INSTALLED = 1, SAME_VERSION_INSTALLED = 2, OLDER_VERSION_INSTALLED = 3, UNCONVENTIONAL_FILENAME = 4, UNKNOWN_VERSION = 5; public static final int NOT_LOADED = 0, LOADED = 1, BAD = 2; public static void installPlugin(JabRefFrame frame, File file, String targetFileName) { String fileName = targetFileName != null ? targetFileName : file.getName(); if (!PluginCore.userPluginDir.exists()) { boolean created = PluginCore.userPluginDir.mkdirs(); if (!created) { JOptionPane.showMessageDialog(frame, Globals.lang("Unable to create plugin directory") +" ("+PluginCore.userPluginDir.getPath()+").", Globals.lang("Plugin installer"), JOptionPane.ERROR_MESSAGE); return; } } int status = checkInstalledVersion(file); int result; switch (status) { case NO_VERSIONS_INSTALLED: result = copyPlugin(frame.getFrame(), file, fileName); if (result == SUCCESS) JOptionPane.showMessageDialog(frame, Globals.lang("Plugin installed successfully. You must restart JabRef to load the new plugin."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); else { String reason; if (result == UNABLE_TO_COPY_FILE) reason = Globals.lang("Unable to copy file"); else reason = Globals.lang("Unable to create user plugin directory") +" ("+PluginCore.userPluginDir.getPath()+")."; JOptionPane.showMessageDialog(frame, Globals.lang("Plugin installation failed.")+" "+reason, Globals.lang("Plugin installer"), JOptionPane.ERROR_MESSAGE); } break; case SAME_VERSION_INSTALLED: JOptionPane.showMessageDialog(frame, Globals.lang("The same version of this plugin is already installed."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); break; case NEWER_VERSION_INSTALLED: JOptionPane.showMessageDialog(frame, Globals.lang("A newer version of this plugin is already installed."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); break; case OLDER_VERSION_INSTALLED: result = copyPlugin(frame.getFrame(), file, fileName); if (result == SUCCESS) { int answer = JOptionPane.showConfirmDialog(frame, Globals.lang("One or more older versions of this plugin is installed. Delete old versions?"), Globals.lang("Plugin installer"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.YES_OPTION) { boolean success = deleteOlderVersions(file); if (success) { JOptionPane.showMessageDialog(frame, Globals.lang("Old versions deleted successfully."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(frame, Globals.lang("Old plugin versions will be deleted next time JabRef starts up."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); } } } else { String reason; if (result == UNABLE_TO_COPY_FILE) reason = Globals.lang("Unable to copy file"); else reason = Globals.lang("Unable to create user plugin directory") +" ("+PluginCore.userPluginDir.getPath()+")."; JOptionPane.showMessageDialog(frame, Globals.lang("Plugin installation failed.")+" "+reason, Globals.lang("Plugin installer"), JOptionPane.ERROR_MESSAGE); } break; //case UNKNOWN_VERSION: // JOptionPane.showMessageDialog(frame, Globals.lang("Could not determine version of ")); // break; case UNKNOWN_VERSION: JLabel lab = new JLabel("<html>"+Globals.lang("Unable to determine plugin name and " +"version. This may not be a valid JabRef plugin.") +"<br>"+Globals.lang("Install anyway?")+"</html>"); int answer = JOptionPane.showConfirmDialog(frame, lab, Globals.lang("Plugin installer"), JOptionPane.YES_NO_OPTION); if (answer == JOptionPane.YES_OPTION) { result = copyPlugin(frame.getFrame(), file, fileName); if (result == SUCCESS) JOptionPane.showMessageDialog(frame, Globals.lang("Plugin installed successfully. You must restart JabRef to load the new plugin."), Globals.lang("Plugin installer"), JOptionPane.INFORMATION_MESSAGE); else { String reason; if (result == UNABLE_TO_COPY_FILE) reason = Globals.lang("Unable to copy file"); else reason = Globals.lang("Unable to create user plugin directory") +" ("+PluginCore.userPluginDir.getPath()+")."; JOptionPane.showMessageDialog(frame, Globals.lang("Plugin installation failed.")+" "+reason, Globals.lang("Plugin installer"), JOptionPane.ERROR_MESSAGE); } } break; } } /** * Check the status of the named plugin - whether an older, the same or a * newer version is already installed. * @param f The plugin file. * @return an integer indicating the status */ public static int checkInstalledVersion(File f) { String[] nav = getNameAndVersion(f); if (nav == null) return UNKNOWN_VERSION; VersionNumber vn = new VersionNumber(nav[1]); Map<VersionNumber, File> versions = getInstalledVersions(nav[0]); if (versions.size() == 0) { return NO_VERSIONS_INSTALLED; } VersionNumber thenum = versions.keySet().iterator().next(); boolean hasSame = vn.compareTo(thenum) == 0; boolean hasNewer = vn.compareTo(thenum) > 0; if (hasNewer) return NEWER_VERSION_INSTALLED; if (hasSame) return SAME_VERSION_INSTALLED; return OLDER_VERSION_INSTALLED; } /** * Delete the given plugin. * @param plugin Name and version information for the plugin to delete. * @return true if deletion is successful, false otherwise. */ public static boolean deletePlugin(NameAndVersion plugin) { /*String file = buildFileName(plugin.name, plugin.version.equals(VersionNumber.ZERO) ? null : plugin.version.toString());*/ return deletePluginFile(plugin.file); } public static boolean deleteOlderVersions(File f) { String[] nav = getNameAndVersion(f); if (nav == null) return false; boolean success = true; VersionNumber num = new VersionNumber(nav[1]); Map<VersionNumber, File> versions = getInstalledVersions(nav[0]); for (Iterator<VersionNumber> iterator = versions.keySet().iterator(); iterator.hasNext();) { VersionNumber versionNumber = iterator.next(); if (num.compareTo(versionNumber) < 0) { String vnString = versionNumber.equals(VersionNumber.ZERO) ? null : versionNumber.toString(); File file = versions.get(versionNumber);//buildFileName(nav[0], vnString); success = deletePluginFile(file);//file).delete() && success; } } return success; } /** * This method deletes a plugin file. If deletion fails - typically happens * on Windows due to file locking - the file is scheduled for deletion on * the next startup. * * @param f The file to delete. * @return true if deletion was successful, false if scheduled for later. */ public static boolean deletePluginFile(File f) { boolean success = f.delete(); if (success) return true; else { schedulePluginForDeletion(f.getPath()); return false; } } /** * Copy a plugin to the user plugin directory. Does not check whether the plugin * already exists. * @param source The local or remote location to copy the plugin from. * @return true if the install was successful */ public static int copyPlugin(JFrame frame, URL source, String destFileName) { if (destFileName == null) destFileName = source.getFile(); if (!PluginCore.userPluginDir.exists()) { boolean created = PluginCore.userPluginDir.mkdirs(); if (!created) { return UNABLE_TO_CREATE_DIR; } } File destFile = new File(PluginCore.userPluginDir, destFileName); URLDownload ud = new URLDownload(frame, source, destFile); try { ud.download(); return SUCCESS; } catch (IOException e) { e.printStackTrace(); return UNABLE_TO_COPY_FILE; } } public static int copyPlugin(JFrame frame, File source, String destFileName) { if (destFileName == null) destFileName = source.getName(); if (!PluginCore.userPluginDir.exists()) { boolean created = PluginCore.userPluginDir.mkdirs(); if (!created) { return UNABLE_TO_CREATE_DIR; } } File destFile = new File(PluginCore.userPluginDir, destFileName); BufferedInputStream in = null; BufferedOutputStream out = null; try { in = new BufferedInputStream(new FileInputStream(source)); out = new BufferedOutputStream(new FileOutputStream(destFile)); byte[] buf = new byte[1024]; int count; while ((count = in.read(buf, 0, buf.length)) > 0) { out.write(buf, 0, count); } } catch (IOException ex) { ex.printStackTrace(); return UNABLE_TO_COPY_FILE; } finally { if (in != null) try { in.close(); } catch (IOException ex) { return UNABLE_TO_COPY_FILE; } if (out != null) try { out.close(); } catch (IOException ex) { return UNABLE_TO_COPY_FILE; } } return SUCCESS; } /** * Based on a plugin name, find all versions that are already present * in the user plugin directory. * @param pluginName The name of the plugin. * @return A map of versions already present, linking to the file containing each. */ public static Map<VersionNumber, File> getInstalledVersions(final String pluginName) { String[] files = PluginCore.userPluginDir.list(new FilenameFilter() { public boolean accept(File file, String s) { return s.endsWith(".jar"); } }); Map<VersionNumber, File> versions = new TreeMap<VersionNumber, File>(); for (int i = 0; i < files.length; i++) { String file = files[i]; File f = new File(PluginCore.userPluginDir,file); String[] nav = getNameAndVersion(f); if (nav != null) { if (nav[0].equals(pluginName)) { VersionNumber vn = new VersionNumber(nav[1]); versions.put(vn, f); } } } return versions; } /** * Add the given filename to the list of plugins to be deleted on the next * JabRef startup. * * @param filename The path to the file to delete. */ public static void schedulePluginForDeletion(String filename) { String[] oldValues = Globals.prefs.getStringArray("deletePlugins"); String[] newValues = oldValues == null ? new String[1] : new String[oldValues.length+1]; if (oldValues != null) for (int i=0; i<oldValues.length; i++) { newValues[i] = oldValues[i]; } newValues[newValues.length-1] = filename; Globals.prefs.putStringArray("deletePlugins", newValues); } /** * Delete the given files. Refuses to delete files outside the user plugin directory. * This method throws no errors is the files don't exist or deletion failed. * @param filenames An array of names of the files to be deleted. */ public static void deletePluginsOnStartup(String[] filenames) { for (int i = 0; i < filenames.length; i++) { String s = filenames[i]; File f = new File(s); if (f.getParentFile().equals(PluginCore.userPluginDir)) { //if (s.startsWith(PluginCore.userPluginDir.getPath())) { boolean success = f.delete(); } else System.out.println("File outside of user plugin dir: "+s); } } static Pattern pluginFilePattern = Pattern.compile("(.*)-([\\d\\.]+).jar"); static Pattern pluginFilePatternNoVersion = Pattern.compile("(.*).jar"); /** * Look inside a jar file, find the plugin.xml file, and use it to determine the name * and version of the plugin. * * @param f The file to investigate. * @return A string array containing the plugin name in the first element and * the version number in the second, or null if the filename couldn't be * interpreted. * */ public static String[] getNameAndVersion(File f) { try { File temp = unpackPluginXML(f); if (temp == null) return null; // Couldn't find the plugin.xml file ManifestInfo mi = PluginCore.getManager().getRegistry(). readManifestInfo(temp.toURI().toURL()); temp.delete(); return new String[] {mi.getId(), mi.getVersion().toString()}; } catch (MalformedURLException e) { e.printStackTrace(); return null; } catch (ManifestProcessingException e) { return null; // Couldn't make sense of the plugin.xml } } /** * Take the name of a jar file and extract the plugin.xml file, if possible, * to a temporary file. * @param f The jar file to extract from. * @return a temporary file to which the plugin.xml file has been copied. */ public static File unpackPluginXML(File f) { InputStream in = null; OutputStream out = null; try { JarFile jar = new JarFile(f); ZipEntry entry = jar.getEntry(PLUGIN_XML_FILE); if (entry == null) { return null; } File dest = File.createTempFile("jabref_plugin", ".xml"); dest.deleteOnExit(); in = new BufferedInputStream(jar.getInputStream(entry)); out = new BufferedOutputStream(new FileOutputStream(dest)); byte[] buffer = new byte[2048]; for (;;) { int nBytes = in.read(buffer); if (nBytes <= 0) break; out.write(buffer, 0, nBytes); } out.flush(); return dest; } catch (IOException ex) { ex.printStackTrace(); return null; } finally { try { if (out != null) out.close(); if (in != null) in.close(); } catch (IOException ex) { ex.printStackTrace(); return null; } } } /** * Build a list of installed plugins. * @return a list of plugin names and version numbers. */ public static EventList<NameAndVersion> findInstalledPlugins() { EventList<NameAndVersion> plugins = new BasicEventList<NameAndVersion>(); if (!PluginCore.userPluginDir.exists()) return plugins; String[] files = PluginCore.userPluginDir.list(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".jar"); } }); HashMap<String,PluginDescriptor> urls = new HashMap<String,PluginDescriptor>(); Collection<PluginDescriptor> descriptors = PluginCore.getManager().getRegistry().getPluginDescriptors(); for (PluginDescriptor desc : descriptors) { if ((desc.getPluginClassName()==null) || !desc.getPluginClassName() .equals("net.sf.jabref.plugin.core.JabRefPlugin")) { urls.put(desc.getId(), desc); } } for (int i=0; i<files.length; i++) { File file = new File(PluginCore.userPluginDir, files[i]); String[] nav = getNameAndVersion(file); if (nav != null) { VersionNumber vn = nav[1] != null ? new VersionNumber(nav[1]) : null; NameAndVersion nameAndVersion = new NameAndVersion(nav[0], vn, true, file); for (Iterator<String> it = urls.keySet().iterator(); it.hasNext();) { String loc = it.next(); if (loc.indexOf(nav[0]) >= 0) { PluginDescriptor desc = urls.get(loc); //System.out.println("Accounted for: "+desc.getId()+" "+desc.getVersion().toString()); if (!PluginCore.getManager().isPluginEnabled(urls.get(loc))) nameAndVersion.setStatus(BAD); else nameAndVersion.setStatus(LOADED); it.remove(); } } plugins.add(nameAndVersion); } } for (String url : urls.keySet()) { PluginDescriptor desc = urls.get(url); File location = new File(desc.getLocation().getFile()); if (location.getPath().indexOf(PluginCore.userPluginDir.getPath()) >= 0) continue; // This must be a loaded user dir plugin that's been deleted. //System.out.println("File: "+desc.getLocation().getFile()); NameAndVersion nameAndVersion = new NameAndVersion(desc.getId(), new VersionNumber(desc.getVersion().toString()), false, location); if (!PluginCore.getManager().isPluginEnabled(urls.get(url))) nameAndVersion.setStatus(BAD); else nameAndVersion.setStatus(LOADED); plugins.add(nameAndVersion); } return plugins; } public static class NameAndVersion implements Comparable { String name; VersionNumber version; int status = 0; boolean inUserDirectory; File file; public NameAndVersion(String name, VersionNumber version, boolean inUserDirectory, File file) { this.name = name; this.version = version; this.inUserDirectory = inUserDirectory; this.file = file; } public int compareTo(Object o) { NameAndVersion oth = (NameAndVersion)o; if (!name.equals(oth.name)) return name.compareTo(oth.name); else { if (version == null) return 1; else if (oth.version == null) return -1; else return version.compareTo(oth.version); } } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } } static class VersionNumber implements Comparable { public static final VersionNumber ZERO = new VersionNumber("0"); List<Integer> digits; public VersionNumber(String number) { digits = new ArrayList<Integer>(); String[] elms = number.split("\\."); for (int i = 0; i < elms.length; i++) { try { int num = Integer.parseInt(elms[i]); digits.add(num); } catch (NumberFormatException ex) { // Do nothing } } } public int compareTo(Object o) { VersionNumber oth = (VersionNumber)o; for (int i=0; i<Math.min(digits.size(), oth.digits.size()); i++) { if (digits.get(i) != oth.digits.get(i)) return oth.digits.get(i)-digits.get(i); } // All digits equal so far, and only one of the numbers has more digits. // The one with digits remaining is the newest one: return oth.digits.size()-digits.size(); } public String toString() { StringBuilder sb = new StringBuilder(); for (Iterator<Integer> integerIterator = digits.iterator(); integerIterator.hasNext();) { sb.append(integerIterator.next()); if (integerIterator.hasNext()) sb.append("."); } return sb.toString(); } public boolean equals(Object o) { return compareTo(o) == 0; } } }