/*==========================================================================*\ | $Id: MainFrameBuilder.java,v 1.2 2011/03/18 11:31:32 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2009 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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 Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.plugintester.ui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.io.File; import java.util.List; import java.util.Map; import java.util.Properties; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JEditorPane; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSeparator; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import javax.swing.Timer; import javax.swing.TransferHandler; import javax.swing.WindowConstants; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.plist.PropertyListConfiguration; import org.webcat.plugintester.AppConstants; import org.webcat.plugintester.PluginRunner; import org.webcat.plugintester.util.PluginConfiguration; import org.webcat.plugintester.util.WebCATConfiguration; //------------------------------------------------------------------------- /** * Manages the main frame window for the application. * * @author Tony Allevato * @version $Id: MainFrameBuilder.java,v 1.2 2011/03/18 11:31:32 aallowat Exp $ */ public class MainFrameBuilder { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new instances of the MainFrameBuilder class. * * @param wcConfig the object that represents the Web-CAT application * configuration * @param settings the Properties object that contains the settings that * are specified in the user interface */ public MainFrameBuilder(WebCATConfiguration wcConfig, Properties settings) { webcatConfig = wcConfig; currentSettings = settings; frame = new JFrame(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setTitle("Web-CAT Plug-in Tester"); constructFrame(frame); initializeFields(); frame.setLocationRelativeTo(null); } //~ Methods ............................................................... // ---------------------------------------------------------- /** * Opens the main frame window. */ public void openFrame() { frame.setVisible(true); } // ---------------------------------------------------------- /** * Creates and lays out the Swing components for the window. * * @param frame the JFrame instance that will contain the components */ private void constructFrame(JFrame frame) { GridBagConstraints gridBagConstraints; fileChooser = new JFileChooser(); bottomPanel = new JPanel(); runButton = new JButton(); mainPanel = new JPanel(); JLabel webCatHomeLabel = new JLabel(); webCatHomeField = new JTextField(); webCatHomeBrowseButton = new JButton(); JSeparator jSeparator0 = new JSeparator(); JLabel submissionLabel = new JLabel(); submissionField = new JTextField(); submissionBrowseButton = new JButton(); JSeparator jSeparator1 = new JSeparator(); JLabel pluginsLabel = new JLabel(); JPanel pluginsPanel = new JPanel(); JScrollPane pluginsScrollPane = new JScrollPane(); pluginsTable = new JTable(); pluginAddButton = new JButton(); pluginRemoveButton = new JButton(); JSeparator jSeparator2 = new JSeparator(); JTabbedPane tabPane = new JTabbedPane(); propertiesPanel = new JPanel(); JScrollPane propertiesScrollPane = new JScrollPane(); propertiesEditor = new JEditorPane(); documentationPanel = new JPanel(); JScrollPane documentationScrollPane = new JScrollPane(); documentationEditor = new JEditorPane(); // File chooser fileChooser.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); // Bottom panel bottomPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 4, 4)); // Run button runButton.setText("Run Plug-ins"); runButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { runButtonActionPerformed(evt); } }); bottomPanel.add(runButton); frame.getContentPane().add(bottomPanel, BorderLayout.PAGE_END); // Main panel mainPanel.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6)); mainPanel.setLayout(new GridBagLayout()); // Web-CAT Home field gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = GridBagConstraints.RELATIVE; gridBagConstraints.anchor = GridBagConstraints.LINE_START; webCatHomeLabel.setText("Web-CAT Home:"); mainPanel.add(webCatHomeLabel, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.weightx = 1.0; webCatHomeField.setTransferHandler(new WebCATHomeTransferHandler()); mainPanel.add(webCatHomeField, gridBagConstraints); // Web-CAT Home browse button webCatHomeBrowseButton.setText("Browse..."); webCatHomeBrowseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { webCatHomeBrowseButtonActionPerformed(evt); } }); mainPanel.add(webCatHomeBrowseButton, new GridBagConstraints()); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; mainPanel.add(jSeparator0, gridBagConstraints); // Submission field gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = GridBagConstraints.RELATIVE; gridBagConstraints.anchor = GridBagConstraints.LINE_START; submissionLabel.setText("Submission:"); mainPanel.add(submissionLabel, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.weightx = 1.0; submissionField.setTransferHandler(new SubmissionTransferHandler()); mainPanel.add(submissionField, gridBagConstraints); // Submission browse button submissionBrowseButton.setText("Browse..."); submissionBrowseButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { submissionBrowseButtonActionPerformed(evt); } }); mainPanel.add(submissionBrowseButton, new GridBagConstraints()); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; mainPanel.add(jSeparator1, gridBagConstraints); // Plug-ins area pluginsLabel.setText("Plug-ins to run:"); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; mainPanel.add(pluginsLabel, gridBagConstraints); pluginsPanel.setPreferredSize(new Dimension(400, 80)); pluginsPanel.setLayout(new GridBagLayout()); pluginsModel = new PluginsTableModel(); pluginsTable.setModel(pluginsModel); pluginsTable.setColumnSelectionAllowed(true); pluginsTable.getTableHeader().setReorderingAllowed(false); pluginsScrollPane.setViewportView(pluginsTable); pluginsTable.getColumnModel().getSelectionModel().setSelectionMode( ListSelectionModel.SINGLE_SELECTION); pluginsTable.getColumnModel().getColumn(0).setResizable(false); pluginsScrollPane.setTransferHandler(new PluginsTransferHandler()); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 0; gridBagConstraints.gridheight = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; pluginsPanel.add(pluginsScrollPane, gridBagConstraints); pluginAddButton.setText("Add..."); pluginAddButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { pluginAddButtonActionPerformed(evt); } }); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.PAGE_START; pluginsPanel.add(pluginAddButton, gridBagConstraints); pluginRemoveButton.setText("Remove"); pluginRemoveButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent evt) { pluginRemoveButtonActionPerformed(evt); } }); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; gridBagConstraints.anchor = GridBagConstraints.PAGE_START; pluginsPanel.add(pluginRemoveButton, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 0.25; mainPanel.add(pluginsPanel, gridBagConstraints); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.HORIZONTAL; mainPanel.add(jSeparator2, gridBagConstraints); tabPane.setPreferredSize(new Dimension(466, 150)); propertiesPanel.setLayout(new GridBagLayout()); propertiesEditor.addKeyListener(new KeyAdapter() { public void keyTyped(KeyEvent e) { propertiesTimer.restart(); } }); propertiesTimer = new Timer(1000, new ActionListener() { public void actionPerformed(ActionEvent e) { updateGradingProperties(); } }); propertiesScrollPane.setViewportView(propertiesEditor); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; propertiesPanel.add(propertiesScrollPane, gridBagConstraints); tabPane.addTab("Properties", propertiesPanel); documentationPanel.setLayout(new GridBagLayout()); documentationEditor.setContentType("text/html"); documentationEditor.setEditable(false); documentationScrollPane.setViewportView(documentationEditor); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagConstraints.weightx = 1.0; gridBagConstraints.weighty = 1.0; documentationPanel.add(documentationScrollPane, gridBagConstraints); tabPane.addTab("Documentation", documentationPanel); gridBagConstraints = new GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridwidth = GridBagConstraints.REMAINDER; gridBagConstraints.gridheight = GridBagConstraints.REMAINDER; gridBagConstraints.fill = GridBagConstraints.BOTH; gridBagConstraints.weighty = 1.0; mainPanel.add(tabPane, gridBagConstraints); frame.getContentPane().add(mainPanel, BorderLayout.CENTER); frame.pack(); } // ---------------------------------------------------------- /** * Initializes the frame components with values obtained from the current * settings. */ private void initializeFields() { String homePath = currentSettings.getProperty( AppConstants.PROP_WEBCAT_HOME); if (homePath != null) { webCatHomeField.setText(homePath); } String subPath = currentSettings.getProperty( AppConstants.PROP_LAST_SUBMISSION_PATH); if (subPath != null) { submissionField.setText(subPath); } pluginsModel.updateModelFromProperties(currentSettings); String props = currentSettings.getProperty( AppConstants.PROP_USER_GRADING_PROPERTIES); if (props != null) { propertiesEditor.setText(props); } updateDocumentationPane(); } // ---------------------------------------------------------- /** * Sets the Web-CAT home location to the specified directory. * * @param file a File object that represents the directory containing the * Web-CAT server */ private void setWebCATHome(File file) { String path; if (file.isDirectory()) { path = file.getAbsolutePath(); } else { path = null; JOptionPane.showMessageDialog(frame, "The selection you have made does not appear to be\n" + "a valid submission. You should select the directory\n" + "that represents the expanded submission contents.\n", "Not a valid submission", JOptionPane.ERROR_MESSAGE); } webCatHomeField.setText(path); if (path == null) { currentSettings.remove(AppConstants.PROP_WEBCAT_HOME); } else { currentSettings.setProperty(AppConstants.PROP_WEBCAT_HOME, path); webcatConfig = new WebCATConfiguration(path); } } // ---------------------------------------------------------- /** * Sets the current submission to the specified directory. * * @param file a File object that represents the directory containing the * submission to use for testing */ private void setSubmission(File file) { String path; if (file.isDirectory()) { path = file.getAbsolutePath(); } else { path = null; JOptionPane.showMessageDialog(frame, "The selection you have made does not appear to be\n" + "a valid submission. You should select the directory\n" + "that represents the expanded submission contents.\n", "Not a valid submission", JOptionPane.ERROR_MESSAGE); } submissionField.setText(path); if (path == null) { currentSettings.remove(AppConstants.PROP_LAST_SUBMISSION_PATH); } else { currentSettings.setProperty(AppConstants.PROP_LAST_SUBMISSION_PATH, path); } } // ---------------------------------------------------------- /** * Adds the plugin represented by the specified file or directory to the * testing chain. * * @param file a File object that represents either the directory * containing the grading plugin or the config.plist file inside the * plugin's directory */ private void addPlugin(File file) { String path; if (file.isDirectory()) { File configFile = new File(file, "config.plist"); if (configFile.exists()) { path = file.getAbsolutePath(); } else { path = null; JOptionPane.showMessageDialog(frame, "The selection you have made does not appear to be\n" + "a valid grading plug-in. You should either select the\n" + "directory that represents the plug-in contents, or\n" + "the config.plist file inside that directory.", "Not a valid plug-in", JOptionPane.ERROR_MESSAGE); } } else { if (file.getName().equalsIgnoreCase("config.plist")) { path = file.getParentFile().getAbsolutePath(); } else { path = null; JOptionPane.showMessageDialog(frame, "The selection you have made does not appear to be\n" + "a valid grading plug-in. You should either select the\n" + "directory that represents the plug-in contents, or\n" + "the config.plist file inside that directory.", "Not a valid plug-in", JOptionPane.ERROR_MESSAGE); } } if (path != null) { try { new PluginConfiguration(new File(path)); pluginsModel.addPlugin(path); pluginsModel.updatePropertiesFromModel(currentSettings); updateDocumentationPane(); } catch (ConfigurationException e) { JOptionPane.showMessageDialog(frame, "An error occurred while parsing the plug-in's " + "config.plist:\n\n" + e.getMessage(), "Not a valid plug-in", JOptionPane.ERROR_MESSAGE); } } } // ---------------------------------------------------------- /** * Update the user's grading properties in the current settings based on * the text entered in the property editor pane. */ private void updateGradingProperties() { String props = propertiesEditor.getText(); if (props == null) { props = ""; } currentSettings.setProperty(AppConstants.PROP_USER_GRADING_PROPERTIES, props); } // ---------------------------------------------------------- /** * Called when the "Browse..." button is pressed to let the user choose the * Web-CAT home location. * * @param evt the event */ private void webCatHomeBrowseButtonActionPerformed(ActionEvent evt) { if (fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { setWebCATHome(fileChooser.getSelectedFile()); } } // ---------------------------------------------------------- /** * Called when the "Browse..." button is pressed to let the user choose the * submission to be used for testing. * * @param evt the event */ private void submissionBrowseButtonActionPerformed(ActionEvent evt) { if (fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { setSubmission(fileChooser.getSelectedFile()); } } // ---------------------------------------------------------- /** * Called when the "Add..." button is pressed to let the user choose a * plugin to be added to the testing chain. * * @param evt the event */ private void pluginAddButtonActionPerformed(ActionEvent evt) { if (fileChooser.showOpenDialog(frame) == JFileChooser.APPROVE_OPTION) { addPlugin(fileChooser.getSelectedFile()); } } // ---------------------------------------------------------- /** * Called when the "Remove" button is pressed to let the user remove the * currently selected plugin from the testing chain. * * @param evt the event */ private void pluginRemoveButtonActionPerformed(ActionEvent evt) { int row = pluginsTable.getSelectedRow(); if (row != -1) { pluginsModel.removePluginAtIndex(row); pluginsModel.updatePropertiesFromModel(currentSettings); updateDocumentationPane(); } } // ---------------------------------------------------------- /** * Called when the "Run Plug-ins" button is pressed to begin the grading * process. * * @param evt the event */ private void runButtonActionPerformed(ActionEvent evt) { PluginRunner runner = new PluginRunner(webcatConfig, currentSettings); runner.run(); } // ---------------------------------------------------------- /** * Updates the documentation pane with property descriptions when the * current plugin chain is modified. */ private void updateDocumentationPane() { StringBuffer buffer = new StringBuffer(); buffer.append("<html><body>"); String[] plugins = pluginsModel.getPlugins(); for (String pluginDir : plugins) { PluginConfiguration pluginConfig = null; try { pluginConfig = new PluginConfiguration(new File(pluginDir)); } catch (ConfigurationException e) { // Do nothing. } Map<String, PropertyListConfiguration> option = pluginConfig.getOptions(); for (String optionName : option.keySet()) { PropertyListConfiguration plist = option.get(optionName); appendPropertyInfoToBuffer(plist, buffer); } } buffer.append("</body></html>"); documentationEditor.setText(buffer.toString()); } // ---------------------------------------------------------- /** * Appends a description of the specified property as HTML to a string * buffer. * * @param plist the property list containing information about the property * @param buffer the buffer to which the description will be appended */ private void appendPropertyInfoToBuffer(PropertyListConfiguration plist, StringBuffer buffer) { buffer.append("<font face=\"Arial\">"); buffer.append("<b>"); buffer.append(plist.getString("property")); buffer.append("</b> <i>(type: "); buffer.append(plist.getString("type")); buffer.append(")</i>"); buffer.append("</font>"); buffer.append("<br>"); buffer.append("<font face=\"Arial\" size=\"-1\">"); buffer.append(plist.getString("description")); buffer.append("</font>"); buffer.append("<hr>"); } //~ Private classes ....................................................... // ---------------------------------------------------------- /** * Handles drop operations of a file onto the current submission text * field. */ private class SubmissionTransferHandler extends TransferHandler { //~ Methods ........................................................... // ---------------------------------------------------------- /** * Returns true if the dropped content is a file; otherwise, false. * * @param comp the component receiving the drop * @param transferFlavors the data flavors of the content being dropped * @return true if the dropped content is a file; otherwise, false. */ public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { for (DataFlavor flavor : transferFlavors) { if (flavor.isFlavorJavaFileListType()) { return true; } } return false; } // ---------------------------------------------------------- /** * Sets the current submission for testing to the dropped file, if * possible. * * @param comp the component receiving the drop * @param t the data being dropped * @return true if the content was successfully dropped; otherwise, * false. */ public boolean importData(JComponent comp, Transferable t) { try { @SuppressWarnings("unchecked") List<File> files = (List<File>) t.getTransferData( DataFlavor.javaFileListFlavor); if (files.size() == 1) { setSubmission(files.get(0)); return true; } } catch (Exception e) { e.printStackTrace(); } return false; } //~ Instance/static variables ......................................... private static final long serialVersionUID = -6870865104793930201L; } // ---------------------------------------------------------- /** * Handles drop operations of a file onto the current submission text * field. */ private class WebCATHomeTransferHandler extends TransferHandler { //~ Methods ........................................................... // ---------------------------------------------------------- /** * Returns true if the dropped content is a file; otherwise, false. * * @param comp the component receiving the drop * @param transferFlavors the data flavors of the content being dropped * @return true if the dropped content is a file; otherwise, false. */ public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { for (DataFlavor flavor : transferFlavors) { if (flavor.isFlavorJavaFileListType()) { return true; } } return false; } // ---------------------------------------------------------- /** * Sets the current submission for testing to the dropped file, if * possible. * * @param comp the component receiving the drop * @param t the data being dropped * @return true if the content was successfully dropped; otherwise, * false. */ public boolean importData(JComponent comp, Transferable t) { try { @SuppressWarnings("unchecked") List<File> files = (List<File>) t.getTransferData( DataFlavor.javaFileListFlavor); if (files.size() == 1) { setWebCATHome(files.get(0)); return true; } } catch (Exception e) { e.printStackTrace(); } return false; } //~ Instance/static variables ......................................... private static final long serialVersionUID = 771815718413890191L; } // ---------------------------------------------------------- private class PluginsTransferHandler extends TransferHandler { //~ Methods ........................................................... // ---------------------------------------------------------- /** * Returns true if the dropped content is a file; otherwise, false. * * @param comp the component receiving the drop * @param transferFlavors the data flavors of the content being dropped * @return true if the dropped content is a file; otherwise, false. */ public boolean canImport(JComponent comp, DataFlavor[] transferFlavors) { for (DataFlavor flavor : transferFlavors) { if (flavor.isFlavorJavaFileListType()) { return true; } } return false; } // ---------------------------------------------------------- /** * Adds the dropped file to the plugin chain, if possible. * * @param comp the component receiving the drop * @param t the data being dropped * @return true if the content was successfully dropped; otherwise, * false. */ public boolean importData(JComponent comp, Transferable t) { try { @SuppressWarnings("unchecked") List<File> files = (List<File>) t.getTransferData( DataFlavor.javaFileListFlavor); if (files.size() == 1) { addPlugin(files.get(0)); return true; } } catch (Exception e) { e.printStackTrace(); } return false; } //~ Instance/static variables ......................................... private static final long serialVersionUID = -99073136818056014L; } //~ Instance/static variables ............................................. private JFrame frame; private JFileChooser fileChooser; private JPanel mainPanel; private JTextField webCatHomeField; private JButton webCatHomeBrowseButton; private JTextField submissionField; private JButton submissionBrowseButton; private JPanel propertiesPanel; private JEditorPane propertiesEditor; private JPanel documentationPanel; private JEditorPane documentationEditor; private JTable pluginsTable; private JButton pluginAddButton; private JButton pluginRemoveButton; private JPanel bottomPanel; private JButton runButton; private PluginsTableModel pluginsModel; private Timer propertiesTimer; private WebCATConfiguration webcatConfig; private Properties currentSettings; }