Package se.llbit.chunky.launcher

Source Code of se.llbit.chunky.launcher.UpdateDialog$DownloadJob

/* Copyright (c) 2013-2014 Jesper Öqvist <jesper@llbit.se>
*
* This file is part of Chunky.
*
* Chunky 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.
*
* Chunky 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 Chunky.  If not, see <http://www.gnu.org/licenses/>.
*/
package se.llbit.chunky.launcher;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import javax.swing.GroupLayout;
import javax.swing.GroupLayout.Alignment;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextPane;
import javax.swing.LayoutStyle.ComponentPlacement;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

import se.llbit.chunky.PersistentSettings;
import se.llbit.chunky.launcher.VersionInfo.Library;
import se.llbit.chunky.launcher.VersionInfo.LibraryStatus;

/**
* Asks user if new version should be downloaded.
* Displays release notes and download status.
* @author Jesper Öqvist <jesper@llbit.se>
*/
@SuppressWarnings("serial")
public class UpdateDialog extends JDialog {
  protected final ExecutorService threadPool = Executors.newFixedThreadPool(4);

  public class Finalizer extends Thread {
    private final Collection<Future<DownloadStatus>> results;
    public Finalizer(Collection<Future<DownloadStatus>> resultFutures) {
      results = resultFutures;
    }
    @Override
    public void run() {
      try {
        boolean failed = false;
        for (Future<DownloadStatus> result: results) {
          try {
            if (result.get() != DownloadStatus.SUCCESS) {
              failed = true;
            }
          } catch (InterruptedException e) {
            failed = true;
          } catch (ExecutionException e) {
            failed = true;
          }
        }
        if (failed) {
          updateFailed("Failed to download some required libraries. Please try again later.");
          return;
        }
        try {
          File versionFile = new File(versionsDir, version.name + ".json");
          version.writeTo(versionFile);
          SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
              progress.setValue(progress.getMaximum());
              completedLbl.setVisible(true);
              cancelBtn.setText("Close");
              parent.updateVersionList();
              parent.selectLatestVersion();
            }
          });
        } catch (IOException e) {
          updateFailed("Failed to update version info. Please try again later.");
        }
      } finally {
        updateCompleted();
      }
    }
    private void updateFailed(final String message) {
      SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
          progress.setStringPainted(true);
          progress.setString(message);
          progress.setForeground(Color.red);
        }
      });
    }
    private void updateCompleted() {
      busyLbl.setVisible(false);
      synchronized (updateLock) {
        busy = false;
        updateLock.notifyAll();
      }
    }
  }

  static class StatusCellRenderer extends DefaultTableCellRenderer {
    @Override
    protected void setValue(Object value) {
      if (value instanceof LibraryStatus) {
        LibraryStatus status = (LibraryStatus) value;
        setText(status.downloadStatus());
        if (status == LibraryStatus.PASSED || status == LibraryStatus.DOWNLOADED_OK) {
          setIcon(Icons.cached);
        } else {
          switch (status) {
          case MD5_MISMATCH:
          case MISSING:
            setIcon(Icons.refresh);
            break;
          default:
            setIcon(Icons.failed);
          }
        }
      }
    }
  }

  private final JButton okBtn = new JButton("Update to New Version");
  private final JButton cancelBtn = new JButton("Cancel");
  private final JLabel completedLbl = new JLabel("Update completed!");
  private final JProgressBar progress = new JProgressBar();
  private final ChunkyLauncher parent;
  private final VersionInfo version;
  private final File libDir;
  private final File versionsDir;
  private final Object updateLock = new Object();
  private boolean busy = false;
  private final JLabel busyLbl = new JLabel();
  private final JTable status;
  private final Collection<VersionInfo.Library> neededLibraries = new LinkedList<VersionInfo.Library>();
  private int downloadBytes = 0;
  private final DefaultTableModel tableModel;

  public UpdateDialog(final ChunkyLauncher parent, final VersionInfo version) {
    super(parent, "Update Available!");

    this.parent = parent;
    this.version = version;

    File chunkyDir = PersistentSettings.getSettingsDirectory();
    libDir = new File(chunkyDir, "lib");
    if (!libDir.isDirectory()) {
      libDir.mkdirs();
    }
    versionsDir = new File(chunkyDir, "versions");
    if (!versionsDir.isDirectory()) {
      versionsDir.mkdirs();
    }

    setModalityType(ModalityType.MODELESS);
    setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);

    addWindowListener(new WindowListener() {
      @Override
      public void windowOpened(WindowEvent e) {
      }
      @Override
      public void windowIconified(WindowEvent e) {
      }
      @Override
      public void windowDeiconified(WindowEvent e) {
      }
      @Override
      public void windowDeactivated(WindowEvent e) {
      }
      @Override
      public void windowClosing(WindowEvent e) {
        close();
      }
      @Override
      public void windowClosed(WindowEvent e) {
      }
      @Override
      public void windowActivated(WindowEvent e) {
      }
    });

    URL url = getClass().getResource("/chunky-cfg.png");
    if (url != null) {
      setIconImage(Toolkit.getDefaultToolkit().getImage(url));
    }

    url = getClass().getResource("/busy.gif");
    if (url != null) {
      busyLbl.setIcon(new ImageIcon(Toolkit.getDefaultToolkit().getImage(url)));
    }
    busyLbl.setVisible(false);

    JLabel infoLbl = new JLabel(
        "<html>A new version of Chunky is available for download!<br>" +
        "<br>Version <b>" + version.name + "</b>, released on " + version.date() +
        "<br>Release notes:");

    okBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        downloadUpdate();
      }
    });

    cancelBtn.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        close();
      }
    });

    completedLbl.setIcon(Icons.cached);
    completedLbl.setVisible(false);

    JTextPane changeLog = new JTextPane();
    changeLog.setCaretPosition(0);
    changeLog.setMargin(new Insets(0, 0, 0, 0));
    if (version.notes.isEmpty()) {
      changeLog.setText("No release notes available.");
    } else {
      changeLog.setText(version.notes);
    }

    tableModel = new DefaultTableModel(version.libraries.size(), 3);
    int i = 0;
    for (Library lib: version.libraries) {
      LibraryStatus libStatus = lib.testIntegrity(libDir);
      if (libStatus != LibraryStatus.PASSED && libStatus != LibraryStatus.INCOMPLETE_INFO) {
        neededLibraries.add(lib);
        downloadBytes += lib.size;
      }

      // pretty print library size
      float size = lib.size;
      String unit = "B";
      if (size >= 1024*1024) {
        size /= 1024*1024;
        unit = "MiB";
      } else if (size >= 1024) {
        size /= 1024;
        unit = "KiB";
      }
      String libSize;
      if (size >= 10) {
        libSize = String.format("%d %s", (int) size, unit);
      } else {
        libSize = String.format("%.1f %s", size, unit);
      }

      tableModel.setValueAt(lib, i, 0);
      tableModel.setValueAt(libStatus, i, 1);
      tableModel.setValueAt(libSize, i, 2);
      i += 1;
    }
    tableModel.setColumnIdentifiers(new String[] { "Library", "Status", "Size" });
    final StatusCellRenderer statusRenderer = new StatusCellRenderer();
    status = new JTable(tableModel) {
      @Override
      public TableCellRenderer getCellRenderer(int row, int column) {
        if (column == 1) {
          return statusRenderer;
        }
        return super.getCellRenderer(row, column);
      }

      @Override
      public boolean isCellEditable(int row, int column) {
        return false;
      }
    };
    status.getTableHeader().setVisible(false);
    status.setVisible(false);
    final JCheckBox details = new JCheckBox("Details");
    details.setIcon(Icons.expand);
    details.setRolloverIcon(Icons.expandHover);
    details.setSelectedIcon(Icons.collapse);
    details.setRolloverSelectedIcon(Icons.collapseHover);
    details.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        boolean expand = details.isSelected();
        status.getTableHeader().setVisible(expand);
        status.setVisible(expand);
      }
    });

    JScrollPane changeLogScrollPane = new JScrollPane(changeLog);

    JPanel panel = new JPanel();
    GroupLayout layout = new GroupLayout(panel);
    panel.setLayout(layout);

    layout.setHorizontalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addGroup(layout.createParallelGroup()
        .addComponent(infoLbl)
        .addComponent(changeLogScrollPane)
        .addComponent(details)
        .addComponent(status.getTableHeader())
        .addComponent(status)
        .addComponent(progress)
        .addGroup(layout.createSequentialGroup()
          .addPreferredGap(ComponentPlacement.RELATED, GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
          .addComponent(busyLbl)
          .addComponent(okBtn)
          .addComponent(completedLbl)
          .addPreferredGap(ComponentPlacement.UNRELATED)
          .addComponent(cancelBtn)
        )
      )
      .addContainerGap()
    );
    layout.setVerticalGroup(layout.createSequentialGroup()
      .addContainerGap()
      .addComponent(infoLbl)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(changeLogScrollPane)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(details)
      .addComponent(status.getTableHeader())
      .addComponent(status)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addComponent(progress)
      .addPreferredGap(ComponentPlacement.UNRELATED)
      .addGroup(layout.createParallelGroup(Alignment.BASELINE)
        .addComponent(busyLbl)
        .addComponent(okBtn)
        .addComponent(completedLbl)
        .addComponent(cancelBtn)
      )
      .addContainerGap()
    );

    setContentPane(panel);

    setPreferredSize(new Dimension(500, 500));
    pack();

    setLocationRelativeTo(parent);
    //setLocationByPlatform(true);
  }

  protected void downloadUpdate() {
    synchronized (updateLock) {
      busy = true;
    }
    okBtn.setVisible(false);
    busyLbl.setVisible(true);
    if (!libDir.isDirectory()) {
      // TODO ERROR
    }
    if (!versionsDir.isDirectory()) {
      // TODO ERROR
    }
    progress.setMaximum(downloadBytes+1);
    progress.setValue(0);
    final List<Future<DownloadStatus>> results = new LinkedList<Future<DownloadStatus>>();
    for (VersionInfo.Library lib: neededLibraries) {
      results.add(threadPool.submit(new DownloadJob(lib, new ProgressIncrementer(lib.size))));
    }
    new Finalizer(results).start();
  }

  protected void close() {
    threadPool.shutdownNow();
    awaitUpdate();
    setVisible(false);
    dispose();
    parent.setBusyEDT(false);
  }

  private void awaitUpdate() {
    // TODO abort download threads!
    synchronized (updateLock) {
      try {
        while (busy) {
          updateLock.wait();
        }
      } catch (InterruptedException e) {
        // not critical
      }
    }
  }

  class DownloadJob implements Callable<DownloadStatus> {

    private final Library lib;
    private final Runnable callback;

    public DownloadJob(VersionInfo.Library lib, Runnable callback) {
      this.lib = lib;
      this.callback = callback;
    }

    @Override
    public DownloadStatus call() throws Exception {
      DownloadStatus result = null;
      if (!lib.url.isEmpty()) {
        result = tryDownload(libDir, lib, lib.url);
        switch (result) {
        case MALFORMED_URL:
          System.err.println("Malformed URL: " + lib.url);
          break;
        case FILE_NOT_FOUND:
          System.err.println("File not found: " + lib.url);
          break;
        case DOWNLOAD_FAILED:
          System.err.println("Download failed: " + lib.url);
          break;
        default:
          break;
        }
      }
      String defaultUrl = "http://chunkyupdate.llbit.se/lib/" + lib.name;
      if (result != DownloadStatus.SUCCESS) {
        result = tryDownload(libDir, lib, defaultUrl);
      }
      switch (result) {
      case SUCCESS:
        updateStatus(lib, LibraryStatus.DOWNLOADED_OK);
        break;
      case MALFORMED_URL:
        updateStatus(lib, LibraryStatus.MALFORMED_URL);
        System.err.println("Malformed URL: " + defaultUrl);
        break;
      case FILE_NOT_FOUND:
        updateStatus(lib, LibraryStatus.FILE_NOT_FOUND);
        System.err.println("File not found: " + defaultUrl);
        break;
      case DOWNLOAD_FAILED:
        updateStatus(lib, LibraryStatus.DOWNLOAD_FAILED);
        System.err.println("Download failed: " + defaultUrl);
        break;
      }
      SwingUtilities.invokeLater(callback);
      return result;
    }

  }

  class ProgressIncrementer implements Runnable {
    private final int value;

    public ProgressIncrementer(int value) {
      this.value = value;
    }

    @Override
    public void run() {
      progress.setValue(progress.getValue() + value);
    }
  }

  public void updateStatus(final Library library, final LibraryStatus status) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
        for (int i = 0; i < tableModel.getRowCount(); ++i) {
          if (tableModel.getValueAt(i, 0) == library) {
            tableModel.setValueAt(status, i, 1);
            break;
          }
        }
      }
    });
  }

  public static DownloadStatus tryDownload(File libDir, Library lib, String theUrl) {
    try {
      URL url = new URL(theUrl);
      ReadableByteChannel inChannel = Channels.newChannel(url.openStream());
      FileOutputStream out = new FileOutputStream(lib.getFile(libDir));
      out.getChannel().transferFrom(inChannel, 0, Long.MAX_VALUE);
      out.close();
      LibraryStatus status = lib.testIntegrity(libDir);
      if (status == LibraryStatus.PASSED) {
        return DownloadStatus.SUCCESS;
      } else {
        return DownloadStatus.DOWNLOAD_FAILED;
      }
    } catch (MalformedURLException e) {
      return DownloadStatus.MALFORMED_URL;
    } catch (FileNotFoundException e) {
      return DownloadStatus.FILE_NOT_FOUND;
    } catch (IOException e) {
      return DownloadStatus.DOWNLOAD_FAILED;
    }

  }

}
TOP

Related Classes of se.llbit.chunky.launcher.UpdateDialog$DownloadJob

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.