/* Copyright (c) 2008-2010, developers of the Ascension Log Visualizer
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom
* the Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
package com.googlecode.logVisualizer.gui;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
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.JTable;
import javax.swing.JTextField;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import net.java.dev.spellcast.utilities.UtilityConstants;
import net.java.swingfx.waitwithstyle.PerformanceInfiniteProgressPanel;
import org.jfree.ui.RefineryUtilities;
import com.googlecode.logVisualizer.Settings;
import com.googlecode.logVisualizer.parser.LogsCreator;
/**
* A dialog to select mafia logs for direct visualisation without having to
* first turn them into parsed ascension logs.
* <p>
* This class only gives controls to select mafia logs for visualisation, it
* does not parse those logs itself. Such a task has to be handled by another
* class. The selected condensed mafia logs are delegated from this class
* through the {@link MafiaLogLoaderListener} instance which is needed for the
* constructor.
* <p>
* Note that the mafia log delegation through the {@link MafiaLogLoaderListener}
* interface must be able to handle concurrent method calls from this class.
*/
final class MafiaLogsVisualizerDialog extends JDialog {
/**
*
*/
private static final long serialVersionUID = 3804936802182556332L;
private static final FilenameFilter MAFIA_LOG_FILTER = new FilenameFilter() {
private final Matcher mafiaLogMatcher = Pattern.compile(
".*_\\d+\\.txt$").matcher("");
private final String preparsedLogPartialFileString = "_ascend";
@Override
public boolean accept(final File dir, final String name) {
return this.mafiaLogMatcher.reset(name).matches()
&& !name.contains(this.preparsedLogPartialFileString);
}
};
private static final List<File> EMPTY_MAFIA_LOGS_LIST = new ArrayList<>(
1);
private final JTextField mafiaLogsDirectoryField;
private final JTable visualizableMafiaLogsTable;
private final JCheckBox toggleAllBox;
private final MafiaLogLoaderListener mafiaLogLoaderListener;
/**
* Constructs the object.
* <p>
* Note that the mafia log delegation through the
* {@link MafiaLogLoaderListener} interface must be able to handle
* concurrent method calls from this class.
*
* @param owner
* The owner of this dialog.
* @param mafiaLogLoaderListener
* The interface through which the selected condensed mafia logs
* are delegated to another class.
* @throws NullPointerException
* if owner is {@code null}; if mafiaLogLoaderListener is
* {@code null}
*/
MafiaLogsVisualizerDialog(final JFrame owner,
final MafiaLogLoaderListener mafiaLogLoaderListener) {
super(owner, true);
if (mafiaLogLoaderListener == null) {
throw new NullPointerException(
"mafiaLogLoaderListener must not be null.");
}
this.setLayout(new BorderLayout(5, 10));
this.setTitle("Mafia Logs Parser");
this.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
this.setGlassPane(new PerformanceInfiniteProgressPanel());
this.mafiaLogLoaderListener = mafiaLogLoaderListener;
this.visualizableMafiaLogsTable = new JTable(new MafiaLogsTableModel(
MafiaLogsVisualizerDialog.EMPTY_MAFIA_LOGS_LIST));
this.toggleAllBox = new JCheckBox("Visualize all logs");
this.toggleAllBox.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(final ChangeEvent e) {
if (MafiaLogsVisualizerDialog.this.toggleAllBox.hasFocus()) {
((MafiaLogsTableModel) MafiaLogsVisualizerDialog.this.visualizableMafiaLogsTable
.getModel())
.setVisualizeAll(MafiaLogsVisualizerDialog.this.toggleAllBox
.isSelected());
}
}
});
this.mafiaLogsDirectoryField = new JTextField(
Settings.getSettingString("Mafia logs location"));
this.mafiaLogsDirectoryField.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
MafiaLogsVisualizerDialog.this.createMafiaLogsTable();
}
});
this.createMafiaLogsTable();
final JButton runButton = new JButton("Run parser");
final JButton cancelButton = new JButton("Cancel");
runButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
if (((MafiaLogsTableModel) MafiaLogsVisualizerDialog.this.visualizableMafiaLogsTable
.getModel()).isVisualizationsOccur()) {
MafiaLogsVisualizerDialog.this
.setWaitingForComputationEnd(true);
MafiaLogsVisualizerDialog.this.runParser();
} else {
JOptionPane
.showMessageDialog(
null,
"There are no mafia logs selected to be visualized.",
"Nothing to visualize",
JOptionPane.INFORMATION_MESSAGE);
}
}
});
cancelButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
MafiaLogsVisualizerDialog.this.dispose();
}
});
this.add(this.createMafiaLogsDirectoryFinderPanel(), BorderLayout.NORTH);
this.add(this.createMafiaLogsTablePanel(), BorderLayout.CENTER);
final JPanel buttonPanel = new JPanel(new GridLayout(1, 0, 10, 0));
buttonPanel.setPreferredSize(new Dimension(150, 50));
buttonPanel.add(runButton);
buttonPanel.add(cancelButton);
this.add(buttonPanel, BorderLayout.SOUTH);
this.pack();
final Dimension currentSize = this.getSize();
if (currentSize.height < 500) {
currentSize.height = 500;
this.setSize(currentSize);
} else if (currentSize.height > 700) {
currentSize.height = 700;
this.setSize(currentSize);
}
RefineryUtilities.centerFrameOnScreen(this);
this.setVisible(true);
}
private JPanel createMafiaLogsDirectoryFinderPanel() {
final JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createTitledBorder("Mafia logs location"));
final JButton directoryChooserButton = new JButton("Find Directory");
GridBagConstraints gbc;
gbc = new GridBagConstraints();
gbc.gridx = 1;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
gbc.insets = new Insets(5, 10, 5, 0);
panel.add(this.mafiaLogsDirectoryField, gbc);
gbc = new GridBagConstraints();
gbc.gridx = 2;
gbc.gridy = 0;
gbc.anchor = GridBagConstraints.EAST;
gbc.insets = new Insets(5, 25, 5, 10);
panel.add(directoryChooserButton, gbc);
File mafiaLogsDirectory = new File(
Settings.getSettingString("Mafia logs location"));
if (!mafiaLogsDirectory.exists()) {
mafiaLogsDirectory = null;
}
final JFileChooser directoryChooser = new JFileChooser(
mafiaLogsDirectory);
directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
directoryChooserButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
final int state = directoryChooser.showOpenDialog(null);
if (state == JFileChooser.APPROVE_OPTION) {
MafiaLogsVisualizerDialog.this.mafiaLogsDirectoryField
.setText(directoryChooser.getSelectedFile()
.getAbsolutePath());
MafiaLogsVisualizerDialog.this.createMafiaLogsTable();
}
}
});
return panel;
}
private JPanel createMafiaLogsTablePanel() {
final JPanel panel = new JPanel(new BorderLayout(5, 10));
final JPanel bottomPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc;
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 1;
gbc.anchor = GridBagConstraints.WEST;
gbc.insets = new Insets(0, 10, 5, 0);
bottomPanel.add(this.toggleAllBox, gbc);
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 2;
gbc.anchor = GridBagConstraints.WEST;
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
gbc.insets = new Insets(10, 5, 5, 5);
bottomPanel
.add(new JLabel(
"<html>Note that depending on the number of mafia logs selected and the amount and contents in those logs"
+ "<p>the parsing process may take a while to finish."
+ "<p><p>Also, note that it is not advisable to have much more than 10 mafia log charts open at the same time.</html>"),
gbc);
final JScrollPane scrollPane = new JScrollPane(
this.visualizableMafiaLogsTable);
scrollPane.setPreferredSize(new Dimension(300, 300));
panel.add(scrollPane, BorderLayout.CENTER);
panel.add(bottomPanel, BorderLayout.SOUTH);
return panel;
}
void createMafiaLogsTable() {
final File mafiaLogsDirectory = new File(
this.mafiaLogsDirectoryField.getText());
if (!mafiaLogsDirectory.exists() || !mafiaLogsDirectory.isDirectory()) {
JOptionPane.showMessageDialog(null,
"Please only specify existing directories.",
"Problem occurred", JOptionPane.WARNING_MESSAGE);
return;
}
final File[] mafiaLogs = mafiaLogsDirectory
.listFiles(MafiaLogsVisualizerDialog.MAFIA_LOG_FILTER);
if (mafiaLogs.length == 0) {
JOptionPane.showMessageDialog(null,
"The specified directory does not contain any mafia logs.",
"Problem occurred", JOptionPane.WARNING_MESSAGE);
return;
}
// If the input seems to be correct, save the directory used.
Settings.setSettingString("Mafia logs location",
this.mafiaLogsDirectoryField.getText());
// In case there are still some logs in the temporary data directory
// delete all of its contents.
for (final File f : UtilityConstants.TEMP_LOCATION.listFiles()) {
if (!f.isDirectory()) {
f.delete();
}
}
// Start the actual computation.
this.setWaitingForComputationEnd(true);
final ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
try {
final File[] condensedMafiaLogs = LogsCreator
.createCondensedMafiaLogs(mafiaLogs);
((MafiaLogsTableModel) MafiaLogsVisualizerDialog.this.visualizableMafiaLogsTable
.getModel()).setMafiaLogs(Arrays
.asList(condensedMafiaLogs));
MafiaLogsVisualizerDialog.this.toggleAllBox
.setSelected(false);
} catch (final IOException e) {
e.printStackTrace();
} finally {
MafiaLogsVisualizerDialog.this
.setWaitingForComputationEnd(false);
}
}
});
executor.shutdown();
}
/**
* @param isComputationNotDone
* A flag showing whether the computation has ended or not.
*/
void setWaitingForComputationEnd(final boolean isComputationNotDone) {
this.getGlassPane().setVisible(isComputationNotDone);
}
void runParser() {
final Thread workerThread = new Thread(new Runnable() {
@Override
public void run() {
// 4 Threads per CPU should be a high enough number to not slow
// the computation too much down by scheduler overhead while
// still making use of threaded computing.
final ExecutorService executor = Executors
.newFixedThreadPool(Runtime.getRuntime()
.availableProcessors() * 4);
for (final File f : ((MafiaLogsTableModel) MafiaLogsVisualizerDialog.this.visualizableMafiaLogsTable
.getModel()).getVisualizableMafiaLogs()) {
executor.execute(new Runnable() {
@Override
public void run() {
MafiaLogsVisualizerDialog.this.mafiaLogLoaderListener
.visualizeMafiaLog(f);
}
});
}
// Wait for all threads to finish.
executor.shutdown();
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
e.printStackTrace();
}
MafiaLogsVisualizerDialog.this.dispose();
}
});
workerThread.start();
}
@Override
public void dispose() {
// Delete any leftover temporary data.
for (final File f : UtilityConstants.TEMP_LOCATION.listFiles()) {
if (!f.isDirectory()) {
f.delete();
}
}
super.dispose();
}
/**
* TableModel used by the JTable which handles the selection of mafia logs
* which should be visualised.
*/
private static final class MafiaLogsTableModel extends AbstractTableModel {
/**
*
*/
private static final long serialVersionUID = 4115093485840189667L;
private static final String[] columnNames = { "Mafia log",
"Should be visualized?" };
private List<File> mafiaLogs;
private List<Boolean> visualizables;
MafiaLogsTableModel(final Collection<File> mafiaLogs) {
this.mafiaLogs = new ArrayList<>(mafiaLogs);
this.visualizables = new ArrayList<>(mafiaLogs.size() + 1);
for (int i = 0, j = mafiaLogs.size(); i < j; i++) {
this.visualizables.add(false);
}
}
@Override
public Class<?> getColumnClass(final int columnIndex) {
if (columnIndex == 1) {
return Boolean.class;
}
return String.class;
}
@Override
public boolean isCellEditable(final int rowIndex, final int columnIndex) {
return columnIndex == 1;
}
@Override
public void setValueAt(final Object aValue, final int rowIndex,
final int columnIndex) {
this.visualizables.set(rowIndex, (Boolean) aValue);
}
@Override
public String getColumnName(final int column) {
return MafiaLogsTableModel.columnNames[column];
}
@Override
public int getColumnCount() {
return MafiaLogsTableModel.columnNames.length;
}
@Override
public int getRowCount() {
return this.mafiaLogs.size();
}
@Override
public Object getValueAt(final int rowIndex, final int columnIndex) {
return columnIndex == 0 ? this.mafiaLogs.get(rowIndex).getName()
.replace(".txt", "") : this.visualizables.get(rowIndex);
}
/**
* Sets the contents of this model to the given collection of mafia
* logs.
*
* @param mafiaLogs
* A collection of condensed mafia logs.
*/
void setMafiaLogs(final Collection<File> mafiaLogs) {
this.mafiaLogs = new ArrayList<>(mafiaLogs);
this.visualizables = new ArrayList<>(mafiaLogs.size());
for (int i = 0, j = mafiaLogs.size(); i < j; i++) {
this.visualizables.add(false);
}
this.fireTableDataChanged();
}
/**
* @return The mafia logs which should be visualised.
*/
List<File> getVisualizableMafiaLogs() {
final List<File> visualizableMafiaLogs = new ArrayList<>(
this.mafiaLogs.size());
for (int i = 0, j = this.visualizables.size(); i < j; i++) {
if (this.visualizables.get(i)) {
visualizableMafiaLogs.add(this.mafiaLogs.get(i));
}
}
return visualizableMafiaLogs;
}
/**
* @param isVisualizeAll
* A flag showing whether all mafia logs inside this model
* should be visualised or not.
*/
void setVisualizeAll(final boolean isVisualizeAll) {
Collections.fill(this.visualizables, isVisualizeAll);
this.fireTableDataChanged();
}
/**
* @return {@code true} in case at least one mafia log from inside this
* model should be visualised.
*/
boolean isVisualizationsOccur() {
return this.visualizables.size() > 0 ? this.visualizables
.contains(true) : false;
}
}
/**
* Interface used to delegate the visualisation of mafia logs to the place
* where it is actually handled (which is not inside the
* {@link MafiaLogsVisualizerDialog} class).
*/
public static interface MafiaLogLoaderListener {
public void visualizeMafiaLog(final File mafiaLog);
}
}