package beast.app.tools; import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Point; import java.awt.Rectangle; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.io.BufferedReader; import java.io.File; import java.io.InputStreamReader; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; import java.util.TreeSet; import javax.swing.Box; import javax.swing.DefaultListCellRenderer; import javax.swing.DefaultListModel; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JDialog; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.UIManager; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import beast.app.util.Utils; import beast.core.util.Log; import beast.util.AddOnManager; /** * launch applications specific to add-ons installed, for example utilities for * post-processing add-on specific data. * * @author Remco Bouckaert * @author Walter Xie */ public class AppStore { public static final String DEFAULT_ICON = "beast/app/tools/images/utility.png"; private final String ALL = "-all-"; JComboBox<String> packageComboBox; DefaultListModel<PackageApp> model = new DefaultListModel<>(); JList<PackageApp> listApps; JButton launchButton = new JButton("Launch"); JDialog mainDialog; public AppStore() { } public JDialog launchGUI() { mainDialog = new JDialog(); mainDialog.setTitle("BEAST 2 Package Application Launcher"); Box top = Box.createHorizontalBox(); JLabel label = new JLabel("Filter: "); packageComboBox = new JComboBox<>(new String[]{ALL}); packageComboBox.setToolTipText("Show application of the installed package(s)"); packageComboBox.addActionListener(e -> { JComboBox<?> cb = (JComboBox<?>) e.getSource(); if (cb.getSelectedItem() != null) { resetAppList(cb.getSelectedItem().toString()); } }); label.setLabelFor(packageComboBox); top.add(label); top.add(packageComboBox); mainDialog.getContentPane().add(BorderLayout.NORTH, top); Component beastObjectListBox = createList(); mainDialog.getContentPane().add(BorderLayout.CENTER, beastObjectListBox); Box buttonBox = createButtonBox(); mainDialog.getContentPane().add(buttonBox, BorderLayout.SOUTH); // Dimension dim = panel.getPreferredSize(); // Dimension dim2 = buttonBox.getPreferredSize(); // setSize(dim.width + 10, dim.height + dim2.height + 30); int size = UIManager.getFont("Label.font").getSize(); mainDialog.setSize(600 * size / 13, 400 * size / 13); mainDialog.setLocationRelativeTo(null); return mainDialog; } private Component createList() { Box box = Box.createVerticalBox(); box.add(Box.createGlue()); listApps = new JList<PackageApp>(model) { private static final long serialVersionUID = 1L; //Subclass JList to workaround bug 4832765, which can cause the //scroll pane to not let the user easily scroll up to the beginning //of the list. An alternative would be to set the unitIncrement //of the JScrollBar to a fixed value. You wouldn't get the nice //aligned scrolling, but it should work. @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { int row; if (orientation == SwingConstants.VERTICAL && direction < 0 && (row = getFirstVisibleIndex()) != -1) { Rectangle r = getCellBounds(row, row); if ((r.y == visibleRect.y) && (row != 0)) { Point loc = r.getLocation(); loc.y--; int prevIndex = locationToIndex(loc); Rectangle prevR = getCellBounds(prevIndex, prevIndex); if (prevR == null || prevR.y >= r.y) { return 0; } return prevR.height; } } return super.getScrollableUnitIncrement( visibleRect, orientation, direction); } }; listApps.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); listApps.setCellRenderer(new DefaultListCellRenderer() { private static final long serialVersionUID = 1L; @Override public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super .getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); label.setIcon(((PackageApp) value).icon); label.setHorizontalTextPosition(SwingConstants.CENTER); label.setVerticalTextPosition(SwingConstants.BOTTOM); return label; } }); listApps.setLayoutOrientation(JList.HORIZONTAL_WRAP); listApps.setVisibleRowCount(-1); listApps.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { launchButton.doClick(); } } }); // if (model.getSize() > 0) { // TODO not working // listApps.setPrototypeCellValue(model.firstElement()); //get extra space // } resetAppList(); JScrollPane listScroller = new JScrollPane(listApps); listScroller.setPreferredSize(new Dimension(660, 400)); listScroller.setAlignmentX(Component.LEFT_ALIGNMENT); JLabel label = new JLabel("List of available package applications"); label.setLabelFor(listApps); box.add(label); box.add(listScroller); return box; } private void resetAppList() { Set<String> packages = new TreeSet<>(); model.clear(); try { List<PackageApp> packageApps = getPackageApps(); for (PackageApp packageApp : packageApps) { model.addElement(packageApp); packages.add(packageApp.packageName); } } catch (Exception e) { e.printStackTrace(); } listApps.setSelectedIndex(0); packageComboBox.removeAllItems(); packageComboBox.addItem(ALL); for (String p : packages) { packageComboBox.addItem(p); } } private void resetAppList(String packageName) { model.clear(); try { List<PackageApp> packageApps = getPackageApps(); for (PackageApp packageApp : packageApps) { if (packageName.equals(ALL) || packageName.equalsIgnoreCase(packageApp.packageName)) model.addElement(packageApp); } } catch (Exception e) { e.printStackTrace(); } listApps.setSelectedIndex(0); } private Box createButtonBox() { Box box = Box.createHorizontalBox(); box.add(Box.createGlue()); launchButton.addActionListener(e -> { PackageApp packageApp = listApps.getSelectedValue(); if (packageApp != null) { try { new PackageAppThread(packageApp).start(); } catch (Exception ex) { JOptionPane.showMessageDialog(null, "Launch failed because: " + ex.getMessage()); } } }); box.add(launchButton); JButton closeButton = new JButton("Close"); closeButton.addActionListener(e -> { // setVisible(false); mainDialog.dispose(); }); box.add(Box.createGlue()); box.add(closeButton); box.add(Box.createGlue()); return box; } List<PackageApp> getPackageApps() { List<PackageApp> packageApps = new ArrayList<>(); List<String> dirs = AddOnManager.getBeastDirectories(); for (String jarDirName : dirs) { File versionFile = new File(jarDirName + "/version.xml"); if (versionFile.exists() && versionFile.isFile()) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document doc; try { doc = factory.newDocumentBuilder().parse(versionFile); doc.normalize(); // get addonapp info from version.xml Element addon = doc.getDocumentElement(); NodeList nodes = doc.getElementsByTagName("addonapp"); for (int j = 0; j < nodes.getLength(); j++) { Element addOnAppElement = (Element) nodes.item(j); PackageApp packageApp = new PackageApp(); packageApp.packageName = addon.getAttribute("name"); packageApp.jarDir = jarDirName; packageApp.className = addOnAppElement.getAttribute("class"); packageApp.description = addOnAppElement.getAttribute("description"); packageApp.argumentsString = addOnAppElement.getAttribute("args"); String iconLocation = addOnAppElement.getAttribute("icon"); packageApp.icon = Utils.getIcon(iconLocation); if (packageApp.icon == null || iconLocation.trim().isEmpty()) packageApp.icon = Utils.getIcon(DEFAULT_ICON); packageApps.add(packageApp); } } catch (Exception e) { // ignore System.err.println(e.getMessage()); } } } return packageApps; } /** * package application information required for launching the app and * displaying in list box **/ class PackageApp { String packageName; String jarDir; String description; String className; String argumentsString; ImageIcon icon; public String[] getArgs() { if (argumentsString == null || argumentsString.trim().isEmpty()) { return new String[]{}; } else { String[] args = argumentsString.split(" ", -1); // System.out.println("package = " + packageName + ", class = " + className + ", args = " + Arrays.toString(args)); return args; } } @Override public String toString() { return description; } } /** thread for launching add on application **/ class PackageAppThread extends Thread { PackageApp packageApp; PackageAppThread(PackageApp packageApp) { this.packageApp = packageApp; } @Override public void run() { // invoke package application // AppStore.runAppFromJar(packageApp.className, packageApp.getArgs()); runAppFromCMD(packageApp, null); } } public void runAppFromCMD(PackageApp packageApp, String[] additionalArgs) { try { AddOnManager.loadExternalJars(); List<String> cmd = new ArrayList<>(); if (System.getenv("JAVA_HOME") != null) { cmd.add(System.getenv("JAVA_HOME") + File.separatorChar + "bin" + File.separatorChar + "java"); } else cmd.add("java"); // TODO: deal with java directives like -Xmx -Xms here if (System.getProperty("java.library.path") != null && System.getProperty("java.library.path").length() > 0) { cmd.add("-Djava.library.path=" + sanitise(System.getProperty("java.library.path"))); } cmd.add("-cp"); final String strClassPath = sanitise(System.getProperty("java.class.path")); cmd.add(strClassPath); cmd.add(packageApp.className); for (String arg : packageApp.getArgs()) { cmd.add(arg); } if (additionalArgs != null) { for (String arg : additionalArgs) cmd.add(arg); } final ProcessBuilder pb = new ProcessBuilder(cmd); System.err.println(pb.command()); //File log = new File("log"); pb.redirectErrorStream(true); // Start the process and wait for it to finish. final Process process = pb.start(); int c; BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream())); while ((c = input.read()) != -1) { Log.info.print((char)c); } input.close(); final int exitStatus = process.waitFor(); if (exitStatus != 0) { Log.err.println(Utils.toString(process.getErrorStream())); } else { // System.out.println(Utils.toString(process.getInputStream())); } } catch (Exception e) { e.printStackTrace(); } } private String sanitise(String property) { // sanitise for windows if (beast.app.util.Utils.isWindows()) { String cwd = System.getProperty("user.dir"); cwd = cwd.replace("\\", "/"); property = property.replaceAll(";\\.", ";" + cwd + "."); property = property.replace("\\", "/"); } return property; } private void printUsage(PrintStream ps) { ps.println("\nAppStore: Run installed BEAST 2 package apps.\n" + "\n" + "Usage:\n" + "\tappstore\n" + "\tappstore -help\n" + "\tappstore -list [package_name]\n" + "\tappstore <app_class_name|app_description>"); } private void printAppList(List<PackageApp> appList, PrintStream ps) { int maxPNlen = appList.stream().mapToInt(x -> x.packageName.length()).max().orElse(0); maxPNlen = Math.max(maxPNlen+1, 15); int maxCNlen = appList.stream().mapToInt(x -> { String[] components = x.className.split("\\."); return components[components.length-1].length(); }).max().orElse(0); maxCNlen = Math.max(maxCNlen+1, 15); String formatStr = "%-" + maxPNlen + "." + maxPNlen + "s|%-" + maxCNlen + "." + maxCNlen + "s|%s\n"; ps.format(formatStr, "Package", "Class", "Description"); ps.print(String.format(formatStr, "", "", "--------------------").replace(" ", "-")); for (PackageApp app : appList) { String[] fullClassName = app.className.split("\\."); String className = fullClassName[fullClassName.length-1]; ps.format(formatStr, app.packageName, className, app.description); } } public static void main(String[] args) { AppStore appStore = new AppStore(); if (args.length == 0) { Utils.loadUIManager(); SwingUtilities.invokeLater(() -> appStore.launchGUI().setVisible(true)); } else { if (args[0].startsWith("-")) { switch(args[0]) { case "-help": appStore.printUsage(System.out); System.exit(0); case "-list": System.out.println("\nAvailable package apps:\n"); if (args.length>1) { String packageNameFilter = args[1].toLowerCase(); List<PackageApp> filteredAppList = new ArrayList<>(); for (PackageApp app : appStore.getPackageApps()) { if (!app.packageName.toLowerCase().contains(packageNameFilter)) filteredAppList.add(app); } appStore.printAppList(filteredAppList, System.out); } else { appStore.printAppList(appStore.getPackageApps(), System.out); } System.exit(0); default: System.err.print("\nUnsupported option."); appStore.printUsage(System.err); System.exit(1); } } else { // Find apps with class name or description that matches // command line. List<PackageApp> partialMatchingApps = new ArrayList<>(); PackageApp exactMatchApp = null; for (PackageApp app : appStore.getPackageApps()) { if (app.className.equals(args[0]) || app.description.equals(args[0])) exactMatchApp = app; else { if (app.className.toLowerCase().contains(args[0].toLowerCase()) || app.description.toLowerCase().contains(args[0].toLowerCase())) partialMatchingApps.add(app); } } String[] packageArgs = Arrays.copyOfRange(args, 1, args.length); if (exactMatchApp != null) appStore.runAppFromCMD(exactMatchApp, packageArgs); else { if (partialMatchingApps.size()==1) { appStore.runAppFromCMD(partialMatchingApps.get(0), packageArgs); } else { if (partialMatchingApps.isEmpty()) { System.err.println("\nNo apps match."); appStore.printUsage(System.err); System.exit(1); } else { System.err.println("\nMultiple apps match:\n"); appStore.printAppList(partialMatchingApps, System.err); } System.exit(1); } } } } } }