package beast.app.beauti;
import beast.app.util.Arguments;
import beast.app.util.Utils;
import beast.core.Description;
import beast.util.AddOnManager;
import beast.util.Package;
import beast.util.PackageVersion;
import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableModel;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;
import static beast.util.AddOnManager.*;
/**
* dialog for managing Package.
* List, install and uninstall Package
*
* @author Remco Bouckaert
* @author Walter Xie
*/
@Description("BEAUti package manager")
public class JPackageDialog extends JPanel {
private static final long serialVersionUID = 1L;
JScrollPane scrollPane;
JLabel jLabel;
Box buttonBox;
JFrame frame;
PackageTable dataTable = null;
boolean useLatestVersion = true;
TreeMap<String, Package> packageMap = new TreeMap<>((s1,s2)->{
if (s1.equals(AddOnManager.BEAST_PACKAGE_NAME)) {
if (s2.equals(AddOnManager.BEAST_PACKAGE_NAME)) {
return 0;
}
return -1;
}
if (s2.equals(AddOnManager.BEAST_PACKAGE_NAME)) {
return 1;
}
return s1.compareToIgnoreCase(s2);
});
List<Package> packageList = null;
boolean isRunning;
Thread t;
public JPackageDialog() {
jLabel = new JLabel("List of available packages for BEAST v" + beastVersion.getMajorVersion() + ".*");
frame = (JFrame) SwingUtilities.getWindowAncestor(this);
setLayout(new BorderLayout());
createTable();
// update packages using a 30 second time out
isRunning = true;
t = new Thread() {
@Override
public void run() {
resetPackages();
dataTable.updateWidths();
isRunning = false;
}
};
t.start();
Thread t2 = new Thread() {
@Override
public void run() {
try {
// wait 30 seconds
sleep(30000);
if (isRunning) {
t.interrupt();
JOptionPane.showMessageDialog(frame, "<html>Download of file " +
AddOnManager.PACKAGES_XML + " timed out.<br>" +
"Perhaps this is due to lack of internet access</br>" +
"or some security settings not allowing internet access.</html>"
);
}
} catch (InterruptedException e) {
}
}
};
t2.start();
try {
t.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
scrollPane = new JScrollPane(dataTable);
/*getContentPane().*/add(BorderLayout.CENTER, scrollPane);
buttonBox = createButtonBox();
/*getContentPane().*/add(buttonBox, BorderLayout.SOUTH);
scrollPane.setPreferredSize(new Dimension(660, 400));
Dimension dim = scrollPane.getPreferredSize();
Dimension dim2 = buttonBox.getPreferredSize();
setSize(dim.width + 30, dim.height + dim2.height + 30);
}
private void createTable() {
DataTableModel dataTableModel = new DataTableModel();
dataTable = new PackageTable(dataTableModel);
dataTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
// TODO:
// The following would work ...
//dataTable.setAutoCreateRowSorter(true);
// ...if all processing was done based on the data in the table,
// instead of the row number alone.
dataTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
dataTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (dataTable.getSelectedColumn() == dataTableModel.linkColumn) {
URL url = getSelectedPackage(dataTable.getSelectedRow()).getProjectURL();
if (url != null) {
try {
Desktop.getDesktop().browse(url.toURI());
} catch (IOException | URISyntaxException e1) {
e1.printStackTrace();
}
}
} else {
if (e.getClickCount() == 2) {
Package selPackage = getSelectedPackage(dataTable.getSelectedRow());
showDetail(selPackage);
}
}
}
});
dataTable.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseMoved(MouseEvent e) {
super.mouseMoved(e);
int row = dataTable.rowAtPoint(e.getPoint());
int col = dataTable.columnAtPoint(e.getPoint());
int currentCursorType = dataTable.getCursor().getType();
if (col != dataTableModel.linkColumn) {
if (currentCursorType == Cursor.HAND_CURSOR)
dataTable.setCursor(Cursor.getDefaultCursor());
return;
}
Package thisPkg = getSelectedPackage(row);
if (thisPkg.getProjectURL() == null) {
if (currentCursorType == Cursor.HAND_CURSOR)
dataTable.setCursor(Cursor.getDefaultCursor());
return;
}
dataTable.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
}
});
int size = dataTable.getFont().getSize();
dataTable.setRowHeight(20 * size/13);
}
private void resetPackages() {
packageMap.clear();
try {
addAvailablePackages(packageMap);
addInstalledPackages(packageMap);
// Create list of packages excluding beast2
packageList = new ArrayList<>();
for (Package pkg : packageMap.values())
if (!pkg.getName().equals("beast2"))
packageList.add(pkg);
} catch (AddOnManager.PackageListRetrievalException e) {
StringBuilder msgBuilder = new StringBuilder("<html>" + e.getMessage() + "<br>");
if (e.getCause() instanceof IOException)
msgBuilder.append(NO_CONNECTION_MESSAGE.replaceAll("\\.", ".<br>"));
msgBuilder.append("</html>");
try {
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(null, msgBuilder));
} catch (Exception e0) {
e0.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
dataTable.tableChanged(new TableModelEvent(dataTable.getModel()));
if (dataTable.getRowCount() > 0)
dataTable.setRowSelectionInterval(0, 0);
}
private Package getSelectedPackage(int selectedRow) {
if (packageList.size() <= selectedRow)
throw new IllegalArgumentException("Incorrect row " + selectedRow +
" is selected from package list, size = " + packageMap.size());
return packageList.get(selectedRow);
}
private void showDetail(Package aPackage) {
//custom title, no icon
JOptionPane.showMessageDialog(null,
aPackage.toHTML(),
aPackage.getName(),
JOptionPane.PLAIN_MESSAGE);
}
private Box createButtonBox() {
Box box = Box.createHorizontalBox();
final JCheckBox latestVersionCheckBox = new JCheckBox("Latest");
latestVersionCheckBox.setToolTipText("If selected, only the latest version is installed when hitting the Install/Upgrade button. "
+ "Otherwise, you can select from a list of available versions.");
box.add(latestVersionCheckBox);
latestVersionCheckBox.addActionListener(e -> {
JCheckBox checkBox = (JCheckBox) e.getSource();
useLatestVersion = checkBox.isSelected();
});
latestVersionCheckBox.setSelected(useLatestVersion);
JButton installButton = new JButton("Install/Upgrade");
installButton.addActionListener(e -> {
// first get rid of existing packages
int[] selectedRows = dataTable.getSelectedRows();
String installedPackageNames = "";
setCursor(new Cursor(Cursor.WAIT_CURSOR));
Map<Package, PackageVersion> packagesToInstall = new HashMap<>();
AddOnManager.useArchive(!useLatestVersion);
for (int selRow : selectedRows) {
Package selPackage = getSelectedPackage(selRow);
if (selPackage != null) {
if (useLatestVersion) {
packagesToInstall.put(selPackage, selPackage.getLatestVersion());
} else {
PackageVersion version = (PackageVersion) JOptionPane.showInputDialog( null, "Select Version for " + selPackage.getName(),
"Select version",
JOptionPane.QUESTION_MESSAGE, null,
selPackage.getAvailableVersions().toArray(), selPackage.getAvailableVersions().toArray()[0]);
if (version == null) {
return;
}
packagesToInstall.put(selPackage, version);
}
}
}
try {
populatePackagesToInstall(packageMap, packagesToInstall);
prepareForInstall(packagesToInstall, false, null);
if (getToDeleteListFile().exists()) {
JOptionPane.showMessageDialog(frame,
"<html><body><p style='width: 200px'>Upgrading packages on your machine requires BEAUti " +
"to restart. Shutting down now.</p></body></html>");
System.exit(0);
}
installPackages(packagesToInstall, false, null);
// Refresh classes:
loadExternalJars();
installedPackageNames = String.join(",",
packagesToInstall.keySet().stream()
.map(Package::toString)
.collect(Collectors.toList()));
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
} catch (DependencyResolutionException | IOException ex) {
JOptionPane.showMessageDialog(null, "Install failed because: " + ex.getMessage());
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
resetPackages();
dataTable.setRowSelectionInterval(selectedRows[0], selectedRows[0]);
if (installedPackageNames.length()>0)
JOptionPane.showMessageDialog(null, "Package(s) "
+ installedPackageNames + " installed. "
+ "Note that any changes to the BEAUti "
+ "interface will\n not appear until a "
+ "new document is created or BEAUti is "
+ "restarted.");
});
box.add(installButton);
JButton uninstallButton = new JButton("Uninstall");
uninstallButton.addActionListener(e -> {
StringBuilder removedPackageNames = new StringBuilder();
int[] selectedRows = dataTable.getSelectedRows();
for (int selRow : selectedRows) {
Package selPackage = getSelectedPackage(selRow);
if (selPackage != null) {
try {
if (selPackage.isInstalled()) {
setCursor(new Cursor(Cursor.WAIT_CURSOR));
List<String> deps = getInstalledDependencyNames(selPackage, packageMap);
if (deps.isEmpty()) {
String result = uninstallPackage(selPackage, selPackage.getInstalledVersion(), false, null);
if (result != null) {
if (removedPackageNames.length() > 0)
removedPackageNames.append(", ");
removedPackageNames.append("'")
.append(selPackage.getName())
.append(" v")
.append(selPackage.getInstalledVersion())
.append("'");
}
} else {
throw new DependencyResolutionException("package " + selPackage
+ " is used by the following packages: "
+ String.join(", ", deps) + "\n"
+ "Remove those packages first.");
}
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
resetPackages();
dataTable.setRowSelectionInterval(selectedRows[0], selectedRows[0]);
} catch (IOException | DependencyResolutionException ex) {
JOptionPane.showMessageDialog(null, "Uninstall failed because: " + ex.getMessage());
setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
}
}
if (getToDeleteListFile().exists()) {
JOptionPane.showMessageDialog(frame,
"<html><body><p style='width: 200px'>Removing packages on your machine requires BEAUti " +
"to restart. Shutting down now.</p></body></html>");
System.exit(0);
}
if (removedPackageNames.length()>0)
JOptionPane.showMessageDialog(null, "Package(s) "
+ removedPackageNames.toString() + " removed. "
+ "Note that any changes to the BEAUti "
+ "interface will\n not appear until a "
+ "new document is created or BEAUti is "
+ "restarted.");
});
box.add(uninstallButton);
box.add(Box.createHorizontalGlue());
JButton packageRepoButton = new JButton("Package repositories");
packageRepoButton.addActionListener(e -> {
JPackageRepositoryDialog dlg = new JPackageRepositoryDialog(frame);
dlg.setVisible(true);
resetPackages();
});
box.add(packageRepoButton);
box.add(Box.createGlue());
JButton closeButton = new JButton("Close");
closeButton.addActionListener(e -> {
if (dlg != null) {
dlg.setVisible(false);
} else {
setVisible(false);
}
});
box.add(closeButton);
JButton button = new JButton("?");
button.setToolTipText(getPackageUserDir() + " " + getPackageSystemDir());
button.addActionListener(e -> {
JOptionPane.showMessageDialog(scrollPane, "<html>By default, packages are installed in <br><br><em>" + getPackageUserDir() +
"</em><br><br>and are available only to you.<br>" +
"<br>Packages can also be moved manually to <br><br><em>" + getPackageSystemDir() +
"</em><br><br>which makes them available to all users<br>"
+ "on your system.</html>");
});
box.add(button);
return box;
}
class DataTableModel extends AbstractTableModel {
private static final long serialVersionUID = 1L;
String[] columnNames = {"Name", "Installed", "Latest", "Dependencies", "Link", "Detail"};
public final int linkColumn = 4;
ImageIcon linkIcon = Utils.getIcon(BeautiPanel.ICONPATH + "link.png");
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getRowCount() {
return packageList.size();
}
@Override
public Object getValueAt(int row, int col) {
Package aPackage = packageList.get(row);
switch (col) {
case 0:
return aPackage.getName();
case 1:
return aPackage.getInstalledVersion();
case 2:
return aPackage.getLatestVersion();
case 3:
return aPackage.getDependenciesString();
case 4:
return aPackage.getProjectURL() != null ? linkIcon : null ;
case 5:
return aPackage.getDescription();
default:
throw new IllegalArgumentException("unknown column, " + col);
}
}
@Override
public String getColumnName(int column) {
return columnNames[column];
}
@Override
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append(getColumnName(0));
for (int j = 1; j < getColumnCount(); j++) {
buffer.append("\t");
buffer.append(getColumnName(j));
}
buffer.append("\n");
for (int i = 0; i < getRowCount(); i++) {
buffer.append(getValueAt(i, 0));
for (int j = 1; j < getColumnCount(); j++) {
buffer.append("\t");
buffer.append(getValueAt(i, j));
}
buffer.append("\n");
}
return buffer.toString();
}
}
public JDialog asDialog(JFrame frame) {
if (frame == null) {
frame = (JFrame) SwingUtilities.getWindowAncestor(this);
}
this.frame = frame;
dlg = new JDialog(frame, "BEAST 2 Package Manager", true);
dlg.getContentPane().add(scrollPane, BorderLayout.CENTER);
dlg.getContentPane().add(jLabel, BorderLayout.NORTH);
dlg.getContentPane().add(buttonBox, BorderLayout.SOUTH);
dlg.pack();
Point frameLocation = frame.getLocation();
Dimension frameSize = frame.getSize();
Dimension dim = getPreferredSize();
int size = UIManager.getFont("Label.font").getSize();
dlg.setSize(690 * size / 13, 430 * size / 13);
dlg.setLocation(frameLocation.x + frameSize.width / 2 - dim.width / 2, frameLocation.y + frameSize.height / 2 - dim.height / 2);
frame.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
return dlg;
}
JDialog dlg = null;
@Override
public void setCursor(Cursor cursor) {
if (dlg != null) {
dlg.setCursor(cursor);
} else {
super.setCursor(cursor);
}
}
class PackageTable extends JTable {
private static final long serialVersionUID = 1L;
Map<Package, PackageVersion> packagesToInstall = new HashMap<>();
public PackageTable(TableModel dm) {
super(dm);
}
@Override
public Class<?> getColumnClass(int column) {
if (column != ((DataTableModel)getModel()).linkColumn)
return String.class;
else
return ImageIcon.class;
}
@Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
Font font = c.getFont();
font.getFamily();
Font boldFont = new Font(font.getName(), Font.BOLD | Font.ITALIC, font.getSize());
Package pkg = packageList.get(row);
if (! isRowSelected(row)) {
if (pkg.newVersionAvailable()) {
if (pkg.isInstalled())
c.setFont(boldFont);
if (column == 2) {
packagesToInstall.clear();
packagesToInstall.put(pkg, pkg.getLatestVersion());
try {
populatePackagesToInstall(packageMap, packagesToInstall);
c.setForeground(new Color(0, 150, 0));
} catch (DependencyResolutionException ex) {
c.setForeground(new Color(150, 0, 0));
}
} else {
c.setForeground(Color.BLACK);
}
} else {
c.setForeground(Color.BLACK);
}
}
return c;
}
/**
* Calculate the width based on the widest cell renderer for the
* given column.
*
* @param cIdx column index
* @return maximum width.
*/
private int getColumnDataWidth(int cIdx)
{
int preferredWidth = 0;
int maxWidth = getColumnModel().getColumn(cIdx).getMaxWidth();
for (int row = 0; row < getRowCount(); row++)
{
preferredWidth = Math.max(preferredWidth, getCellDataWidth(row, cIdx));
// We've exceeded the maximum width, no need to check other rows
if (preferredWidth >= maxWidth)
break;
}
preferredWidth = Math.max(preferredWidth, getHeaderWidth(cIdx));
return preferredWidth;
}
/*
* Get the preferred width for the specified cell
*/
private int getCellDataWidth(int row, int column)
{
// Inovke the renderer for the cell to calculate the preferred width
TableCellRenderer cellRenderer = getCellRenderer(row, column);
Component c = prepareRenderer(cellRenderer, row, column);
return c.getPreferredSize().width + 2*getIntercellSpacing().width;
}
/*
* Get the preferred width for the specified header
*/
private int getHeaderWidth(int cIdx)
{
// Inovke the renderer for the cell to calculate the preferred width
TableColumn column = getColumnModel().getColumn(cIdx);
TableCellRenderer cellRenderer = getDefaultRenderer(String.class);
Component c = cellRenderer.getTableCellRendererComponent(this, column.getHeaderValue(), false, false, -1, cIdx);
return c.getPreferredSize().width + 2*getIntercellSpacing().width;
}
void updateWidths() {
for (int cIdx = 0; cIdx < getColumnCount(); cIdx++) {
int width = getColumnDataWidth(cIdx);
TableColumn column = getColumnModel().getColumn(cIdx);
getTableHeader().setResizingColumn(column);
column.setWidth(width);
}
}
}
}