/** * Copyright (C) 2005 - 2011 Eric Van Dewoestine * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.eclim.installer.step; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.io.BufferedInputStream; import java.io.File; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.DefaultTableModel; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.SystemUtils; import org.eclim.installer.URLProgressInputStream; import org.eclim.installer.step.command.Command; import org.eclim.installer.step.command.InstallCommand; import org.eclim.installer.step.command.ListCommand; import org.eclim.installer.step.command.OutputHandler; import org.eclim.installer.step.command.UninstallCommand; import org.eclim.installer.theme.DesertBlue; import org.formic.Installer; import org.formic.util.Extractor; import org.formic.util.dialog.gui.GuiDialogs; import org.formic.wizard.step.gui.InstallStep; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import com.jgoodies.looks.plastic.PlasticTheme; import foxtrot.Worker; /** * Step which installs necessary third party eclipse plugins. * * @author Eric Van Dewoestine */ public class EclipsePluginsStep extends InstallStep implements OutputHandler { private static final Logger logger = LoggerFactory.getLogger(EclipsePluginsStep.class); private static final String BEGIN_TASK = "beginTask"; private static final String PREPARE_TASK = "prepare"; private static final String SUB_TASK = "subTask"; private static final String INTERNAL_WORKED = "worked"; private static final String SET_TASK_NAME = "setTaskName"; private static final String FEATURE = " Feature: "; private static final String DEPENDENCIES = "/resources/dependencies.xml"; private static final Color ERROR_COLOR = new Color(255, 201, 201); private String taskName = ""; private JPanel stepPanel; private JPanel featuresPanel; private JLabel messageLabel; private ImageIcon errorIcon; private DefaultTableModel tableModel; private List<Dependency> dependencies; private PlasticTheme theme; private int overallProgressStep; private String primaryUpdateSite; /** * Constructs this step. */ public EclipsePluginsStep(String name, Properties properties) { super(name, properties); } /** * {@inheritDoc} * @see org.formic.wizard.step.GuiStep#init() */ public Component init() { theme = new DesertBlue(); stepPanel = (JPanel)super.init(); stepPanel.setBorder(null); JPanel panel = new JPanel(); panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS)); messageLabel = new JLabel(); messageLabel.setPreferredSize(new Dimension(25, 25)); panel.add(messageLabel); panel.add(stepPanel); panel.setBorder(BorderFactory.createEmptyBorder(25, 25, 10, 25)); return panel; } private void setMessage(String message) { if(errorIcon == null){ errorIcon = new ImageIcon(Installer.getImage("form.error.icon")); } if(message != null){ messageLabel.setIcon(errorIcon); messageLabel.setText(message); }else{ messageLabel.setIcon(null); messageLabel.setText(null); } } /** * Invoked when this step is displayed in the gui. */ public void displayed() { setBusy(true); setPreviousEnabled(false); try{ // reset these two to account for re-entry into this step. overallProgress.setValue(0); taskLabel.setText(""); taskProgress.setValue(0); // handle step re-entry. taskProgress.setIndeterminate(true); if (featuresPanel != null){ stepPanel.remove(featuresPanel); } EclipseInfo info = (EclipseInfo)Worker.post(new foxtrot.Task(){ public Object run() throws Exception { overallLabel.setText( "Installing eclim installer feature (may take a few moments)."); if (installInstallerPlugin()){ EclipseInfo info = new EclipseInfo(getDependencies()); return info; } return null; } }); // should only occur on shutdown. if (info != null){ dependencies = info.getDependencies(); if(dependencies.size() == 0){ overallProgress.setMaximum(1); overallProgress.setValue(1); overallLabel.setText("All third party plugins are up to date."); taskProgress.setMaximum(1); taskProgress.setValue(1); taskLabel.setText(""); }else{ tableModel = new DefaultTableModel(); tableModel.addColumn("Feature"); tableModel.addColumn("Version"); tableModel.addColumn("Install / Upgrade"); JTable table = new JTable(tableModel); table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); table.setDefaultRenderer(Object.class, new DependencyCellRenderer()); table.getSelectionModel().addListSelectionListener( new DependencySelectionListener()); featuresPanel = new JPanel(new BorderLayout()); featuresPanel.setAlignmentX(0.0f); JPanel container = new JPanel(new BorderLayout()); container.add(table, BorderLayout.CENTER); JScrollPane scrollPane = new JScrollPane(container); scrollPane.setAlignmentX(0.0f); for (Dependency dependency : dependencies){ String version = dependency.getFeatureVersion(); String manual = ""; if (version == null){ manual = " (Manual)"; version = dependency.getVersion(); } tableModel.addRow(new Object[]{ dependency.getId(), version, (dependency.isUpgrade() ? "Upgrade" : "Install") + manual, }); } JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); buttons.setAlignmentX(0.0f); JButton skipButton = new JButton(new SkipPluginsAction()); JButton installButton = new JButton(new InstallPluginsAction(skipButton)); buttons.add(installButton); buttons.add(skipButton); featuresPanel.add(scrollPane, BorderLayout.CENTER); featuresPanel.add(buttons, BorderLayout.SOUTH); stepPanel.add(featuresPanel); overallProgress.setValue(0); overallLabel.setText(""); taskProgress.setValue(0); taskLabel.setText(""); } } }catch(Exception e){ setError(e); }finally{ setValid(dependencies != null && dependencies.size() == 0); setBusy(false); setPreviousEnabled(true); taskProgress.setIndeterminate(false); } } /** * Installs the eclipse plugin used to install eclipse features. */ private boolean installInstallerPlugin() throws Exception { File updateDir = Installer.tempDir("update"); Extractor.extractResource("/files/installer-site.zip", updateDir); String url = "file://" + updateDir; Command command = new InstallCommand( null, url, "org.eclim.installer", "org.eclipse.equinox.p2.director"); try{ command.start(); command.join(); if(command.getReturnCode() != 0){ if (command.isShutdown()){ return false; } RuntimeException re = new RuntimeException( "error: " + command.getErrorMessage() + " out: " + command.getResult()); logger.warn( "Error installing eclim installer feature, " + "attempting uninstall/reinstall."); // attempt to uninstall the feature Command uninstall = new UninstallCommand( null, url, "org.eclim.installer", "org.eclipse.equinox.p2.director"); uninstall.start(); uninstall.join(); // now try to install again command = new InstallCommand( null, url, "org.eclim.installer", "org.eclipse.equinox.p2.director"); command.start(); command.join(); if(command.getReturnCode() != 0){ if (command.isShutdown()){ return false; } throw re; } } Installer.getProject().setProperty("eclim.feature.installed", "true"); Installer.getProject().setProperty("eclim.feature.location", url); }finally{ command.destroy(); } return true; } /** * Gets a list of required dependencies based on the chosen set of eclim * features to be installed. * * @return List of dependencies. */ private List<Dependency> getDependencies() throws Exception { Document document = DocumentBuilderFactory.newInstance() .newDocumentBuilder().parse( EclipsePluginsStep.class.getResource(DEPENDENCIES).toString()); primaryUpdateSite = document.getDocumentElement().getAttribute("primary"); // determine which dependencies are required ArrayList<Dependency> dependencies = new ArrayList<Dependency>(); String[] features = Installer.getContext().getKeysByPrefix("featureList"); Arrays.sort(features, new FeatureNameComparator()); for (int ii = 0; ii < features.length; ii++){ Boolean enabled = (Boolean)Installer.getContext().getValue(features[ii]); String name = features[ii].substring(features[ii].indexOf('.') + 1); // check if the enabled eclim feature has any dependencies. if(enabled.booleanValue()){ Element featureNode = document.getElementById(name); if(featureNode != null){ NodeList nodes = featureNode.getElementsByTagName("dependency"); // parse out all the possible dependencies for(int jj = 0; jj < nodes.getLength(); jj++){ Element node = (Element)nodes.item(jj); ArrayList<String> sites = new ArrayList<String>(); NodeList siteList = node.getElementsByTagName("site"); for(int kk = 0; kk < siteList.getLength(); kk++){ Element site = (Element)siteList.item(kk); sites.add(site.getAttribute("url")); } dependencies.add(new Dependency( node.getAttribute("id"), node.getAttribute("version"), sites.toArray(new String[sites.size()]))); } } } } return dependencies; } public void process(final String line) { SwingUtilities.invokeLater(new Runnable(){ public void run(){ if (line.startsWith(BEGIN_TASK)){ String l = line.substring(BEGIN_TASK.length() + 2); double work = Double.parseDouble( l.substring(l.indexOf('=') + 1, l.indexOf(' '))); taskProgress.setIndeterminate(false); taskProgress.setMaximum((int)work); taskProgress.setValue(0); }else if(line.startsWith(PREPARE_TASK)){ taskLabel.setText(line.substring(PREPARE_TASK.length() + 1).trim()); }else if(line.startsWith(SUB_TASK)){ taskLabel.setText( taskName + line.substring(SUB_TASK.length() + 1).trim()); }else if(line.startsWith(INTERNAL_WORKED)){ double worked = Double.parseDouble( line.substring(INTERNAL_WORKED.length() + 2)); taskProgress.setValue((int)worked); overallProgress.setValue( overallProgressStep + (int)(taskProgress.getPercentComplete() * 100)); }else if(line.startsWith(SET_TASK_NAME)){ taskName = line.substring(SET_TASK_NAME.length() + 1).trim() + ' '; } } }); } private class InstallPluginsAction extends AbstractAction { private static final long serialVersionUID = 1L; private JButton skipButton; public InstallPluginsAction(JButton skipButton) { super("Install Features"); this.skipButton = skipButton; } public void actionPerformed(ActionEvent e) { ((JButton)e.getSource()).setEnabled(false); setPreviousEnabled(true); try{ Boolean successful = (Boolean)Worker.post(new foxtrot.Task(){ public Object run() throws Exception { // check if any of the features cannot be installed. for (Dependency dependency : dependencies){ Feature feature = dependency.getFeature(); if (feature != null) { if (feature.getSite() == null){ String installLog = FilenameUtils.concat( SystemUtils.JAVA_IO_TMPDIR, "install.log"); GuiDialogs.showWarning(Installer.getString( "eclipsePlugins.install.features.site.not.found", installLog)); return Boolean.FALSE; }else if (!feature.getSite().canWrite()){ GuiDialogs.showWarning(Installer.getString( "eclipsePlugins.install.features.permission.denied")); return Boolean.FALSE; } } } overallProgress.setMaximum(dependencies.size() * 100); overallProgress.setValue(0); int removeIndex = 0; for (Iterator<Dependency> ii = dependencies.iterator(); ii.hasNext();){ Dependency dependency = ii.next(); if (dependency.getFeatureUrl() != null){ if(!dependency.isUpgrade()){ overallLabel.setText("Installing feature: " + dependency.getId() + '-' + dependency.getVersion()); }else{ overallLabel.setText("Updating feature: " + dependency.getId() + '-' + dependency.getVersion()); } taskProgress.setValue(0); taskProgress.setIndeterminate(true); Command command = new InstallCommand( EclipsePluginsStep.this, dependency.getFeatureUrl(), dependency.getId()); try{ command.start(); command.join(); if(command.getReturnCode() != 0){ if (command.isShutdown()){ return Boolean.TRUE; } throw new RuntimeException( "error: " + command.getErrorMessage() + " out: " + command.getResult()); } }finally{ command.destroy(); } ii.remove(); tableModel.removeRow(removeIndex); }else{ removeIndex++; } overallProgressStep += 100; overallProgress.setValue(overallProgressStep); } taskLabel.setText(""); taskProgress.setValue(taskProgress.getMaximum()); overallProgress.setValue(overallProgress.getMaximum()); if(dependencies.size() > 0){ GuiDialogs.showWarning(Installer.getString("eclipsePlugins.manual")); } return Boolean.TRUE; } }); if(successful.booleanValue()){ overallProgress.setValue(overallProgress.getMaximum()); overallLabel.setText(Installer.getString("install.done")); taskProgress.setValue(taskProgress.getMaximum()); taskLabel.setText(Installer.getString("install.done")); setBusy(false); setValid(true); taskProgress.setIndeterminate(false); skipButton.setEnabled(false); } }catch(Exception ex){ setError(ex); } } } private class SkipPluginsAction extends AbstractAction { private static final long serialVersionUID = 1L; public SkipPluginsAction() { super("Skip"); } public void actionPerformed(ActionEvent e) { if(GuiDialogs.showConfirm(Installer.getString("eclipsePlugins.skip"))){ setValid(true); } } } private class DependencyCellRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 1L; public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component component = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); Feature feature = ((Dependency)dependencies.get(row)).getFeature(); if (feature != null && (feature.getSite() == null || !feature.getSite().canWrite())) { component.setBackground(isSelected ? theme.getMenuItemSelectedBackground() : ERROR_COLOR); component.setForeground(isSelected ? ERROR_COLOR : Color.BLACK); }else{ component.setBackground(isSelected ? theme.getMenuItemSelectedBackground() : Color.WHITE); component.setForeground(isSelected ? theme.getMenuItemSelectedForeground() : theme.getMenuForeground()); } return component; } } /** * Mouse listener for the feature list. */ private class DependencySelectionListener implements ListSelectionListener { /** * {@inheritDoc} * @see ListSelectionListener#valueChanged(ListSelectionEvent) */ public void valueChanged(ListSelectionEvent e) { ListSelectionModel model = (ListSelectionModel)e.getSource(); int index = model.getMinSelectionIndex(); Feature feature = index >= 0 ? ((Dependency)dependencies.get(index)).getFeature() : null; if (feature != null) { if (feature.getSite() == null){ setMessage(Installer.getString("eclipsePlugins.upgrade.site.not.found")); }else if (!feature.getSite().canWrite()){ setMessage(Installer.getString( "eclipsePlugins.upgrade.permission.denied")); } }else{ setMessage(null); } } } private class EclipseInfo { private Map<String,Feature> features; private List<Dependency> dependencies; private Map<String,String> availableFeatures; /** * Contstruct a new EclipseInfo instance containing installed features and * required dependencies. * * @param List of dependencies. */ public EclipseInfo(List<Dependency> dependencies) throws Exception { this.dependencies = dependencies; this.features = new HashMap<String,Feature>(); this.availableFeatures = new HashMap<String,String>(); overallProgress.setMaximum(5); overallProgress.setValue(1); // run eclipse to get a list of existing installed features overallLabel.setText("Analyzing installed features..."); Command command = new ListCommand(new OutputHandler(){ public void process(String line){ logger.info(line); if(line.startsWith(FEATURE)){ String[] attrs = StringUtils.split(line.substring(FEATURE.length())); File site = null; try{ site = new File(new URL(attrs[2]).getFile()); }catch(Exception e){ logger.error("Failed to parse feature: " + line, e); } features.put(attrs[0], new Feature(attrs[1], site)); } } }); try{ command.start(); command.join(); if(command.getReturnCode() != 0){ if (command.isShutdown()){ return; } throw new RuntimeException( "error: " + command.getErrorMessage() + " out: " + command.getResult()); } }finally{ command.destroy(); } // load up available features from the primary update site. overallProgress.setValue(2); overallLabel.setText( "Loading available features from the primary update site..."); BufferedInputStream in = null; SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); // download compositeContent.jar to determine location on content.jar final String[] location = new String[1]; try{ overallProgress.setValue(3); System.out.println("Downloading compositeContent.jar"); taskLabel.setText("Downloading compositeContent.jar"); in = new BufferedInputStream( new URLProgressInputStream( taskProgress, new URL(primaryUpdateSite + "compositeContent.jar").openConnection())); JarInputStream jin = new JarInputStream(in); JarEntry entry = jin.getNextJarEntry(); while (!entry.getName().equals("compositeContent.xml")){ entry = jin.getNextJarEntry(); } parser.parse(jin, new DefaultHandler(){ public void startElement( String uri, String localName, String qName, Attributes attributes) throws SAXException { if(qName.equals("child")){ location[0] = attributes.getValue("location"); } } }); }finally{ IOUtils.closeQuietly(in); } try{ overallProgress.setValue(4); System.out.println("Downloading " + location[0] + "/content.jar"); taskLabel.setText("Downloading " + location[0] + "/content.jar"); in = new BufferedInputStream( new URLProgressInputStream(taskProgress, new URL( primaryUpdateSite + location[0] + "/content.jar").openConnection())); JarInputStream jin = new JarInputStream(in); JarEntry entry = jin.getNextJarEntry(); while (!entry.getName().equals("content.xml")){ entry = jin.getNextJarEntry(); } parser.parse(jin, new DefaultHandler(){ public void startElement( String uri, String localName, String qName, Attributes attributes) throws SAXException { if(qName.equals("unit")){ String id = attributes.getValue("id"); if (id.endsWith(".feature.group")){ String version = attributes.getValue("version"); String name = id.substring(0, id.length() - 14); availableFeatures.put(name, version); } } } }); }finally{ IOUtils.closeQuietly(in); } overallProgress.setValue(5); filterDependencies(); } public List<Dependency> getDependencies() { return dependencies; } /** * Filters the supplied list of dependencies to determine which are already * installed or need to be upgraded. */ private void filterDependencies() throws Exception { for (Iterator<Dependency> ii = dependencies.iterator(); ii.hasNext();){ Dependency dependency = ii.next(); Feature feature = (Feature)features.get(dependency.getId()); boolean include = dependency.eval(feature, availableFeatures); if (!include){ ii.remove(); } } } } private class Dependency { private String id; private String[] sites; private boolean upgrade; private Feature feature; private String version; private String featureUrl; private String featureVersion; public Dependency(String id, String version, String[] sites) { this.id = id; this.version = version; this.sites = sites; } public String getId() { return id; } public String getVersion() { return version; } public boolean isUpgrade() { return upgrade; } public Feature getFeature() { return feature; } public String getFeatureUrl() { return featureUrl; } public String getFeatureVersion() { return featureVersion; } public boolean eval(Feature feature, Map<String,String> availableFeatures) throws Exception { this.feature = feature; if(feature != null){ int result = compareVersions(this.version, feature.getVersion()); if (result >= 0){ return false; } this.upgrade = true; } String[] urlVersion = findUrlVersion(availableFeatures); this.featureUrl = urlVersion[0]; this.featureVersion = urlVersion[1]; return true; } private int compareVersions(String v1, String v2) { String[] dv = StringUtils.split(v1, "."); String[] fv = StringUtils.split(v2, "."); for (int ii = 0; ii < dv.length; ii++){ int dp = Integer.parseInt(dv[ii]); int fp = Integer.parseInt(fv[ii]); if(dp != fp){ return fp - dp; } } return 0; } private String[] findUrlVersion(Map<String,String> availableFeatures) throws Exception { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); String resolvedUrl = null; String resolvedVersion = null; for(int ii = 0; ii < sites.length; ii++){ if(sites[ii].equals(primaryUpdateSite)){ if(availableFeatures.containsKey(this.id)){ String version = (String)availableFeatures.get(this.id); return new String[]{primaryUpdateSite, version}; } } BufferedInputStream in = null; try{ in = new BufferedInputStream( new URL(sites[ii] + "site.xml").openStream()); Document document = builder.parse(in); NodeList nodes = document.getElementsByTagName("feature"); for(int jj = 0; jj < nodes.getLength(); jj++){ Element feature = (Element)nodes.item(jj); if (this.id.equals(feature.getAttribute("id"))){ String fv = feature.getAttribute("version"); Matcher matcher = Feature.VERSION.matcher(fv); matcher.find(); fv = matcher.group(1); int result = -1; if(resolvedVersion == null){ result = compareVersions(this.version, fv); }else{ result = compareVersions(resolvedVersion, fv); } if (result >= 0){ resolvedUrl = sites[ii]; resolvedVersion = feature.getAttribute("version"); } } } if(resolvedUrl != null){ return new String[]{resolvedUrl, resolvedVersion}; } }finally{ IOUtils.closeQuietly(in); } } return new String[]{null, null}; } } private static class Feature { public static final Pattern VERSION = Pattern.compile("^(\\d+\\.\\d+\\.\\d+)(\\..*)?"); private String version; private File site; public Feature(String version, File site) { this.site = site; Matcher matcher = VERSION.matcher(version); matcher.find(); this.version = matcher.group(1); } public String getVersion() { return version; } public File getSite() { return this.site; } } private static class FeatureNameComparator implements Comparator<String> { private static ArrayList<String> NAMES = new ArrayList<String>(); static{ NAMES.add("featureList.jdt"); NAMES.add("featureList.ant"); NAMES.add("featureList.maven"); NAMES.add("featureList.wst"); NAMES.add("featureList.cdt"); NAMES.add("featureList.pdt"); NAMES.add("featureList.python"); } public int compare(String ob1, String ob2) { return NAMES.indexOf(ob1) - NAMES.indexOf(ob2); } } }