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.update;
020
021import icy.file.FileUtil;
022import icy.gui.frame.ActionFrame;
023import icy.gui.frame.progress.AnnounceFrame;
024import icy.gui.frame.progress.CancelableProgressFrame;
025import icy.gui.frame.progress.DownloadFrame;
026import icy.gui.frame.progress.FailedAnnounceFrame;
027import icy.gui.frame.progress.ProgressFrame;
028import icy.gui.util.GuiUtil;
029import icy.main.Icy;
030import icy.network.NetworkUtil;
031import icy.network.URLUtil;
032import icy.preferences.ApplicationPreferences;
033import icy.system.SystemUtil;
034import icy.system.thread.ThreadUtil;
035import icy.update.ElementDescriptor.ElementFile;
036import icy.util.StringUtil;
037
038import java.awt.BorderLayout;
039import java.awt.Dimension;
040import java.awt.event.ActionEvent;
041import java.awt.event.ActionListener;
042import java.util.ArrayList;
043import java.util.List;
044
045import javax.swing.Box;
046import javax.swing.JLabel;
047import javax.swing.JList;
048import javax.swing.JPanel;
049import javax.swing.JScrollPane;
050import javax.swing.JSplitPane;
051import javax.swing.JTextArea;
052import javax.swing.ListSelectionModel;
053import javax.swing.event.ListSelectionEvent;
054import javax.swing.event.ListSelectionListener;
055
056/**
057 * @author stephane
058 */
059public class IcyUpdater
060{
061    private final static int ANNOUNCE_SHOWTIME = 15;
062
063    public final static String PARAM_ARCH = "arch";
064    public final static String PARAM_VERSION = "version";
065
066    // internals
067    static boolean wantUpdate = false;
068    private static boolean silent;
069    private static boolean updating = false;
070    private static boolean checking = false;
071    private static ActionFrame frame = null;
072    private static Runnable checker = new Runnable()
073    {
074        @Override
075        public void run()
076        {
077            processCheckUpdate();
078        }
079    };
080
081    public static boolean getWantUpdate()
082    {
083        return wantUpdate;
084    }
085
086    /**
087     * return true if we are currently checking for update
088     */
089    public static boolean isCheckingForUpdate()
090    {
091        return checking || ThreadUtil.hasWaitingBgSingleTask(checker);
092    }
093
094    /**
095     * return true if we are currently processing update
096     */
097    public static boolean isUpdating()
098    {
099        return isCheckingForUpdate() || ((frame != null) && frame.isVisible()) || updating;
100    }
101
102    /**
103     * Do the check update process
104     */
105    public static void checkUpdate(boolean silent)
106    {
107        if (!isUpdating())
108        {
109            IcyUpdater.silent = silent;
110            ThreadUtil.bgRunSingle(checker);
111        }
112    }
113
114    /**
115     * @deprecated Use {@link #checkUpdate(boolean)} instead
116     */
117    @Deprecated
118    public static void checkUpdate(boolean showProgress, boolean auto)
119    {
120        checkUpdate(!showProgress || auto);
121    }
122
123    /**
124     * Check for application update process (synchronized method)
125     */
126    public static synchronized void processCheckUpdate()
127    {
128        checking = true;
129        try
130        {
131            wantUpdate = false;
132
133            // delete update directory to avoid partial update
134            FileUtil.delete(Updater.UPDATE_DIRECTORY, true);
135
136            final ArrayList<ElementDescriptor> toUpdate;
137            final ProgressFrame checkingFrame;
138
139            if (!silent && !Icy.getMainInterface().isHeadLess())
140                checkingFrame = new CancelableProgressFrame("checking for application update...");
141            else
142                checkingFrame = null;
143
144            final String params = PARAM_ARCH + "=" + SystemUtil.getOSArchIdString() + "&" + PARAM_VERSION + "="
145                    + Icy.version.toShortString();
146
147            try
148            {
149                // error (or cancel) while downloading XML ?
150                if (!downloadAndSaveForUpdate(
151                        ApplicationPreferences.getUpdateRepositoryBase()
152                                + ApplicationPreferences.getUpdateRepositoryFile() + "?" + params,
153                        Updater.UPDATE_NAME, checkingFrame, !silent))
154                {
155                    // remove partially downloaded files
156                    FileUtil.delete(Updater.UPDATE_DIRECTORY, true);
157                    return;
158                }
159
160                // check if some elements need to be updated from network
161                toUpdate = Updater.getUpdateElements(Updater.getLocalElements());
162            }
163            finally
164            {
165                if (checkingFrame != null)
166                    checkingFrame.close();
167            }
168
169            final boolean needUpdate;
170
171            // empty ? --> no update
172            if (toUpdate.isEmpty())
173                needUpdate = false;
174            // only the updater require updates ? --> no update
175            else if ((toUpdate.size() == 1) && (toUpdate.get(0).getName().equals(Updater.ICYUPDATER_NAME)))
176                needUpdate = false;
177            // otherwise --> update
178            else
179                needUpdate = true;
180
181            // some elements need to be updated ?
182            if (needUpdate)
183            {
184                // silent update or headless mode
185                if (silent || Icy.getMainInterface().isHeadLess())
186                {
187                    // automatically install updates
188                    if (prepareUpdate(toUpdate, true))
189                        // we want update when application will exit
190                        wantUpdate = true;
191                }
192                else
193                {
194                    final String mess;
195
196                    if (toUpdate.size() > 1)
197                        mess = "Some updates are available...";
198                    else
199                        mess = "An update is available...";
200
201                    // show announcement for 15 seconds
202                    new AnnounceFrame(mess, "View", new Runnable()
203                    {
204                        @Override
205                        public void run()
206                        {
207                            // display updates and process them if user accept
208                            showUpdateAndProcess(toUpdate);
209                        }
210                    }, ANNOUNCE_SHOWTIME);
211                }
212            }
213            else
214            {
215                // cleanup
216                FileUtil.delete(Updater.UPDATE_DIRECTORY, true);
217                // inform that there is no update available
218                if (!silent && !Icy.getMainInterface().isHeadLess())
219                    new AnnounceFrame("No application update available", 10);
220            }
221        }
222        finally
223        {
224            checking = false;
225        }
226    }
227
228    static void showUpdateAndProcess(final ArrayList<ElementDescriptor> elements)
229    {
230        if (frame != null)
231        {
232            synchronized (frame)
233            {
234                if (frame.isVisible())
235                    return;
236                frame.getMainPanel().removeAll();
237            }
238        }
239        else
240            frame = new ActionFrame("Application update", true);
241
242        frame.setPreferredSize(new Dimension(640, 500));
243
244        frame.getOkBtn().setText("Install");
245        frame.setOkAction(new ActionListener()
246        {
247            @Override
248            public void actionPerformed(ActionEvent e)
249            {
250                ThreadUtil.bgRun(new Runnable()
251                {
252                    @Override
253                    public void run()
254                    {
255                        // download required files
256                        if (prepareUpdate(elements, true))
257                        {
258                            // ask to update and restart application now
259                            wantUpdate = true;
260                            Icy.confirmRestart();
261                        }
262                        else
263                            new FailedAnnounceFrame("An error occured while downloading files (see details in console)",
264                                    10000);
265                    }
266                });
267            }
268        });
269
270        final JPanel topPanel = GuiUtil.createPageBoxPanel(Box.createVerticalStrut(4),
271                GuiUtil.createCenteredBoldLabel("The following(s) element(s) will be updated"),
272                Box.createVerticalStrut(4));
273
274        final JTextArea changeLogArea = new JTextArea();
275        changeLogArea.setEditable(false);
276        final JLabel changeLogTitleLabel = GuiUtil.createBoldLabel("Change log :");
277
278        final JList list = new JList(elements.toArray());
279        list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
280        list.getSelectionModel().addListSelectionListener(new ListSelectionListener()
281        {
282            @Override
283            public void valueChanged(ListSelectionEvent e)
284            {
285                if (list.getSelectedValue() != null)
286                {
287                    final ElementDescriptor element = (ElementDescriptor) list.getSelectedValue();
288
289                    final String changeLog = element.getChangelog().trim();
290
291                    if (StringUtil.isEmpty(changeLog))
292                        changeLogArea.setText("no change log");
293                    else
294                        changeLogArea.setText(changeLog);
295                    changeLogArea.setCaretPosition(0);
296                    changeLogTitleLabel.setText(element.getName() + " change log");
297                }
298            }
299        });
300        list.setSelectedIndex(0);
301
302        final JScrollPane medScrollPane = new JScrollPane(list);
303        final JScrollPane changeLogScrollPane = new JScrollPane(GuiUtil.createTabArea(changeLogArea, 4));
304        final JPanel bottomPanel = GuiUtil.createPageBoxPanel(Box.createVerticalStrut(4),
305                GuiUtil.createCenteredLabel(changeLogTitleLabel), Box.createVerticalStrut(4), changeLogScrollPane);
306
307        final JPanel mainPanel = frame.getMainPanel();
308
309        final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, medScrollPane, bottomPanel);
310
311        mainPanel.add(topPanel, BorderLayout.NORTH);
312        mainPanel.add(splitPane, BorderLayout.CENTER);
313
314        frame.pack();
315        frame.addToDesktopPane();
316        frame.setVisible(true);
317        frame.center();
318        frame.requestFocus();
319
320        // set splitter to middle
321        splitPane.setDividerLocation(0.5d);
322    }
323
324    static boolean prepareUpdate(List<ElementDescriptor> elements, boolean showProgress)
325    {
326        final DownloadFrame downloadingFrame;
327
328        updating = true;
329        if (showProgress && !Icy.getMainInterface().isHeadLess())
330            downloadingFrame = new DownloadFrame("");
331        else
332            downloadingFrame = null;
333        try
334        {
335            // get total number of files to process
336            int numFile = 0;
337            for (ElementDescriptor element : elements)
338                numFile += element.getFiles().size();
339
340            if (downloadingFrame != null)
341                downloadingFrame.setLength(numFile);
342
343            int curFile = 0;
344            for (ElementDescriptor element : elements)
345            {
346                for (ElementFile elementFile : element.getFiles())
347                {
348                    curFile++;
349
350                    if (downloadingFrame != null)
351                    {
352                        // update progress frame message and position
353                        downloadingFrame.setMessage("Downloading updates " + curFile + " / " + numFile);
354
355                        final String toolTip = "Downloading " + element.getName() + " : "
356                                + FileUtil.getFileName(elementFile.getLocalPath());
357                        // update progress frame tooltip
358                        downloadingFrame.setToolTipText(toolTip);
359                    }
360
361                    // symbolic link file ?
362                    if (elementFile.isLink())
363                    {
364                        // special treatment
365                        if (!FileUtil.createLink(
366                                Updater.UPDATE_DIRECTORY + FileUtil.separator + elementFile.getLocalPath(),
367                                elementFile.getOnlinePath()))
368                        {
369                            // remove partially downloaded files
370                            FileUtil.delete(Updater.UPDATE_DIRECTORY, true);
371                            return false;
372                        }
373                    }
374                    else
375                    {
376                        // local file need to be updated --> download new file
377                        if (Updater.needUpdate(elementFile.getLocalPath(), elementFile.getDateModif()))
378                        {
379                            // error (or cancel) while downloading ?
380                            if (!downloadAndSaveForUpdate(
381                                    URLUtil.getNetworkURLString(ApplicationPreferences.getUpdateRepositoryBase(),
382                                            elementFile.getOnlinePath()),
383                                    elementFile.getLocalPath(), downloadingFrame, showProgress))
384                            {
385                                // remove partially downloaded files
386                                FileUtil.delete(Updater.UPDATE_DIRECTORY, true);
387                                return false;
388                            }
389                        }
390                    }
391                }
392            }
393        }
394        finally
395        {
396            if (downloadingFrame != null)
397                downloadingFrame.close();
398            updating = false;
399        }
400
401        return true;
402    }
403
404    public static boolean downloadAndSaveForUpdate(String downloadPath, String savePath, ProgressFrame frame,
405            boolean displayError)
406    {
407        // get data
408        final byte[] data;
409
410        data = NetworkUtil.download(downloadPath, frame, displayError);
411        if (data == null)
412            return false;
413
414        // build save filename
415        String saveFilename = Updater.UPDATE_DIRECTORY + FileUtil.separator;
416
417        if (StringUtil.isEmpty(savePath))
418            saveFilename += URLUtil.getURLFileName(downloadPath, true);
419        else
420            saveFilename += savePath;
421
422        if (!FileUtil.save(saveFilename, data, displayError))
423            return false;
424
425        return true;
426    }
427
428    /**
429     * Return true if required files for updates are present
430     */
431    private static boolean canDoUpdate()
432    {
433        // check for updater presence
434        boolean requiredFilesExist = FileUtil
435                .exists(FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + Updater.UPDATER_NAME);
436        // // in update directory ?
437        // requiredFilesExist |= FileUtil.exists(Updater.UPDATE_DIRECTORY + FileUtil.separator +
438        // Updater.UPDATER_NAME);
439        // check for update xml file
440        requiredFilesExist &= FileUtil.exists(Updater.UPDATE_DIRECTORY + FileUtil.separator + Updater.UPDATE_NAME);
441
442        // required files present so we can do update
443        return requiredFilesExist;
444    }
445
446    /**
447     * Launch the updater with the specified update and restart parameters
448     */
449    public static boolean launchUpdater(boolean doUpdate, boolean restart)
450    {
451        if (doUpdate)
452        {
453            final String updateName = Updater.UPDATE_DIRECTORY + FileUtil.separator + Updater.UPDATER_NAME;
454
455            // updater need update ? process it first
456            if (FileUtil.exists(updateName))
457            {
458                // replace updater
459                if (!FileUtil.rename(updateName,
460                        FileUtil.APPLICATION_DIRECTORY + FileUtil.separator + Updater.UPDATER_NAME, true))
461                {
462                    System.err.println("Can't update 'Upater.jar', Update process can't continue.");
463                    return false;
464                }
465            }
466
467            // this is not really needed...
468            if (!canDoUpdate())
469            {
470                System.err.println("Can't process update : some required files are missing.");
471                return false;
472            }
473        }
474
475        String params = "";
476
477        if (doUpdate)
478            params += Updater.ARG_UPDATE + " ";
479        if (!restart)
480            params += Updater.ARG_NOSTART + " ";
481
482        // launch updater
483        // WARNING: don't use application folder here, it doesn't work as expected !
484        SystemUtil.execJAR(Updater.UPDATER_NAME, params);
485
486        // you have to exit application then...
487        return true;
488    }
489}