001/*
002 * Copyright 2010-2015 Institut Pasteur.
003 * 
004 * This file is part of Icy.
005 * 
006 * Icy is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 * 
011 * Icy is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
014 * GNU General Public License for more details.
015 * 
016 * You should have received a copy of the GNU General Public License
017 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
018 */
019package icy.plugin;
020
021import java.net.URL;
022import java.util.ArrayList;
023import java.util.EventListener;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027
028import javax.swing.event.EventListenerList;
029
030import icy.file.FileUtil;
031import icy.gui.dialog.ConfirmDialog;
032import icy.gui.frame.progress.AnnounceFrame;
033import icy.gui.frame.progress.CancelableProgressFrame;
034import icy.gui.frame.progress.DownloadFrame;
035import icy.gui.frame.progress.FailedAnnounceFrame;
036import icy.gui.frame.progress.SuccessfullAnnounceFrame;
037import icy.main.Icy;
038import icy.network.NetworkUtil;
039import icy.network.URLUtil;
040import icy.plugin.PluginDescriptor.PluginIdent;
041import icy.preferences.RepositoryPreferences.RepositoryInfo;
042import icy.system.IcyExceptionHandler;
043import icy.system.thread.ThreadUtil;
044import icy.update.Updater;
045import icy.util.StringUtil;
046import icy.util.XMLUtil;
047import icy.util.ZipUtil;
048
049/**
050 * @author Stephane
051 */
052public class PluginInstaller implements Runnable
053{
054    public static interface PluginInstallerListener extends EventListener
055    {
056        public void pluginInstalled(PluginDescriptor plugin, boolean success);
057
058        public void pluginRemoved(PluginDescriptor plugin, boolean success);
059    }
060
061    private static class PluginInstallInfo
062    {
063        // final PluginRepositoryLoader loader;
064        final PluginDescriptor plugin;
065        final boolean showProgress;
066
067        public PluginInstallInfo(PluginDescriptor plugin, boolean showProgress)
068        {
069            super();
070
071            this.plugin = plugin;
072            this.showProgress = showProgress;
073        }
074
075        @Override
076        public boolean equals(Object obj)
077        {
078            if (obj instanceof PluginInstallInfo)
079                return ((PluginInstallInfo) obj).plugin.equals(plugin);
080
081            return super.equals(obj);
082        }
083
084        @Override
085        public int hashCode()
086        {
087            return plugin.hashCode();
088        }
089    }
090
091    private static final String ERROR_DOWNLOAD = "Error while downloading ";
092    private static final String ERROR_SAVE = "Error while saving";
093    // private static final String INSTALL_CANCELED = "Plugin installation canceled by user.";
094
095    /**
096     * static class
097     */
098    private static final PluginInstaller instance = new PluginInstaller();
099
100    /**
101     * plugin(s) to install FIFO
102     */
103    private final List<PluginInstallInfo> installFIFO;
104    /**
105     * plugin(s) to delete FIFO
106     */
107    private final List<PluginInstallInfo> removeFIFO;
108
109    /**
110     * listeners
111     */
112    private final EventListenerList listeners;
113
114    /**
115     * internals
116     */
117    private final List<PluginDescriptor> installingPlugins;
118    private final List<PluginDescriptor> desinstallingPlugin;
119
120    /**
121     * static class
122     */
123    private PluginInstaller()
124    {
125        super();
126
127        installFIFO = new ArrayList<PluginInstallInfo>();
128        removeFIFO = new ArrayList<PluginInstallInfo>();
129
130        listeners = new EventListenerList();
131
132        installingPlugins = new ArrayList<PluginDescriptor>();
133        desinstallingPlugin = new ArrayList<PluginDescriptor>();
134
135        // launch installer thread
136        new Thread(this, "Plugin installer").start();
137    }
138
139    /**
140     * Return true if install or desinstall is possible
141     */
142    private static boolean isEnabled()
143    {
144        return !PluginLoader.isJCLDisabled();
145    }
146
147    /**
148     * Install a plugin (asynchronous)
149     * 
150     * @param plugin
151     *        the plugin to install
152     * @param showProgress
153     *        show a progress frame during process
154     */
155    public static void install(PluginDescriptor plugin, boolean showProgress)
156    {
157        if ((plugin != null) && isEnabled())
158        {
159            if (!NetworkUtil.hasInternetAccess())
160            {
161                final String text = "Cannot install '" + plugin.getName()
162                        + "' plugin : you are not connected to Internet.";
163
164                if (Icy.getMainInterface().isHeadLess())
165                    System.err.println(text);
166                else
167                    new FailedAnnounceFrame(text, 10);
168
169                return;
170            }
171
172            synchronized (instance.installFIFO)
173            {
174                instance.installFIFO.add(new PluginInstallInfo(plugin, showProgress));
175            }
176        }
177    }
178
179    /**
180     * return true if PluginInstaller is processing
181     */
182    public static boolean isProcessing()
183    {
184        return isInstalling() || isDesinstalling();
185    }
186
187    /**
188     * return a copy of the install FIFO
189     */
190    public static ArrayList<PluginInstallInfo> getInstallFIFO()
191    {
192        synchronized (instance.installFIFO)
193        {
194            return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.installFIFO);
195        }
196    }
197
198    /**
199     * Wait while installer is installing plugin.
200     */
201    public static void waitInstall()
202    {
203        while (isInstalling())
204            ThreadUtil.sleep(100);
205    }
206
207    /**
208     * return true if PluginInstaller is installing plugin(s)
209     */
210    public static boolean isInstalling()
211    {
212        return !instance.installFIFO.isEmpty() || !instance.installingPlugins.isEmpty();
213    }
214
215    /**
216     * return true if 'plugin' is in the install FIFO
217     */
218    public static boolean isWaitingForInstall(PluginDescriptor plugin)
219    {
220        synchronized (instance.installFIFO)
221        {
222            for (PluginInstallInfo info : instance.installFIFO)
223                if (plugin == info.plugin)
224                    return true;
225        }
226
227        return false;
228    }
229
230    /**
231     * return true if specified plugin is currently being installed or will be installed
232     */
233    public static boolean isInstallingPlugin(PluginDescriptor plugin)
234    {
235        return (instance.installingPlugins.indexOf(plugin) != -1) || isWaitingForInstall(plugin);
236    }
237
238    /**
239     * Uninstall a plugin (asynchronous)
240     * 
241     * @param plugin
242     *        the plugin to uninstall
243     * @param showConfirm
244     *        show a confirmation dialog
245     * @param showProgress
246     *        show a progress frame during process
247     */
248    public static void desinstall(PluginDescriptor plugin, boolean showConfirm, boolean showProgress)
249    {
250        if ((plugin != null) && isEnabled())
251        {
252            if (showConfirm)
253            {
254                // get local plugins which depend from the plugin we want to delete
255                final List<PluginDescriptor> dependants = getLocalDependenciesFrom(plugin);
256
257                String message = "<html>";
258
259                if (!dependants.isEmpty())
260                {
261                    message = message + "The following plugin(s) won't work anymore :<br>";
262
263                    for (PluginDescriptor depPlug : dependants)
264                        message = message + depPlug.getName() + " " + depPlug.getVersion() + "<br>";
265
266                    message = message + "<br>";
267                }
268
269                message = message + "Are you sure you want to remove '" + plugin.getName() + " " + plugin.getVersion()
270                        + "' ?</html>";
271
272                if (ConfirmDialog.confirm(message))
273                {
274                    synchronized (instance.removeFIFO)
275                    {
276                        instance.removeFIFO.add(new PluginInstallInfo(plugin, showConfirm));
277                    }
278                }
279            }
280            else
281            {
282                synchronized (instance.removeFIFO)
283                {
284                    instance.removeFIFO.add(new PluginInstallInfo(plugin, showProgress));
285                }
286            }
287        }
288    }
289
290    /**
291     * @deprecated Use {@link #desinstall(PluginDescriptor, boolean, boolean)} instead.
292     */
293    @Deprecated
294    public static void desinstall(PluginDescriptor plugin, boolean showConfirm)
295    {
296        desinstall(plugin, showConfirm, showConfirm);
297    }
298
299    /**
300     * return a copy of the remove FIFO
301     */
302    public static ArrayList<PluginInstallInfo> getRemoveFIFO()
303    {
304        synchronized (instance.removeFIFO)
305        {
306            return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.removeFIFO);
307        }
308    }
309
310    /**
311     * Wait while installer is removing plugin.
312     */
313    public static void waitDesinstall()
314    {
315        while (isDesinstalling())
316            ThreadUtil.sleep(100);
317    }
318
319    /**
320     * return true if PluginInstaller is desinstalling plugin(s)
321     */
322    public static boolean isDesinstalling()
323    {
324        return !instance.removeFIFO.isEmpty() || !instance.desinstallingPlugin.isEmpty();
325    }
326
327    /**
328     * return true if 'plugin' is in the remove FIFO
329     */
330    public static boolean isWaitingForDesinstall(PluginDescriptor plugin)
331    {
332        synchronized (instance.removeFIFO)
333        {
334            for (PluginInstallInfo info : instance.removeFIFO)
335                if (plugin == info.plugin)
336                    return true;
337        }
338
339        return false;
340    }
341
342    /**
343     * return true if specified plugin is currently being desinstalled or will be desinstalled
344     */
345    public static boolean isDesinstallingPlugin(PluginDescriptor plugin)
346    {
347        return (instance.desinstallingPlugin.indexOf(plugin) != -1) || isWaitingForDesinstall(plugin);
348    }
349
350    @Override
351    public void run()
352    {
353        while (!Thread.interrupted())
354        {
355            // process installations
356            if (!installFIFO.isEmpty())
357            {
358                // so list has sometime to fill-up
359                ThreadUtil.sleep(200);
360
361                do
362                    installInternal();
363                while (!installFIFO.isEmpty());
364            }
365
366            // process deletions
367            while (!removeFIFO.isEmpty())
368            {
369                // so list has sometime to fill-up
370                ThreadUtil.sleep(200);
371
372                do
373                    desinstallInternal();
374                while (!removeFIFO.isEmpty());
375            }
376
377            ThreadUtil.sleep(100);
378        }
379    }
380
381    /**
382     * Backup specified plugin if it already exists.<br>
383     * Return an empty string if no error else return error message
384     */
385    private static String backup(PluginDescriptor plugin)
386    {
387        boolean ok;
388
389        // backup JAR, XML and image files
390        ok = Updater.backup(plugin.getJarFilename()) && Updater.backup(plugin.getXMLFilename())
391                && Updater.backup(plugin.getIconFilename()) && Updater.backup(plugin.getImageFilename());
392
393        if (!ok)
394            return "Can't backup plugin '" + plugin.getName() + "'";
395
396        return "";
397    }
398
399    /**
400     * Return an empty string if no error else return error message
401     */
402    private static String downloadAndSavePlugin(PluginDescriptor plugin, DownloadFrame taskFrame)
403    {
404        String result;
405
406        if (taskFrame != null)
407            taskFrame.setMessage("Downloading " + plugin);
408
409        // ensure descriptor is loaded
410        plugin.loadDescriptor();
411
412        final RepositoryInfo repos = plugin.getRepository();
413        final String login;
414        final String pass;
415
416        // use authentication (repos should not be null at this point)
417        if (repos.isAuthenticationEnabled())
418        {
419            login = repos.getLogin();
420            pass = repos.getPassword();
421        }
422        else
423        {
424            login = null;
425            pass = null;
426        }
427
428        // try to build the final path using base repository address and plugin relative address
429        // (useful for local repository)
430        URL url;
431        final String basePath = FileUtil.getDirectory(repos.getLocation());
432        // download and save JAR file
433        url = URLUtil.buildURL(basePath, plugin.getJarUrl());
434        result = downloadAndSave(url, plugin.getJarFilename(), login, pass, true, taskFrame);
435        if (!StringUtil.isEmpty(result))
436            return result;
437
438        // verify JAR file is not corrupted
439        if (!ZipUtil.isValid(plugin.getJarFilename(), false))
440            return "Downloaded JAR file '" + plugin.getJarFilename() + "' is corrupted !";
441
442        // download and save XML file
443        url = URLUtil.buildURL(basePath, plugin.getUrl());
444        result = downloadAndSave(url, plugin.getXMLFilename(), login, pass, true, taskFrame);
445        if (!StringUtil.isEmpty(result))
446            return result;
447
448        // verify XML file is not corrupted
449        if (XMLUtil.loadDocument(plugin.getXMLFilename()) == null)
450            return "Downloaded XML file '" + plugin.getXMLFilename() + "' is corrupted !";
451
452        // download and save icon & image files
453        if (!StringUtil.isEmpty(plugin.getIconUrl()))
454        {
455            url = URLUtil.buildURL(basePath, plugin.getIconUrl());
456            downloadAndSave(url, plugin.getIconFilename(), login, pass, false, taskFrame);
457        }
458        if (!StringUtil.isEmpty(plugin.getImageUrl()))
459        {
460            url = URLUtil.buildURL(basePath, plugin.getImageUrl());
461            downloadAndSave(url, plugin.getImageFilename(), login, pass, false, taskFrame);
462        }
463
464        return "";
465    }
466
467    /**
468     * Return an empty string if no error else return error message
469     */
470    private static String downloadAndSave(URL downloadPath, String savePath, String login, String pass,
471            boolean displayError, DownloadFrame downloadFrame)
472    {
473        if (downloadFrame != null)
474            downloadFrame.setPath(FileUtil.getFileName(savePath));
475
476        // load data
477        final byte[] data = NetworkUtil.download(downloadPath, login, pass, downloadFrame, displayError);
478        if (data == null)
479            return ERROR_DOWNLOAD + downloadPath.toString();
480
481        // save data
482        if (!FileUtil.save(savePath, data, displayError))
483        {
484            System.err.println("Can't write '" + savePath + "' !");
485            System.err.println("File may be locked or you don't own the rights to write files here.");
486            return ERROR_SAVE + savePath;
487        }
488
489        return null;
490    }
491
492    private static boolean deletePlugin(PluginDescriptor plugin)
493    {
494        if (!FileUtil.delete(plugin.getJarFilename(), false))
495        {
496            System.err.println("Can't delete '" + plugin.getJarFilename() + "' file !");
497            // fatal error
498            return false;
499        }
500
501        if (FileUtil.exists(plugin.getXMLFilename()))
502            if (!FileUtil.delete(plugin.getXMLFilename(), false))
503                System.err.println("Can't delete '" + plugin.getXMLFilename() + "' file !");
504
505        FileUtil.delete(plugin.getImageFilename(), false);
506        FileUtil.delete(plugin.getIconFilename(), false);
507
508        return true;
509    }
510
511    /**
512     * Fill list with local dependencies (plugins) of specified plugin
513     */
514    public static void getLocalDependenciesOf(List<PluginDescriptor> result, PluginDescriptor plugin)
515    {
516        // load plugin descriptor informations if not yet done
517        plugin.loadDescriptor();
518
519        for (PluginIdent ident : plugin.getRequired())
520        {
521            // already in our dependences ? --> pass to the next one
522            if (PluginDescriptor.getPlugin(result, ident, true) != null)
523                continue;
524
525            // find local dependent plugin
526            final PluginDescriptor dep = PluginLoader.getPlugin(ident, true);
527
528            // dependence found ?
529            if (dep != null)
530            {
531                // and add it to list
532                PluginDescriptor.addToList(result, dep);
533                // search its dependencies too
534                getLocalDependenciesOf(result, dep);
535            }
536        }
537    }
538
539    /**
540     * Return local plugins list which depend from the specified list of plugins.
541     */
542    public static List<PluginDescriptor> getLocalDependenciesFrom(List<PluginDescriptor> plugins)
543    {
544        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
545
546        for (PluginDescriptor plugin : plugins)
547            getLocalDependenciesFrom(plugin, result);
548
549        return result;
550    }
551
552    /**
553     * Return local plugins list which depend from the specified plugin.
554     */
555    public static List<PluginDescriptor> getLocalDependenciesFrom(PluginDescriptor plugin)
556    {
557        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
558
559        getLocalDependenciesFrom(plugin, result);
560
561        return result;
562    }
563
564    /**
565     * Return local plugins list which depend from the specified plugin.
566     */
567    private static void getLocalDependenciesFrom(PluginDescriptor plugin, List<PluginDescriptor> result)
568    {
569        for (PluginDescriptor curPlug : PluginLoader.getPlugins(false))
570            // require specified plugin ?
571            if (curPlug.requires(plugin))
572                PluginDescriptor.addToList(result, curPlug);
573    }
574
575    /**
576     * Fill list with 'sources' dependencies of specified plugin
577     */
578    private static void getLocalDependenciesOf(List<PluginDescriptor> result, List<PluginDescriptor> sources,
579            PluginDescriptor plugin)
580    {
581        // load plugin descriptor informations if not yet done
582        plugin.loadDescriptor();
583
584        for (PluginIdent ident : plugin.getRequired())
585        {
586            // already in our dependences ? --> pass to the next one
587            if ((ident == null) || (PluginDescriptor.getPlugin(result, ident, true) != null))
588                continue;
589
590            // find sources dependent plugin
591            final PluginDescriptor dep = PluginDescriptor.getPlugin(sources, ident, true);
592
593            // dependence found ?
594            if (dep != null)
595            {
596                // and add it to list
597                PluginDescriptor.addToList(result, dep);
598                // search its dependencies too
599                getLocalDependenciesOf(result, sources, dep);
600            }
601        }
602    }
603
604    /**
605     * Reorder the list so needed dependencies comes first in list
606     */
607    public static List<PluginDescriptor> orderDependencies(List<PluginDescriptor> plugins)
608    {
609        final List<PluginDescriptor> sources = new ArrayList<PluginDescriptor>(plugins);
610        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();
611
612        while (sources.size() > 0)
613        {
614            final List<PluginDescriptor> deps = new ArrayList<PluginDescriptor>();
615
616            getLocalDependenciesOf(result, sources, sources.get(0));
617
618            // add last to first dep
619            for (int i = deps.size() - 1; i >= 0; i--)
620                PluginDescriptor.addToList(result, deps.get(i));
621
622            // then add plugin
623            PluginDescriptor.addToList(result, sources.get(0));
624
625            // remove tested plugin and its dependencies from source
626            sources.removeAll(result);
627        }
628
629        return result;
630    }
631
632    /**
633     * Resolve dependencies for specified plugin
634     * 
635     * @param taskFrame
636     */
637    public static boolean getDependencies(PluginDescriptor plugin, List<PluginDescriptor> pluginsToInstall,
638            CancelableProgressFrame taskFrame, boolean showError)
639    {
640        // load plugin descriptor informations if not yet done
641        plugin.loadDescriptor();
642
643        // check dependencies
644        for (PluginIdent ident : plugin.getRequired())
645        {
646            if ((taskFrame != null) && taskFrame.isCancelRequested())
647                return false;
648
649            // should not happen but...
650            if (ident == null)
651                continue;
652
653            // already in our dependencies ? --> pass to the next one
654            if (PluginDescriptor.getPlugin(pluginsToInstall, ident, true) != null)
655                continue;
656
657            final String className = ident.getClassName();
658
659            // get local & online plugin
660            final PluginDescriptor localPlugin = PluginLoader.getPlugin(className);
661            final PluginDescriptor onlinePlugin = PluginRepositoryLoader.getPlugin(className);
662
663            // plugin not yet installed or outdated ?
664            if ((localPlugin == null) || ident.getVersion().isGreater(localPlugin.getVersion()))
665            {
666                // online plugin not found ?
667                if (onlinePlugin == null)
668                {
669                    // error
670                    if (showError)
671                    {
672                        System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :");
673
674                        if (localPlugin == null)
675                            System.err.println("Plugin class '" + ident.getClassName() + " not found !");
676                        else
677                        {
678                            System.err.println(localPlugin.getName() + " " + localPlugin.getVersion() + " installed");
679                            System.err.println("but version " + ident.getVersion() + " or greater needed.");
680                        }
681                    }
682
683                    return false;
684                }
685                // online plugin version incorrect
686                else if (ident.getVersion().isGreater(onlinePlugin.getVersion()))
687                {
688                    // error
689                    if (showError)
690                    {
691                        System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :");
692                        System.err.println(
693                                onlinePlugin.getName() + " " + onlinePlugin.getVersion() + " found in repository");
694                        System.err.println("but version " + ident.getVersion() + " or greater needed.");
695                    }
696
697                    return false;
698                }
699
700                // add to the install list
701                PluginDescriptor.addToList(pluginsToInstall, onlinePlugin);
702                // and check dependencies for this plugin
703                if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError))
704                    return false;
705            }
706            else
707            {
708                // just check if we have update for dependency
709                if ((onlinePlugin != null) && (localPlugin.getVersion().isLower(onlinePlugin.getVersion())))
710                {
711                    // as web site doesn't handle version dependency, we force the update
712
713                    // add to the install list
714                    PluginDescriptor.addToList(pluginsToInstall, onlinePlugin);
715                    // and check dependencies for this plugin
716                    if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError))
717                        return false;
718                }
719            }
720        }
721
722        return true;
723    }
724
725    private void installInternal()
726    {
727        DownloadFrame taskFrame = null;
728
729        try
730        {
731            final List<PluginInstallInfo> infos;
732            boolean showProgress;
733
734            synchronized (installFIFO)
735            {
736                infos = new ArrayList<PluginInstaller.PluginInstallInfo>(installFIFO);
737
738                showProgress = false;
739                for (int i = infos.size() - 1; i >= 0; i--)
740                {
741                    final PluginInstallInfo info = infos.get(i);
742
743                    PluginDescriptor.addToList(installingPlugins, info.plugin);
744                    showProgress |= info.showProgress;
745                }
746
747                installFIFO.clear();
748            }
749
750            if (showProgress && !Icy.getMainInterface().isHeadLess())
751            {
752                taskFrame = new DownloadFrame();
753                taskFrame.setMessage("Initializing...");
754            }
755
756            List<PluginDescriptor> dependencies = new ArrayList<PluginDescriptor>();
757            final Set<PluginDescriptor> pluginsOk = new HashSet<PluginDescriptor>();
758            final Set<PluginDescriptor> pluginsNOk = new HashSet<PluginDescriptor>();
759            final Set<PluginDescriptor> pluginsNewJava = new HashSet<PluginDescriptor>();
760
761            // get dependencies
762            for (int i = installingPlugins.size() - 1; i >= 0; i--)
763            {
764                final PluginDescriptor plugin = installingPlugins.get(i);
765                final String plugDesc = plugin.getName() + " " + plugin.getVersion();
766
767                if (taskFrame != null)
768                {
769                    // cancel requested ?
770                    if (taskFrame.isCancelRequested())
771                        return;
772
773                    taskFrame.setMessage("Checking dependencies for '" + plugDesc + "' ...");
774                }
775
776                // check dependencies
777                if (!getDependencies(plugin, dependencies, taskFrame, true))
778                {
779                    // can't resolve dependencies for this plugin
780                    pluginsNOk.add(plugin);
781                    installingPlugins.remove(i);
782                }
783            }
784
785            // nothing to install
786            if (installingPlugins.isEmpty())
787                return;
788
789            // order dependencies
790            dependencies = orderDependencies(dependencies);
791            // add dependencies at the beginning of the installing list
792            for (PluginDescriptor plugin : dependencies)
793                PluginDescriptor.addToList(installingPlugins, plugin, 0);
794
795            String error = "";
796
797            // clear backup folder
798            FileUtil.delete(Updater.BACKUP_DIRECTORY, true);
799
800            // now we can proceed the installation itself
801            for (PluginDescriptor plugin : installingPlugins)
802            {
803                for (PluginIdent ident : plugin.getRequired())
804                {
805                    // one of the dependencies was not correctly installed ?
806                    if (PluginDescriptor.existInList(pluginsNOk, ident))
807                    {
808                        // we can't install the plugin, continue with the next one
809                        pluginsNOk.add(plugin);
810                        continue;
811                    }
812                }
813
814                final String plugDesc = plugin.getName() + " " + plugin.getVersion();
815
816                if (taskFrame != null)
817                {
818                    // cancel requested ? --> interrupt installation
819                    if (taskFrame.isCancelRequested())
820                        break;
821
822                    taskFrame.setMessage("Installing " + plugDesc + "...");
823                }
824
825                try
826                {
827                    // backup plugin
828                    error = backup(plugin);
829
830                    // backup ok --> install plugin
831                    if (StringUtil.isEmpty(error))
832                    {
833                        error = downloadAndSavePlugin(plugin, taskFrame);
834
835                        // an error occurred ? --> restore
836                        if (!StringUtil.isEmpty(error))
837                            Updater.restore();
838                    }
839                }
840                finally
841                {
842                    // delete backup
843                    FileUtil.delete(Updater.BACKUP_DIRECTORY, true);
844                }
845
846                if (StringUtil.isEmpty(error))
847                    pluginsOk.add(plugin);
848                else
849                {
850                    pluginsNOk.add(plugin);
851                    // print error
852                    System.err.println(error);
853                }
854            }
855
856            // verify installed plugins
857            if (taskFrame != null)
858                taskFrame.setMessage("Verifying plugins...");
859
860            // reload plugin list
861            PluginLoader.reload();
862
863            for (PluginDescriptor plugin : pluginsOk)
864            {
865                error = PluginLoader.verifyPlugin(plugin);
866
867                // send report when we have verification error
868                if (!StringUtil.isEmpty(error))
869                {
870                    final String mess = "Fatal error while loading '" + plugin.getClassName() + "' class from "
871                            + plugin.getJarFilename() + " :\n" + error;
872
873                    // new java version required ?
874                    if (error.contains(PluginLoader.NEWER_JAVA_REQUIRED))
875                    {
876                        // print error in console
877                        System.err.println(mess);
878                        // add to list
879                        pluginsNewJava.add(plugin);
880                    }
881                    else
882                    {
883                        // report error to developer
884                        IcyExceptionHandler.report(plugin, "An error occured while installing the plugin :\n" + error);
885                        // print error
886                        System.err.println(mess);
887                        // add to list
888                        pluginsNOk.add(plugin);
889                    }
890                }
891            }
892
893            // remove all plugins which failed or require new version of Java from OK list
894            pluginsOk.removeAll(pluginsNOk);
895            pluginsOk.removeAll(pluginsNewJava);
896
897            if (!pluginsNOk.isEmpty())
898            {
899                System.err.println();
900                System.err.println("Installation of the following plugin(s) failed:");
901                for (PluginDescriptor plugin : pluginsNOk)
902                {
903                    System.err.println(plugin.getName() + " " + plugin.getVersion());
904                    // notify about installation fails
905                    fireInstalledEvent(plugin, false);
906                }
907                System.err.println();
908            }
909
910            if (!pluginsOk.isEmpty())
911            {
912                System.out.println();
913                System.out.println("The following plugin(s) has been correctly installed:");
914                for (PluginDescriptor plugin : pluginsOk)
915                {
916                    System.out.println(plugin.getName() + " " + plugin.getVersion());
917                    // notify about installation successes
918                    fireInstalledEvent(plugin, true);
919                }
920                System.out.println();
921            }
922
923            if (!pluginsNewJava.isEmpty())
924            {
925                System.err.println();
926                System.err.println("The following plugin(s) require a newer version of java:");
927                for (PluginDescriptor plugin : pluginsNewJava)
928                {
929                    System.err.println(plugin.getName() + " " + plugin.getVersion());
930                    // notify about installation even fails
931                    fireInstalledEvent(plugin, false);
932                }
933                System.err.println();
934            }
935
936            if (showProgress && !Icy.getMainInterface().isHeadLess())
937            {
938                // no plugin success ?
939                if (pluginsOk.isEmpty())
940                    new FailedAnnounceFrame("Plugin(s) installation failed !", 10);
941                // no plugin fail ?
942                else if (pluginsNOk.isEmpty() && pluginsNewJava.isEmpty())
943                    new SuccessfullAnnounceFrame("Plugin(s) installation was successful !", 10);
944                else if (!pluginsNOk.isEmpty())
945                    new FailedAnnounceFrame(
946                            "Some plugin(s) installation failed (looks at the output console for detail) !", 10);
947
948                // notify about new java version required
949                for (PluginDescriptor plugin : pluginsNewJava)
950                    new AnnounceFrame("Plugin '" + plugin.getName() + " requires a new version of Java !", 10);
951            }
952        }
953        finally
954
955        {
956            // installation end
957            installingPlugins.clear();
958            if (taskFrame != null)
959                taskFrame.close();
960        }
961    }
962
963    private void desinstallInternal()
964    {
965        CancelableProgressFrame taskFrame = null;
966
967        try
968        {
969            final List<PluginInstallInfo> infos;
970            boolean showProgress;
971
972            synchronized (removeFIFO)
973            {
974                infos = new ArrayList<PluginInstaller.PluginInstallInfo>(removeFIFO);
975
976                // determine if we should display the progress bar
977                showProgress = false;
978                for (int i = infos.size() - 1; i >= 0; i--)
979                {
980                    final PluginInstallInfo info = infos.get(i);
981
982                    desinstallingPlugin.add(info.plugin);
983                    showProgress |= info.showProgress;
984                }
985
986                removeFIFO.clear();
987            }
988
989            if (showProgress && !Icy.getMainInterface().isHeadLess())
990                taskFrame = new CancelableProgressFrame("Initializing...");
991
992            // now we can proceed remove
993            for (PluginDescriptor plugin : desinstallingPlugin)
994            {
995                final String plugDesc = plugin.getName() + " " + plugin.getVersion();
996                final boolean result;
997
998                if (taskFrame != null)
999                {
1000                    // cancel requested ?
1001                    if (taskFrame.isCancelRequested())
1002                        return;
1003
1004                    taskFrame.setMessage("Removing plugin '" + plugDesc + "'...");
1005                }
1006
1007                result = deletePlugin(plugin);
1008
1009                // notify plugin deletion
1010                fireRemovedEvent(plugin, result);
1011
1012                if (showProgress && !Icy.getMainInterface().isHeadLess())
1013                {
1014                    if (!result)
1015                        new FailedAnnounceFrame("Plugin '" + plugDesc + "' delete operation failed !");
1016                }
1017
1018                if (result)
1019                    System.out.println("Plugin '" + plugDesc + "' correctly removed.");
1020                else
1021                    System.err.println("Plugin '" + plugDesc + "' delete operation failed !");
1022            }
1023        }
1024        finally
1025        {
1026            if (taskFrame != null)
1027                taskFrame.close();
1028            // removing end
1029            desinstallingPlugin.clear();
1030        }
1031
1032        // reload plugin list
1033        PluginLoader.reload();
1034    }
1035
1036    /**
1037     * Add a listener
1038     * 
1039     * @param listener
1040     */
1041    public static void addListener(PluginInstallerListener listener)
1042    {
1043        synchronized (instance.listeners)
1044        {
1045            instance.listeners.add(PluginInstallerListener.class, listener);
1046        }
1047    }
1048
1049    /**
1050     * Remove a listener
1051     * 
1052     * @param listener
1053     */
1054    public static void removeListener(PluginInstallerListener listener)
1055    {
1056        synchronized (instance.listeners)
1057        {
1058            instance.listeners.remove(PluginInstallerListener.class, listener);
1059        }
1060    }
1061
1062    /**
1063     * fire plugin installed event
1064     */
1065    private void fireInstalledEvent(PluginDescriptor plugin, boolean success)
1066    {
1067        synchronized (listeners)
1068        {
1069            for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class))
1070                listener.pluginInstalled(plugin, success);
1071        }
1072    }
1073
1074    /**
1075     * fire plugin removed event
1076     */
1077    private void fireRemovedEvent(PluginDescriptor plugin, boolean success)
1078    {
1079        synchronized (listeners)
1080        {
1081            for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class))
1082                listener.pluginRemoved(plugin, success);
1083        }
1084    }
1085
1086}