/******************************************************************************* * Breakout Cave Survey Visualizer * * Copyright (C) 2014 James Edwards * * jedwards8 at fastmail dot fm * * 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 2 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, write to the Free Software Foundation, Inc., 51 * Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. *******************************************************************************/ package org.breakout.update; import static org.breakout.update.UpdateStatus.CHECKING; import static org.breakout.update.UpdateStatus.STARTING_DOWNLOAD; import static org.breakout.update.UpdateStatus.UPDATE_AVAILABLE; import static org.breakout.update.UpdateStatus.UPDATE_DOWNLOADED; import static org.breakout.update.UpdateStatus.UP_TO_DATE; import java.awt.Desktop; import java.awt.event.ActionEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.PrintStream; import java.net.URL; import java.net.URLConnection; import java.nio.file.Paths; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import javax.swing.AbstractAction; import org.andork.io.Downloader; import org.andork.io.Downloader.State; import org.andork.io.FileDigest; import org.andork.io.FileUtils; import org.andork.swing.OnEDT; import org.andork.util.Java7.Objects; import org.andork.util.VersionUtil; import org.breakout.update.UpdateStatus.CheckFailed; import org.breakout.update.UpdateStatus.ChecksumFailed; import org.breakout.update.UpdateStatus.DownloadFailed; import org.breakout.update.UpdateStatus.Downloading; import org.breakout.update.UpdateStatus.Failure; public class UpdateStatusPanelController { @SuppressWarnings("serial") private class CancelDownloadAction extends AbstractAction { /** * */ private static final long serialVersionUID = -7048544377537696960L; @Override public void actionPerformed(ActionEvent e) { if (checksumDownloader != null) { try { checksumDownloader.cancel(); } catch (Exception ex) { ex.printStackTrace(); } } if (downloader != null) { try { downloader.cancel(); } catch (Exception ex) { ex.printStackTrace(); } } } } @SuppressWarnings("serial") private class CheckForUpdatesAction extends AbstractAction { /** * */ private static final long serialVersionUID = 8764498265738805122L; @Override public void actionPerformed(ActionEvent e) { checkForUpdate(); } } @SuppressWarnings("serial") private class DownloadAction extends AbstractAction { /** * */ private static final long serialVersionUID = 1476589982079559886L; @Override public void actionPerformed(ActionEvent e) { UpdateStatus status = panel.getStatus(); if (status instanceof Failure && !(status instanceof CheckFailed)) { panel.setStatus(UPDATE_AVAILABLE); } if (panel.getStatus() == UPDATE_AVAILABLE) { downloadUpdate(); } } } @SuppressWarnings("serial") private class InstallAction extends AbstractAction { /** * */ private static final long serialVersionUID = -3097880855616005553L; @Override public void actionPerformed(ActionEvent e) { panel.showInstallInstructionsDialog(); Desktop desktop = Desktop.getDesktop(); try { desktop.open(downloadDir); } catch (Exception ex) { ex.printStackTrace(); } } } private static ExecutorService createDefaultExecutor() { ThreadFactory threadFactory = new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setDaemon(false); thread.setName("UpdateStatusPanelController thread"); return thread; } }; return new ThreadPoolExecutor(0, 1, 1000, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>(), threadFactory); } private static String formatException(Exception ex) { return ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage(); } private UpdateStatusPanel panel; private String currentVersion; private URL latestVersionInfoUrl; private URL latestVersionDownloadUrl; private URL latestVersionChecksumUrl; private File downloadDir; private File downloadPropsFile; private ExecutorService executor; private Downloader checksumDownloader; private Downloader downloader; public UpdateStatusPanelController(UpdateStatusPanel panel, ExecutorService executor, String currentVersion, URL latestVersionInfoUrl, File downloadDir) { this.panel = panel; this.currentVersion = currentVersion; this.latestVersionInfoUrl = latestVersionInfoUrl; this.downloadDir = downloadDir; downloadPropsFile = new File(downloadDir, "downloaded.properties"); this.executor = executor; panel.setDownloadAction(new DownloadAction()); panel.setCheckForUpdatesAction(new CheckForUpdatesAction()); panel.setCancelDownloadAction(new CancelDownloadAction()); panel.setInstallAction(new InstallAction()); } public UpdateStatusPanelController(UpdateStatusPanel panel, String currentVersion, URL latestVersionInfoUrl, File downloadDir) { this(panel, createDefaultExecutor(), currentVersion, latestVersionInfoUrl, downloadDir); } protected void checkChecksum(final File fileToCheck, final File expectedChecksumFile) throws Exception { String actualChecksum = null; actualChecksum = FileDigest.format(FileDigest.checksum(fileToCheck.toPath(), "md5")); String expectedChecksum = null; expectedChecksum = FileUtils.slurpAsString(expectedChecksumFile.toPath()); expectedChecksum = expectedChecksum.substring(0, Math.min(expectedChecksum.length(), 32)); if (!Objects.equals(actualChecksum, expectedChecksum)) { throw new Exception(panel.getLocalizer() .getFormattedString("downloadUpdate.exception.checksumMismatch", expectedChecksum, actualChecksum)); } } public void checkForUpdate() { executor.submit(() -> { OnEDT.onEDT(() -> panel.setStatus(CHECKING)); UpdateStatus newStatus = null; String latestVersion = null; URLConnection connection = null; try { Properties props = new Properties(); connection = latestVersionInfoUrl.openConnection(); props.load(connection.getInputStream()); connection.getInputStream().close(); latestVersion = props.getProperty("version"); if (latestVersion == null) { throw new Exception(panel.getLocalizer().getString( "checkForUpdate.exception.missingVersionNumber")); } if (!latestVersion.matches("(\\d+)(\\.\\d+)*")) { throw new Exception(panel.getLocalizer().getFormattedString( "checkForUpdate.exception.invalidVersionNumber", latestVersion)); } String downloadUrlString = props.getProperty("url"); if (downloadUrlString == null) { throw new Exception(panel.getLocalizer().getString( "checkForUpdate.exception.missingDownloadUrl")); } try { latestVersionDownloadUrl = new URL(downloadUrlString); } catch (Exception ex) { throw new Exception(panel.getLocalizer().getFormattedString( "checkForUpdate.exception.invalidUrl", downloadUrlString)); } String checksumUrlString = props.getProperty("checksum"); if (checksumUrlString != null) { try { latestVersionChecksumUrl = new URL(checksumUrlString); } catch (Exception ex) { throw new Exception(panel.getLocalizer().getFormattedString( "checkForUpdate.exception.invalidUrl", checksumUrlString)); } } if (VersionUtil.compareVersions(currentVersion, latestVersion) < 0) { newStatus = UPDATE_AVAILABLE; if (downloadPropsFile.exists()) { props = new Properties(); try (InputStream in = new FileInputStream(downloadPropsFile)) { props.load(in); in.close(); String downloadedVersion = props.getProperty("version"); if (Objects.equals(downloadedVersion, latestVersion)) { File destFile = downloadDir.toPath() .resolve(Paths.get(props.getProperty("file"))).toFile(); File checksumFile = new File(destFile.getPath() + ".md5"); try { checkChecksum(destFile, checksumFile); newStatus = UPDATE_DOWNLOADED; } catch (Exception ex) { newStatus = new ChecksumFailed(ex.getLocalizedMessage()); } } } catch (Exception ex) { } } } else { newStatus = UP_TO_DATE; } } catch (final Exception ex) { ex.printStackTrace(); if (connection != null) { try { connection.getInputStream().close(); } catch (final Exception ex2) { ex2.printStackTrace(); } } newStatus = new CheckFailed(formatException(ex)); } final String finalLatestVersion = latestVersion; final UpdateStatus finalNewStatus = newStatus; OnEDT.onEDT(() -> { panel.setLatestVersion(finalLatestVersion); panel.setStatus(finalNewStatus); }); }); } private void downloadUpdate() { OnEDT.onEDT(() -> panel.setStatus(STARTING_DOWNLOAD)); if (downloadDir.exists()) { try { for (File file : downloadDir.listFiles()) { file.delete(); } } catch (Exception ex) { ex.printStackTrace(); } } final UpdateStatus prevStatus = panel.getStatus(); String file = latestVersionDownloadUrl.getFile(); int i = file.lastIndexOf('/') + 1; if (i > file.length()) { i = 0; } File destFile = new File(downloadDir, file.substring(i)); File checksumFile = new File(destFile.getPath() + ".md5"); if (latestVersionChecksumUrl != null) { checksumDownloader = new Downloader().url(latestVersionChecksumUrl).destFile(checksumFile) .blockSize(1024); checksumDownloader.addPropertyChangeListener(evt -> { OnEDT.onEDT(() -> { if (downloader.getState() == State.DOWNLOADING) { panel.setStatus(new Downloading(0, downloader .getTotalSize())); } else if (downloader.getState() == State.FAILED) { panel.setStatus(new DownloadFailed(formatException(downloader.getException()))); } else if (downloader.getState() == State.CANCELED) { panel.setStatus(prevStatus); } }); }); } downloader = new Downloader().url(latestVersionDownloadUrl).destFile(destFile).blockSize(1024); downloader.addPropertyChangeListener(new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { OnEDT.onEDT(() -> { if (downloader.getState() == State.DOWNLOADING) { panel.setStatus(new Downloading(downloader.getNumBytesDownloaded(), downloader .getTotalSize())); } else if (downloader.getState() == State.FAILED) { panel.setStatus(new DownloadFailed(formatException(downloader.getException()))); } else if (downloader.getState() == State.CANCELED) { panel.setStatus(prevStatus); } }); } }); final File finalChecksumFile = checksumFile; executor.submit(() -> { if (!downloadDir.exists()) { downloadDir.mkdirs(); } if (checksumDownloader != null) { checksumDownloader.download(); if (checksumDownloader.getState() == State.CANCELED) { return; } if (checksumDownloader.getState() == State.FAILED) { OnEDT.onEDT(() -> panel.setStatus(new DownloadFailed(checksumDownloader.getException() .getLocalizedMessage()))); return; } } downloader.download(); if (downloader.getState() == State.CANCELED) { return; } if (downloader.getState() == State.FAILED) { OnEDT.onEDT(() -> panel.setStatus(new DownloadFailed(checksumDownloader.getException() .getLocalizedMessage()))); return; } if (finalChecksumFile != null) { try { checkChecksum(destFile, finalChecksumFile); } catch (Exception ex) { ex.printStackTrace(); OnEDT.onEDT(() -> panel.setStatus(new ChecksumFailed(ex.getLocalizedMessage()))); return; } } try (PrintStream ps = new PrintStream(new FileOutputStream(downloadPropsFile))) { ps.println("version=" + panel.getLatestVersion()); ps.println("file=" + downloadDir.toPath().relativize(destFile.toPath())); ps.close(); } catch (Exception ex) { ex.printStackTrace(); } OnEDT.onEDT(() -> panel.setStatus(UPDATE_DOWNLOADED)); }); } }