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}