/*
Copyright (C) 2006 EBI
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the itmplied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.biomart.builder.view.gui.dialogs;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.net.Socket;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.ListCellRenderer;
import javax.swing.ListSelectionModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.WindowConstants;
import javax.swing.border.EmptyBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.biomart.common.resources.Log;
import org.biomart.common.resources.Resources;
import org.biomart.common.view.gui.DraggableJTree;
import org.biomart.common.view.gui.LongProcess;
import org.biomart.common.view.gui.dialogs.StackTrace;
import org.biomart.runner.controller.MartRunnerProtocol.Client;
import org.biomart.runner.exceptions.ProtocolException;
import org.biomart.runner.model.JobPlan;
import org.biomart.runner.model.JobStatus;
import org.biomart.runner.model.JobPlan.JobPlanAction;
import org.biomart.runner.model.JobPlan.JobPlanSection;
/**
* This dialog monitors and interacts with SQL being run on a remote host.
*
* @author Richard Holland <holland@ebi.ac.uk>
* @version $Revision: 1.31 $, $Date: 2007-12-20 15:38:46 $, modified by
* $Author: rh4 $
* @since 0.6
*/
public class MartRunnerMonitorDialog extends JFrame {
private static final long serialVersionUID = 1;
private static final int DEFAULT_REFRESH = 60; // seconds
private static final int MIN_REFRESH = 5; // seconds
private static final Font PLAIN_FONT = Font.decode("SansSerif-PLAIN-12");
private static final Font ITALIC_FONT = Font.decode("SansSerif-ITALIC-12");
private static final Font BOLD_FONT = Font.decode("SansSerif-BOLD-12");
private static final Font BOLD_ITALIC_FONT = Font
.decode("SansSerif-BOLDITALIC-12");
private static final Color PALE_BLUE = Color.decode("0xEEEEFF");
private static final Color PALE_GREEN = Color.decode("0xEEFFEE");
private static final Map STATUS_COLOR_MAP = new HashMap();
private static final Map STATUS_FONT_MAP = new HashMap();
private final JButton refreshJobList;
private boolean listRefreshing = false;
private final String host;
private final String port;
static {
// Colours.
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.NOT_QUEUED,
Color.BLACK);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.INCOMPLETE,
Color.CYAN);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.QUEUED,
Color.MAGENTA);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.FAILED,
Color.RED);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.RUNNING,
Color.BLUE);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.STOPPED,
Color.ORANGE);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.COMPLETED,
Color.GREEN);
MartRunnerMonitorDialog.STATUS_COLOR_MAP.put(JobStatus.UNKNOWN,
Color.LIGHT_GRAY);
// Fonts
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.NOT_QUEUED,
MartRunnerMonitorDialog.PLAIN_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.INCOMPLETE,
MartRunnerMonitorDialog.ITALIC_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.QUEUED,
MartRunnerMonitorDialog.PLAIN_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.FAILED,
MartRunnerMonitorDialog.BOLD_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.RUNNING,
MartRunnerMonitorDialog.BOLD_ITALIC_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.STOPPED,
MartRunnerMonitorDialog.BOLD_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.COMPLETED,
MartRunnerMonitorDialog.PLAIN_FONT);
MartRunnerMonitorDialog.STATUS_FONT_MAP.put(JobStatus.UNKNOWN,
MartRunnerMonitorDialog.ITALIC_FONT);
}
/**
* Opens an explanation showing what a remote MartRunner host is up to.
*
* @param host
* the host to monitor.
* @param port
* the port to connect to the host with.
*/
public static void monitor(final String host, final String port) {
// Open the dialog.
new MartRunnerMonitorDialog(host, port).setVisible(true);
}
private MartRunnerMonitorDialog(final String host, final String port) {
// Create the blank dialog, and give it an appropriate title.
super(Resources.get("monitorDialogTitle", new String[] { host, port }));
this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
// Make the RHS scrollpane containing job descriptions.
final JobPlanPanel jobPlanPanel = new JobPlanPanel(this, host, port);
this.host = host;
this.port = port;
// Make the LHS list of jobs.
final JobPlanListModel jobPlanListModel = new JobPlanListModel(host,
port);
final JList jobList = new JList(jobPlanListModel);
jobList.setCellRenderer(new JobPlanListCellRenderer());
jobList.setBackground(Color.WHITE);
jobList.setOpaque(true);
jobList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.refreshJobList = new JButton(Resources.get("refreshButton"));
final JTextField refreshRate = new JTextField(""
+ MartRunnerMonitorDialog.DEFAULT_REFRESH, 5);
final JPanel jobListPanel = new JPanel(new BorderLayout());
jobListPanel.setBorder(new EmptyBorder(new Insets(2, 2, 2, 2)));
jobListPanel.add(new JLabel(Resources.get("jobListTitle")),
BorderLayout.PAGE_START);
jobListPanel.add(new JScrollPane(jobList), BorderLayout.CENTER);
final JPanel refreshPanel = new JPanel();
refreshPanel.add(this.refreshJobList);
refreshPanel.add(refreshRate);
jobListPanel.add(refreshPanel, BorderLayout.PAGE_END);
// Updates when refresh button is hit.
this.refreshJobList.addActionListener(new ActionListener() {
private boolean firstRun = true;
public void actionPerformed(final ActionEvent e) {
new LongProcess() {
public void run() {
if (MartRunnerMonitorDialog.this.listRefreshing)
return;
Object selection = jobList.getSelectedValue();
try {
MartRunnerMonitorDialog.this.listRefreshing = true;
jobPlanListModel.updateList();
} catch (final ProtocolException e) {
StackTrace.showStackTrace(e);
} finally {
MartRunnerMonitorDialog.this.listRefreshing = false;
// Attempt to select the first item on first run.
if (firstRun && jobPlanListModel.size() > 0)
selection = jobPlanListModel.lastElement();
jobList.setSelectedValue(selection, true);
firstRun = false;
}
}
}.start();
}
});
// Add a listener to the list to update the pane on the right.
jobList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(final ListSelectionEvent e) {
final Object selection = jobList.getSelectedValue();
if (!e.getValueIsAdjusting()
&& !MartRunnerMonitorDialog.this.listRefreshing)
// Update the panel on the right with the new job.
jobPlanPanel
.setJobPlan(selection instanceof JobPlan ? (JobPlan) selection
: null);
}
});
// Add context menu to the job list.
jobList.addMouseListener(new MouseListener() {
public void mouseClicked(final MouseEvent e) {
this.doMouse(e);
}
public void mouseEntered(final MouseEvent e) {
this.doMouse(e);
}
public void mouseExited(final MouseEvent e) {
this.doMouse(e);
}
public void mousePressed(final MouseEvent e) {
this.doMouse(e);
}
public void mouseReleased(final MouseEvent e) {
this.doMouse(e);
}
private void doMouse(final MouseEvent e) {
if (e.isPopupTrigger()) {
final int index = jobList.locationToIndex(e.getPoint());
if (index >= 0) {
final JobPlan plan = (JobPlan) jobPlanListModel
.getElementAt(index);
final JPopupMenu menu = new JPopupMenu();
// Remove job.
final JMenuItem empty = new JMenuItem(Resources
.get("emptyTableJobTitle"));
empty.setMnemonic(Resources
.get("emptyTableJobMnemonic").charAt(0));
empty.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent evt) {
new LongProcess() {
public void run() throws Exception {
// Do the job.
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.makeEmptyTableJob(clientSocket,
plan.getJobId());
clientSocket.close();
}
}.start();
}
});
menu.add(empty);
menu.addSeparator();
// Remove job.
final JMenuItem remove = new JMenuItem(Resources
.get("removeJobTitle"));
remove.setMnemonic(Resources.get("removeJobMnemonic")
.charAt(0));
remove.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent evt) {
// Confirm.
if (JOptionPane.showConfirmDialog(jobList,
Resources.get("removeJobConfirm"),
Resources.get("questionTitle"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
new LongProcess() {
public void run() throws Exception {
// Remove the job.
final Socket clientSocket = Client
.createClientSocket(host,
port);
Client.removeJob(clientSocket, plan
.getJobId());
clientSocket.close();
// Update the list.
MartRunnerMonitorDialog.this.refreshJobList
.doClick();
}
}.start();
}
});
menu.add(remove);
// Remove all job.
final JMenuItem removeAll = new JMenuItem(Resources
.get("removeAllJobsTitle"));
removeAll.setMnemonic(Resources.get(
"removeAllJobsMnemonic").charAt(0));
removeAll.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent evt) {
// Confirm.
if (JOptionPane.showConfirmDialog(jobList,
Resources.get("removeAllJobsConfirm"),
Resources.get("questionTitle"),
JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)
new LongProcess() {
public void run() throws Exception {
// Remove all jobs.
final Enumeration e = jobPlanListModel
.elements();
final Socket clientSocket = Client
.createClientSocket(host,
port);
while (e.hasMoreElements())
Client.removeJob(clientSocket,
((JobPlan) e
.nextElement())
.getJobId());
clientSocket.close();
// Update the list.
MartRunnerMonitorDialog.this.refreshJobList
.doClick();
}
}.start();
}
});
menu.add(removeAll);
// Show the menu.
menu.show(jobList, e.getX(), e.getY());
e.consume();
}
}
}
});
// Set up a timer to update the list.
final class TimerUpdate extends TimerTask {
public void run() {
MartRunnerMonitorDialog.this.refreshJobList.doClick();
}
}
final class TimerListener extends WindowAdapter implements
DocumentListener {
private Timer timer = new Timer();
// Called after the constructor.
{
this.timer.schedule(new TimerUpdate(), 0,
MartRunnerMonitorDialog.DEFAULT_REFRESH * 1000);
}
public void changedUpdate(final DocumentEvent e) {
this.updateTimer();
}
public void insertUpdate(final DocumentEvent e) {
this.updateTimer();
}
public void removeUpdate(final DocumentEvent e) {
this.updateTimer();
}
private void updateTimer() {
String val = refreshRate.getText();
if (val == null)
val = "";
int delay;
try {
delay = Integer.parseInt(val);
} catch (final NumberFormatException ne) {
delay = 0;
}
// Can't have it too short.
delay = delay == 0 ? 0 : Math.max(delay,
MartRunnerMonitorDialog.MIN_REFRESH) * 1000;
// Reschedule.
this.timer.cancel();
if (delay > 0) {
this.timer = new Timer();
this.timer.schedule(new TimerUpdate(), delay, delay);
}
}
public void windowClosed(final WindowEvent e) {
this.timer.cancel();
}
public void windowClosing(final WindowEvent e) {
this.timer.cancel();
}
}
final TimerListener timerListener = new TimerListener();
refreshRate.getDocument().addDocumentListener(timerListener);
this.addWindowListener(timerListener);
// Make the content pane.
final JSplitPane splitPane = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT, false, jobListPanel, jobPlanPanel);
splitPane.setOneTouchExpandable(true);
// Set up our content pane.
this.setContentPane(splitPane);
// Pack the window.
this.pack();
// Move ourselves.
this.setLocationRelativeTo(null);
}
// Renders cells nicely.
private static class JobPlanListCellRenderer implements ListCellRenderer {
private static final long serialVersionUID = 1L;
public Component getListCellRendererComponent(final JList list,
final Object value, final int index, final boolean isSelected,
final boolean cellHasFocus) {
final JLabel label = new JLabel(value.toString());
label.setOpaque(true);
Color fgColor = Color.BLACK;
Color bgColor = Color.WHITE;
Font font = MartRunnerMonitorDialog.PLAIN_FONT;
// A Job Plan entry node?
if (value instanceof JobPlan) {
final JobStatus status = ((JobPlan) value).getRoot()
.getStatus();
// White/Cyan stripes.
bgColor = index % 2 == 0 ? Color.WHITE
: MartRunnerMonitorDialog.PALE_BLUE;
// Color-code text.
fgColor = (Color) MartRunnerMonitorDialog.STATUS_COLOR_MAP
.get(status);
// Set font.
font = (Font) MartRunnerMonitorDialog.STATUS_FONT_MAP
.get(status);
}
// Always white-on-color or color-on-white.
label.setFont(font);
label.setForeground(isSelected ? bgColor : fgColor);
label.setBackground(isSelected ? fgColor : bgColor);
// Others get no extra material.
return label;
}
}
// Renders cells nicely.
private static class JobPlanTreeCellRenderer implements TreeCellRenderer {
private static final long serialVersionUID = 1L;
public Component getTreeCellRendererComponent(final JTree tree,
final Object value, final boolean sel, final boolean expanded,
final boolean leaf, final int row, final boolean hasFocus) {
final JLabel label = new JLabel(value.toString());
label.setOpaque(true);
Color fgColor = Color.BLACK;
Color bgColor = Color.WHITE;
Font font = MartRunnerMonitorDialog.PLAIN_FONT;
// Sections are given text labels.
if (value instanceof SectionNode) {
final JobStatus status = ((SectionNode) value).getSection()
.getStatus();
// White/Cyan stripes.
bgColor = row % 2 == 0 ? Color.WHITE
: MartRunnerMonitorDialog.PALE_BLUE;
// Color-code text.
fgColor = (Color) MartRunnerMonitorDialog.STATUS_COLOR_MAP
.get(status);
// Set font.
font = (Font) MartRunnerMonitorDialog.STATUS_FONT_MAP
.get(status);
}
// Actions are given text labels.
else if (value instanceof ActionNode) {
final JobStatus status = ((ActionNode) value).getAction()
.getStatus();
// White/Cyan stripes.
bgColor = row % 2 == 0 ? Color.WHITE
: MartRunnerMonitorDialog.PALE_GREEN;
// Color-code text.
fgColor = (Color) MartRunnerMonitorDialog.STATUS_COLOR_MAP
.get(status);
// Set font.
font = (Font) MartRunnerMonitorDialog.STATUS_FONT_MAP
.get(status);
}
// Always white-on-color or color-on-white.
label.setFont(font);
label.setForeground(sel ? bgColor : fgColor);
label.setBackground(sel ? fgColor : bgColor);
// Everything else is default.
return label;
}
}
// A model for representing lists of jobs.
private static class JobPlanListModel extends DefaultListModel {
private static final long serialVersionUID = 1L;
private final String host;
private final String port;
private JobPlanListModel(final String host, final String port) {
super();
this.host = host;
this.port = port;
}
private void updateList() throws ProtocolException {
try {
// Communicate and update model.
this.removeAllElements();
final Socket clientSocket = Client.createClientSocket(
this.host, this.port);
for (final Iterator i = Client.listJobs(clientSocket)
.getAllJobs().iterator(); i.hasNext();)
this.addElement(i.next());
clientSocket.close();
} catch (final Throwable t) {
throw new ProtocolException(t);
}
}
}
// A panel for showing the job plans in.
private static class JobPlanPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final String host;
private final String port;
private JTree tree;
private JobPlanTreeModel treeModel;
private String jobId;
private final JTextField jobIdField;
private final JSpinner threadSpinner;
private final SpinnerNumberModel threadSpinnerModel;
private final JTextField jdbcUrl;
private final JTextField jdbcUser;
private final JTextField contactEmail;
private final JButton updateEmailButton;
private final JFormattedTextField started;
private final JFormattedTextField finished;
private final JTextField elapsed;
private final JTextField status;
private final JTextArea messages;
private final JButton startJob;
private final JButton stopJob;
private final JCheckBox skipDropTable;
/**
* Create a new job description panel. In the top half goes two panes -
* an email settings pane, and the job tree view. In the bottom half
* goes an explanation panel.
*
* @param parentDialog
* the dialog we are displaying in.
* @param host
* the host to talk to MartRunner at.
* @param port
* the port to talk to MartRunner at.
*/
public JobPlanPanel(final MartRunnerMonitorDialog parentDialog,
final String host, final String port) {
super(new BorderLayout(2, 2));
this.setBorder(new EmptyBorder(new Insets(2, 2, 2, 2)));
this.host = host;
this.port = port;
// Create constraints for labels that are not in the last row.
final GridBagConstraints labelConstraints = new GridBagConstraints();
labelConstraints.gridwidth = GridBagConstraints.RELATIVE;
labelConstraints.fill = GridBagConstraints.HORIZONTAL;
labelConstraints.anchor = GridBagConstraints.LINE_END;
labelConstraints.insets = new Insets(0, 2, 0, 0);
// Create constraints for fields that are not in the last row.
final GridBagConstraints fieldConstraints = new GridBagConstraints();
fieldConstraints.gridwidth = GridBagConstraints.REMAINDER;
fieldConstraints.fill = GridBagConstraints.NONE;
fieldConstraints.anchor = GridBagConstraints.LINE_START;
fieldConstraints.insets = new Insets(0, 1, 0, 2);
// Create constraints for labels that are in the last row.
final GridBagConstraints labelLastRowConstraints = (GridBagConstraints) labelConstraints
.clone();
labelLastRowConstraints.gridheight = GridBagConstraints.REMAINDER;
// Create constraints for fields that are in the last row.
final GridBagConstraints fieldLastRowConstraints = (GridBagConstraints) fieldConstraints
.clone();
fieldLastRowConstraints.gridheight = GridBagConstraints.REMAINDER;
// Create a panel to hold the header details.
final JPanel headerPanel = new JPanel(new GridBagLayout());
// Create the user-interactive bits of the panel.
this.threadSpinnerModel = new SpinnerNumberModel(1, 1, 1, 1);
this.threadSpinner = new JSpinner(this.threadSpinnerModel);
// Spinner listener updates summary thread count instantly.
this.threadSpinnerModel.addChangeListener(new ChangeListener() {
public void stateChanged(final ChangeEvent e) {
if (JobPlanPanel.this.jobId != null)
try {
final Socket clientSocket = Client
.createClientSocket(host, port);
Client
.setThreadCount(
clientSocket,
JobPlanPanel.this.jobId,
((Integer) JobPlanPanel.this.threadSpinnerModel
.getValue()).intValue());
clientSocket.close();
} catch (final Throwable pe) {
StackTrace.showStackTrace(pe);
}
}
});
// Populate the header panel.
JLabel label = new JLabel(Resources.get("jobIdLabel"));
headerPanel.add(label, labelConstraints);
JPanel field = new JPanel();
this.jobIdField = new JTextField(12);
this.jobIdField.setEnabled(false);
field.add(this.jobIdField);
field.add(new JLabel(Resources.get("threadCountLabel")));
field.add(this.threadSpinner);
headerPanel.add(field, fieldConstraints);
label = new JLabel(Resources.get("jdbcURLLabel"));
headerPanel.add(label, labelConstraints);
field = new JPanel();
this.jdbcUrl = new JTextField(30);
this.jdbcUrl.setEnabled(false);
field.add(this.jdbcUrl);
field.add(new JLabel(Resources.get("usernameLabel")));
this.jdbcUser = new JTextField(12);
this.jdbcUser.setEnabled(false);
field.add(this.jdbcUser);
headerPanel.add(field, fieldConstraints);
label = new JLabel(Resources.get("contactEmailLabel"));
headerPanel.add(label, labelConstraints);
field = new JPanel();
this.contactEmail = new JTextField(30);
field.add(this.contactEmail);
this.updateEmailButton = new JButton(Resources.get("updateButton"));
// Listener on button to instantly update email address.
this.updateEmailButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
if (JobPlanPanel.this.jobId != null)
try {
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.setEmailAddress(clientSocket,
JobPlanPanel.this.jobId,
JobPlanPanel.this.contactEmail.getText()
.trim());
clientSocket.close();
} catch (final Throwable pe) {
StackTrace.showStackTrace(pe);
}
}
});
field.add(this.updateEmailButton);
headerPanel.add(field, fieldConstraints);
headerPanel.add(new JLabel(), labelLastRowConstraints);
field = new JPanel();
this.startJob = new JButton(Resources.get("startJobButton"));
this.stopJob = new JButton(Resources.get("stopJobButton"));
// Button listeners to start+stop jobs.
this.startJob.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
if (JobPlanPanel.this.jobId != null)
try {
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.startJob(clientSocket,
JobPlanPanel.this.jobId);
clientSocket.close();
JobPlanPanel.this.startJob.setEnabled(false);
} catch (final Throwable pe) {
StackTrace.showStackTrace(pe);
}
}
});
this.stopJob.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
if (JobPlanPanel.this.jobId != null)
try {
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.stopJob(clientSocket,
JobPlanPanel.this.jobId);
clientSocket.close();
JobPlanPanel.this.stopJob.setEnabled(false);
} catch (final Throwable pe) {
StackTrace.showStackTrace(pe);
}
}
});
this.skipDropTable = new JCheckBox(Resources
.get("skipDropTableLabel"));
this.skipDropTable.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
if (JobPlanPanel.this.jobId != null)
try {
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.setSkipDropTable(clientSocket,
JobPlanPanel.this.jobId,
JobPlanPanel.this.skipDropTable
.isSelected());
clientSocket.close();
} catch (final Throwable pe) {
StackTrace.showStackTrace(pe);
}
}
});
field.add(this.startJob);
field.add(this.stopJob);
field.add(this.skipDropTable);
headerPanel.add(field, fieldLastRowConstraints);
// Create a panel to hold the footer details.
final JPanel footerPanel = new JPanel(new GridBagLayout());
// Populate the footer panel.
label = new JLabel(Resources.get("statusLabel"));
footerPanel.add(label, labelConstraints);
field = new JPanel();
this.status = new JTextField(12);
this.status.setEnabled(false);
field.add(this.status);
field.add(new JLabel(Resources.get("elapsedLabel")));
this.elapsed = new JTextField(12);
this.elapsed.setEnabled(false);
field.add(this.elapsed);
field.add(new JLabel(Resources.get("startedLabel")));
this.started = new JFormattedTextField(new SimpleDateFormat());
this.started.setColumns(12);
this.started.setEnabled(false);
field.add(this.started);
field.add(new JLabel(Resources.get("finishedLabel")));
this.finished = new JFormattedTextField(new SimpleDateFormat());
this.finished.setColumns(12);
this.finished.setEnabled(false);
field.add(this.finished);
footerPanel.add(field, fieldConstraints);
label = new JLabel(Resources.get("messagesLabel"));
footerPanel.add(label, labelConstraints);
field = new JPanel();
this.messages = new JTextArea(6, 60);
this.messages.setEnabled(false);
field.add(new JScrollPane(this.messages));
footerPanel.add(field, fieldConstraints);
// Create the tree and default model.
// Create a JTree to hold job details.
this.treeModel = new JobPlanTreeModel(this.host, this.port, this,
parentDialog);
this.tree = new DraggableJTree(this.treeModel) {
private static final long serialVersionUID = 1L;
public boolean isPathEditable(final TreePath path) {
return path.getPathCount() > 0
&& path.getLastPathComponent() instanceof ActionNode;
}
public boolean isValidDragPath(final TreePath path) {
// Drag-and-drop of level-1 nodes (ie. first level
// below root only)
return path.getPathCount() == 2;
}
public boolean isValidDropPath(final TreePath path) {
// Drag-and-drop of level-1 nodes (ie. first level
// below root only) PLUS level-0 node (root).
return path.getPathCount() <= 2;
}
public void dragCompleted(final int action,
final TreePath from, final TreePath to) {
// On successful drag-and-drop, call move-section
// with the identifiers of the source and target sections.
final JobPlanSection fromSection = ((SectionNode) from
.getLastPathComponent()).getSection();
final JobPlanSection toSection = ((SectionNode) to
.getLastPathComponent()).getSection();
// Confirm.
new LongProcess() {
public void run() throws Exception {
// Queue the job.
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.moveSection(clientSocket,
JobPlanPanel.this.jobId, fromSection
.getIdentifier(),
toSection == treeModel.getRoot() ? null
: toSection.getIdentifier());
clientSocket.close();
// Update the list.
parentDialog.refreshJobList.doClick();
}
}.start();
}
};
this.tree.setOpaque(true);
this.tree.setBackground(Color.WHITE);
this.tree.setEditable(true);
this.tree.setRootVisible(true); // Always show the root node.
this.tree.setShowsRootHandles(true); // Allow root expansion.
this.tree.setCellRenderer(new JobPlanTreeCellRenderer());
// Add context menu to the job plan tree.
this.tree.addMouseListener(new MouseListener() {
public void mouseClicked(final MouseEvent e) {
this.doMouse(e);
}
public void mouseEntered(final MouseEvent e) {
this.doMouse(e);
}
public void mouseExited(final MouseEvent e) {
this.doMouse(e);
}
public void mousePressed(final MouseEvent e) {
this.doMouse(e);
}
public void mouseReleased(final MouseEvent e) {
this.doMouse(e);
}
private void doMouse(final MouseEvent e) {
if (e.isPopupTrigger()) {
final TreePath treePath = JobPlanPanel.this.tree
.getPathForLocation(e.getX(), e.getY());
if (treePath != null) {
// Work out what was clicked on or
// multiply selected.
final TreePath[] selectedPaths;
if (JobPlanPanel.this.tree.getSelectionCount() == 0)
selectedPaths = new TreePath[] { treePath };
else
selectedPaths = JobPlanPanel.this.tree
.getSelectionPaths();
// Show menu.
final JPopupMenu contextMenu = this
.getContextMenu(Arrays
.asList(selectedPaths));
if (contextMenu != null
&& contextMenu.getComponentCount() > 0) {
contextMenu.show(JobPlanPanel.this.tree, e
.getX(), e.getY());
e.consume();
}
}
}
}
private JPopupMenu getContextMenu(final Collection selectedPaths) {
// Convert paths to identifiers.
final Set identifiers = new HashSet();
final List selectedNodes = new ArrayList();
for (final Iterator i = selectedPaths.iterator(); i
.hasNext();)
selectedNodes.add(((TreePath) i.next())
.getLastPathComponent());
for (int i = 0; i < selectedNodes.size(); i++) {
final Object node = selectedNodes.get(i);
if (node instanceof ActionNode)
identifiers.add(((ActionNode) node).getAction()
.getIdentifier());
else if (node instanceof SectionNode)
identifiers.add(((SectionNode) node).getSection()
.getIdentifier());
}
// Did we produce anything?
if (identifiers.size() < 1)
return null;
// Build menu.
final JPopupMenu contextMenu = new JPopupMenu();
// Queue row.
final JMenuItem queue = new JMenuItem(Resources
.get("queueSelectionTitle"));
queue.setMnemonic(Resources.get("queueSelectionMnemonic")
.charAt(0));
queue.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent evt) {
// Confirm.
new LongProcess() {
public void run() throws Exception {
// Queue the job.
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.queue(clientSocket,
JobPlanPanel.this.jobId,
identifiers);
clientSocket.close();
// Update the list.
parentDialog.refreshJobList.doClick();
}
}.start();
}
});
contextMenu.add(queue);
// Unqueue row.
final JMenuItem unqueue = new JMenuItem(Resources
.get("unqueueSelectionTitle"));
unqueue.setMnemonic(Resources.get(
"unqueueSelectionMnemonic").charAt(0));
unqueue.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent evt) {
// Confirm.
new LongProcess() {
public void run() throws Exception {
// Unqueue the job.
final Socket clientSocket = Client
.createClientSocket(host, port);
Client.unqueue(clientSocket,
JobPlanPanel.this.jobId,
identifiers);
clientSocket.close();
// Update the list.
parentDialog.refreshJobList.doClick();
}
}.start();
}
});
contextMenu.add(unqueue);
return contextMenu;
}
});
// Listener on tree to update footer panel fields.
this.tree.addTreeSelectionListener(new TreeSelectionListener() {
public void valueChanged(final TreeSelectionEvent e) {
// Default values.
Date started = null;
Date ended = null;
JobStatus status = JobStatus.UNKNOWN;
String messages = null;
long elapsed = 0;
// Check a path was actually selected.
final TreePath path = e.getPath();
if (path != null) {
final Object selectedNode = e.getPath()
.getLastPathComponent();
// Get info.
if (selectedNode instanceof SectionNode) {
final JobPlanSection section = ((SectionNode) selectedNode)
.getSection();
status = section.getStatus();
started = section.getStarted();
ended = section.getEnded();
messages = null;
} else if (selectedNode instanceof ActionNode) {
final JobPlanAction action = ((ActionNode) selectedNode)
.getAction();
status = action.getStatus();
started = action.getStarted();
ended = action.getEnded();
messages = action.getMessage();
}
// Elapsed time calculation.
if (started != null)
if (ended != null)
elapsed = ended.getTime() - started.getTime();
else
elapsed = new Date().getTime()
- started.getTime();
}
// Elapsed time to string.
elapsed /= 1000; // Un-millify.
final long seconds = elapsed % 60;
elapsed /= 60;
final long minutes = elapsed % 60;
elapsed /= 60;
final long hours = elapsed % 24;
elapsed /= 24;
final long days = elapsed;
// Update dialog.
try {
if (started != null) {
JobPlanPanel.this.started.setValue(started);
JobPlanPanel.this.started.commitEdit();
} else
JobPlanPanel.this.started.setText(null);
if (ended != null) {
JobPlanPanel.this.finished.setValue(ended);
JobPlanPanel.this.finished.commitEdit();
} else
JobPlanPanel.this.finished.setText(null);
} catch (final ParseException pe) {
// Don't be so silly.
Log.error(pe);
}
JobPlanPanel.this.elapsed.setText(Resources.get(
"timeElapsedPattern", new String[] { "" + days,
"" + hours, "" + minutes, "" + seconds }));
JobPlanPanel.this.status.setText(status.toString());
JobPlanPanel.this.messages.setText(messages);
// Redraw.
JobPlanPanel.this.revalidate();
}
});
// Install an ExpansionListener on the tree which causes the model
// to add actions dynamically from server.
this.tree.addTreeWillExpandListener(this.treeModel);
// Update the layout.
this.add(headerPanel, BorderLayout.PAGE_START);
this.add(new JScrollPane(this.tree), BorderLayout.CENTER);
this.add(footerPanel, BorderLayout.PAGE_END);
// Set the default values.
this.setNoJob();
}
private void setNoJob() {
this.jobId = null;
this.jobIdField.setText(Resources.get("noJobSelected"));
this.threadSpinnerModel.setValue(new Integer(1));
this.threadSpinner.setEnabled(false);
this.jdbcUrl.setText(null);
this.jdbcUser.setText(null);
this.contactEmail.setText(null);
this.contactEmail.setEnabled(false);
this.updateEmailButton.setEnabled(false);
this.startJob.setEnabled(false);
this.stopJob.setEnabled(false);
this.skipDropTable.setSelected(false);
this.skipDropTable.setEnabled(false);
try {
this.treeModel.setJobPlan(null);
} catch (final ProtocolException e) {
StackTrace.showStackTrace(e);
}
}
private void setJobPlan(final JobPlan jobPlan) {
if (jobPlan == null)
this.setNoJob();
else
new LongProcess() {
public void run() throws Exception {
// Get new job ID.
final String jobId = jobPlan.getJobId();
// Update viewable fields.
JobPlanPanel.this.jobIdField.setText(jobId);
JobPlanPanel.this.threadSpinner.setEnabled(true);
JobPlanPanel.this.contactEmail.setEnabled(true);
JobPlanPanel.this.updateEmailButton.setEnabled(true);
JobPlanPanel.this.skipDropTable.setEnabled(true);
// Same job ID as before? Remember expansion set.
final boolean jobIdChanged = !jobId
.equals(JobPlanPanel.this.jobId);
final List openRows = new ArrayList();
if (!jobIdChanged) {
// Remember tree state.
final Enumeration openNodePaths = JobPlanPanel.this.tree
.getExpandedDescendants(JobPlanPanel.this.tree
.getPathForRow(0));
while (openNodePaths != null
&& openNodePaths.hasMoreElements()) {
final TreePath openNodePath = (TreePath) openNodePaths
.nextElement();
openRows.add(new Integer(JobPlanPanel.this.tree
.getRowForPath(openNodePath)));
}
// Sort the row numbers to prevent weirdness with
// opening parents of already opened paths.
Collections.sort(openRows);
} else
// Update our job ID.
JobPlanPanel.this.jobId = jobId;
// Update tree.
JobPlanPanel.this.treeModel.setJobPlan(jobPlan);
if (!jobIdChanged)
// Re-expand tree.
for (final Iterator i = openRows.iterator(); i
.hasNext();)
JobPlanPanel.this.tree.expandRow(((Integer) i
.next()).intValue());
}
}.start();
}
}
private static class JobPlanTreeModel extends DefaultTreeModel implements
TreeWillExpandListener {
private static final long serialVersionUID = 1L;
private static final TreeNode LOADING_TREE = new DefaultMutableTreeNode(
Resources.get("loadingTree"));
private static final TreeNode EMPTY_TREE = new DefaultMutableTreeNode(
Resources.get("emptyTree"));
private final JobPlanPanel planPanel;
private final String host;
private final String port;
private final MartRunnerMonitorDialog parentDialog;
private JobPlanTreeModel(final String host, final String port,
final JobPlanPanel planPanel,
final MartRunnerMonitorDialog parentDialog) {
super(JobPlanTreeModel.EMPTY_TREE, true);
this.planPanel = planPanel;
this.host = host;
this.port = port;
this.parentDialog = parentDialog;
}
/**
* Change the job this tree shows.
*
* @param jobPlan
* the job plan.
* @throws ProtocolException
* if it was unable to do it.
*/
public void setJobPlan(final JobPlan jobPlan) throws ProtocolException {
if (jobPlan == null) {
this.setRoot(JobPlanTreeModel.EMPTY_TREE);
this.reload();
} else {
// Set loading message.
this.setRoot(JobPlanTreeModel.LOADING_TREE);
this.reload();
// Get job details.
final SectionNode rootNode = new SectionNode(null, jobPlan
.getRoot(), this.parentDialog);
rootNode.expanded(this.host, this.port, this.planPanel.jobId);
this.setRoot(rootNode);
this.reload();
// Update GUI bits from the updated plan.
this.planPanel.threadSpinnerModel.setValue(new Integer(jobPlan
.getThreadCount()));
this.planPanel.threadSpinnerModel.setMaximum(new Integer(
jobPlan.getMaxThreadCount()));
this.planPanel.jdbcUrl.setText(jobPlan.getJDBCURL());
this.planPanel.jdbcUser.setText(jobPlan.getJDBCUsername());
this.planPanel.contactEmail.setText(jobPlan
.getContactEmailAddress());
this.planPanel.startJob.setEnabled(!jobPlan.getRoot()
.getStatus().equals(JobStatus.RUNNING));
this.planPanel.stopJob.setEnabled(jobPlan.getRoot().getStatus()
.equals(JobStatus.RUNNING));
this.planPanel.skipDropTable.setSelected(jobPlan
.isSkipDropTable());
}
}
public void treeWillCollapse(final TreeExpansionEvent event)
throws ExpandVetoException {
// Remove children.
final Object collapsedNode = event.getPath().getLastPathComponent();
if (collapsedNode instanceof SectionNode)
((SectionNode) collapsedNode).collapsed();
}
public void treeWillExpand(final TreeExpansionEvent event)
throws ExpandVetoException {
// Insert children.
final Object expandedNode = event.getPath().getLastPathComponent();
if (expandedNode instanceof SectionNode)
((SectionNode) expandedNode).expanded(this.host, this.port,
this.planPanel.jobId);
}
}
private static class SectionNode implements TreeNode {
private final JobPlanSection section;
private final SectionNode parent;
private final MartRunnerMonitorDialog parentDialog;
// Must use Vector to be able to provide enumeration.
private final Vector children = new Vector();
private SectionNode(final SectionNode parent,
final JobPlanSection section,
final MartRunnerMonitorDialog parentDialog) {
this.section = section;
this.parent = parent;
this.parentDialog = parentDialog;
}
private JobPlanSection getSection() {
return this.section;
}
private void collapsed() {
// Forget children.
this.children.clear();
}
private void expanded(final String host, final String port,
final String jobId) {
// Create children.
// Actions first.
try {
final Socket clientSocket = Client.createClientSocket(host,
port);
final Collection actions = Client.getActions(clientSocket,
jobId, this.section);
for (final Iterator i = actions.iterator(); i.hasNext();)
this.children.add(new ActionNode(this, (JobPlanAction) i
.next(), this.parentDialog));
clientSocket.close();
} catch (final Throwable e) {
// Log it.
Log.error(e);
// Add dummy actions instead.
for (int i = 0; i < this.section.getActionCount(); i++)
this.children.add(new DefaultMutableTreeNode(Resources
.get("emptyTree")));
}
// Then subsections.
for (final Iterator i = this.section.getSubSections().iterator(); i
.hasNext();)
this.children.add(new SectionNode(this, (JobPlanSection) i
.next(), this.parentDialog));
}
public Enumeration children() {
return this.children.elements();
}
public boolean getAllowsChildren() {
return this.getChildCount() > 0;
}
public TreeNode getChildAt(final int childIndex) {
return (TreeNode) this.children.get(childIndex);
}
public int getChildCount() {
return this.section.getActionCount()
+ this.section.getSubSections().size();
}
public int getIndex(final TreeNode node) {
return this.children.indexOf(node);
}
public TreeNode getParent() {
return this.parent;
}
public boolean isLeaf() {
return this.getChildCount() == 0;
}
public String toString() {
return this.section.toString();
}
}
private static class ActionNode extends DefaultMutableTreeNode {
private static final long serialVersionUID = 1L;
private final JobPlanAction action;
private final SectionNode parent;
private final MartRunnerMonitorDialog parentDialog;
private ActionNode(final SectionNode parent,
final JobPlanAction action,
final MartRunnerMonitorDialog parentDialog) {
this.action = action;
this.parent = parent;
this.parentDialog = parentDialog;
}
private JobPlanAction getAction() {
return this.action;
}
public Enumeration children() {
return null;
}
public boolean getAllowsChildren() {
return false;
}
public TreeNode getChildAt(final int childIndex) {
return null;
}
public int getChildCount() {
return 0;
}
public int getIndex(final TreeNode node) {
return 0;
}
public TreeNode getParent() {
return this.parent;
}
public boolean isLeaf() {
return true;
}
public void setUserObject(final Object userObject) {
// Set the actions.
final String oldAction = this.action.getAction();
this.action.setAction((String) userObject);
final JobPlanSection section = this.parent.getSection();
// Send the update to the server.
try {
final Socket clientSocket = Client.createClientSocket(
this.parentDialog.host, this.parentDialog.port);
Client.updateAction(clientSocket, section.getJobPlan()
.getJobId(), section, this.action);
clientSocket.close();
} catch (final Throwable pe) {
this.action.setAction(oldAction);
StackTrace.showStackTrace(pe);
}
}
public String toString() {
return this.action.toString();
}
}
}