// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.check;
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedMap;
import java.util.concurrent.ThreadPoolExecutor;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.ProgressMonitor;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import org.infinity.NearInfinity;
import org.infinity.gui.BrowserMenuBar;
import org.infinity.gui.Center;
import org.infinity.gui.ChildFrame;
import org.infinity.gui.SortableTable;
import org.infinity.gui.TableItem;
import org.infinity.gui.ViewFrame;
import org.infinity.gui.WindowBlocker;
import org.infinity.icon.Icons;
import org.infinity.resource.Profile;
import org.infinity.resource.Resource;
import org.infinity.resource.ResourceFactory;
import org.infinity.resource.bcs.BcsResource;
import org.infinity.resource.bcs.Compiler;
import org.infinity.resource.bcs.Decompiler;
import org.infinity.resource.key.ResourceEntry;
import org.infinity.util.Debugging;
import org.infinity.util.Misc;
public final class ScriptChecker implements Runnable, ActionListener, ListSelectionListener, ChangeListener
{
private static final String FMT_PROGRESS = "Checking resource %d/%d";
private ChildFrame resultFrame;
private JButton bopen, bopennew, bsave;
private JTabbedPane tabbedPane;
private SortableTable errorTable, warningTable;
private ProgressMonitor progress;
private int progressIndex;
private List<ResourceEntry> scriptFiles;
public ScriptChecker()
{
new Thread(this).start();
}
// --------------------- Begin Interface ActionListener ---------------------
@Override
public void actionPerformed(ActionEvent event)
{
SortableTable table = errorTable;
if (tabbedPane.getSelectedIndex() == 1)
table = warningTable;
if (event.getSource() == bopen) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
NearInfinity.getInstance().showResourceEntry(resourceEntry);
((BcsResource)NearInfinity.getInstance().getViewable()).highlightText(
((Integer)table.getValueAt(row, 2)).intValue(), null);
}
}
else if (event.getSource() == bopennew) {
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
Resource resource = ResourceFactory.getResource(resourceEntry);
new ViewFrame(resultFrame, resource);
((BcsResource)resource).highlightText(((Integer)table.getValueAt(row, 2)).intValue(), null);
}
}
else if (event.getSource() == bsave) {
JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile());
fc.setDialogTitle("Save search result");
fc.setSelectedFile(new File(fc.getCurrentDirectory(), "result.txt"));
if (fc.showSaveDialog(resultFrame) == JFileChooser.APPROVE_OPTION) {
Path output = fc.getSelectedFile().toPath();
if (Files.exists(output)) {
String options[] = {"Overwrite", "Cancel"};
if (JOptionPane.showOptionDialog(resultFrame, output + " exists. Overwrite?",
"Save result", JOptionPane.YES_NO_OPTION,
JOptionPane.WARNING_MESSAGE, null, options, options[0]) != 0)
return;
}
try (BufferedWriter bw = Files.newBufferedWriter(output)) {
bw.write("Result of script check"); bw.newLine();
if (table == errorTable) {
bw.write("Number of errors: " + table.getRowCount()); bw.newLine();
} else {
bw.write("Number of warnings: " + table.getRowCount()); bw.newLine();
}
for (int i = 0; i < table.getRowCount(); i++) {
bw.write(table.getTableItemAt(i).toString()); bw.newLine();
}
JOptionPane.showMessageDialog(resultFrame, "Result saved to " + output, "Save complete",
JOptionPane.INFORMATION_MESSAGE);
} catch (IOException e) {
JOptionPane.showMessageDialog(resultFrame, "Error while saving " + output,
"Error", JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
}
}
// --------------------- End Interface ActionListener ---------------------
// --------------------- Begin Interface ChangeListener ---------------------
@Override
public void stateChanged(ChangeEvent e)
{
if (tabbedPane.getSelectedIndex() == 0)
bopen.setEnabled(errorTable.getSelectedRowCount() > 0);
else
bopen.setEnabled(warningTable.getSelectedRowCount() > 0);
bopennew.setEnabled(bopen.isEnabled());
}
// --------------------- End Interface ChangeListener ---------------------
// --------------------- Begin Interface ListSelectionListener ---------------------
@Override
public void valueChanged(ListSelectionEvent event)
{
if (tabbedPane.getSelectedIndex() == 0)
bopen.setEnabled(errorTable.getSelectedRowCount() > 0);
else
bopen.setEnabled(warningTable.getSelectedRowCount() > 0);
bopennew.setEnabled(bopen.isEnabled());
}
// --------------------- End Interface ListSelectionListener ---------------------
// --------------------- Begin Interface Runnable ---------------------
@Override
public void run()
{
WindowBlocker blocker = new WindowBlocker(NearInfinity.getInstance());
blocker.setBlocked(true);
try {
ThreadPoolExecutor executor = Misc.createThreadPool();
scriptFiles = ResourceFactory.getResources("BCS");
scriptFiles.addAll(ResourceFactory.getResources("BS"));
progressIndex = 0;
progress = new ProgressMonitor(NearInfinity.getInstance(), "Checking scripts...",
String.format(FMT_PROGRESS, scriptFiles.size(), scriptFiles.size()),
0, scriptFiles.size());
progress.setNote(String.format(FMT_PROGRESS, 0, scriptFiles.size()));
List<Class<? extends Object>> colClasses = new ArrayList<Class<? extends Object>>(3);
colClasses.add(Object.class); colClasses.add(Object.class); colClasses.add(Integer.class);
errorTable = new SortableTable(Arrays.asList(new String[]{"Script", "Error message", "Line"}),
colClasses, Arrays.asList(new Integer[]{120, 440, 50}));
warningTable = new SortableTable(Arrays.asList(new String[]{"Script", "Warning", "Line"}),
colClasses, Arrays.asList(new Integer[]{120, 440, 50}));
boolean isCancelled = false;
Debugging.timerReset();
for (int i = 0; i < scriptFiles.size(); i++) {
Misc.isQueueReady(executor, true, -1);
executor.execute(new Worker(scriptFiles.get(i)));
if (progress.isCanceled()) {
isCancelled = true;
break;
}
}
// enforcing thread termination if process has been cancelled
if (isCancelled) {
executor.shutdownNow();
} else {
executor.shutdown();
}
// waiting for pending threads to terminate
while (!executor.isTerminated()) {
if (!isCancelled && progress.isCanceled()) {
executor.shutdownNow();
isCancelled = true;
}
try { Thread.sleep(1); } catch (InterruptedException e) {}
}
if (isCancelled) {
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Operation cancelled",
"Info", JOptionPane.INFORMATION_MESSAGE);
return;
}
if (errorTable.getRowCount() + warningTable.getRowCount() == 0)
JOptionPane.showMessageDialog(NearInfinity.getInstance(), "No errors or warnings found",
"Info", JOptionPane.INFORMATION_MESSAGE);
else {
errorTable.tableComplete();
warningTable.tableComplete();
resultFrame = new ChildFrame("Result of script check", true);
resultFrame.setIconImage(Icons.getIcon(Icons.ICON_REFRESH_16).getImage());
JScrollPane scrollErrorTable = new JScrollPane(errorTable);
scrollErrorTable.getViewport().setBackground(errorTable.getBackground());
JScrollPane scrollWarningTable = new JScrollPane(warningTable);
scrollWarningTable.getViewport().setBackground(warningTable.getBackground());
tabbedPane = new JTabbedPane();
tabbedPane.addTab("Errors (" + errorTable.getRowCount() + ')', scrollErrorTable);
tabbedPane.addTab("Warnings (" + warningTable.getRowCount() + ')', scrollWarningTable);
tabbedPane.addChangeListener(this);
bopen = new JButton("Open", Icons.getIcon(Icons.ICON_OPEN_16));
bopennew = new JButton("Open in new window", Icons.getIcon(Icons.ICON_OPEN_16));
bsave = new JButton("Save...", Icons.getIcon(Icons.ICON_SAVE_16));
bopen.setMnemonic('o');
bopennew.setMnemonic('n');
bsave.setMnemonic('s');
resultFrame.getRootPane().setDefaultButton(bopennew);
JPanel panel = new JPanel(new FlowLayout(FlowLayout.CENTER));
panel.add(bopen);
panel.add(bopennew);
panel.add(bsave);
JPanel pane = (JPanel)resultFrame.getContentPane();
pane.setLayout(new BorderLayout(0, 3));
pane.add(tabbedPane, BorderLayout.CENTER);
pane.add(panel, BorderLayout.SOUTH);
bopen.setEnabled(false);
bopennew.setEnabled(false);
errorTable.setFont(BrowserMenuBar.getInstance().getScriptFont());
errorTable.getSelectionModel().addListSelectionListener(this);
warningTable.setFont(BrowserMenuBar.getInstance().getScriptFont());
warningTable.getSelectionModel().addListSelectionListener(this);
MouseListener listener = new MouseAdapter()
{
@Override
public void mouseReleased(MouseEvent event)
{
if (event.getClickCount() == 2) {
SortableTable table = (SortableTable)event.getSource();
int row = table.getSelectedRow();
if (row != -1) {
ResourceEntry resourceEntry = (ResourceEntry)table.getValueAt(row, 0);
Resource resource = ResourceFactory.getResource(resourceEntry);
new ViewFrame(resultFrame, resource);
((BcsResource)resource).highlightText(((Integer)table.getValueAt(row, 2)).intValue(), null);
}
}
}
};
errorTable.addMouseListener(listener);
warningTable.addMouseListener(listener);
bopen.addActionListener(this);
bopennew.addActionListener(this);
bsave.addActionListener(this);
pane.setBorder(BorderFactory.createEmptyBorder(3, 3, 3, 3));
resultFrame.pack();
Center.center(resultFrame, NearInfinity.getInstance().getBounds());
resultFrame.setVisible(true);
}
} finally {
advanceProgress(true);
blocker.setBlocked(false);
if (scriptFiles != null) {
scriptFiles.clear();
scriptFiles = null;
}
}
Debugging.timerShow("Check completed", Debugging.TimeFormat.MILLISECONDS);
}
// --------------------- End Interface Runnable ---------------------
private synchronized void advanceProgress(boolean finished)
{
if (progress != null) {
if (finished) {
progressIndex = 0;
progress.close();
progress = null;
} else {
progressIndex++;
if (progressIndex % 100 == 0) {
progress.setNote(String.format(FMT_PROGRESS, progressIndex, scriptFiles.size()));
}
progress.setProgress(progressIndex);
}
}
}
// -------------------------- INNER CLASSES --------------------------
private static final class ScriptErrorsTableLine implements TableItem
{
private final ResourceEntry resourceEntry;
private final Integer lineNr;
private final String error;
private ScriptErrorsTableLine(ResourceEntry resourceEntry, Integer lineNr, String error)
{
this.resourceEntry = resourceEntry;
this.lineNr = lineNr;
this.error = error;
}
@Override
public Object getObjectAt(int columnIndex)
{
if (columnIndex == 0)
return resourceEntry;
else if (columnIndex == 1)
return error;
return lineNr;
}
@Override
public String toString()
{
return String.format("File: %1$s Error: %2$s Line: %3$d",
resourceEntry.toString(), error, lineNr);
}
}
private class Worker implements Runnable
{
private final ResourceEntry entry;
public Worker(ResourceEntry entry)
{
this.entry = entry;
}
@Override
public void run()
{
if (entry != null) {
try {
BcsResource script = new BcsResource(entry);
Decompiler decompiler = new Decompiler(script.getCode(), true);
String decompiled = decompiler.getSource();
Compiler compiler = new Compiler(decompiled);
compiler.compile();
SortedMap<Integer, String> errorMap = compiler.getErrors();
for (final Integer lineNr : errorMap.keySet()) {
String error = errorMap.get(lineNr);
synchronized (errorTable) {
errorTable.addTableItem(new ScriptErrorsTableLine(entry, lineNr, error));
}
}
SortedMap<Integer, String> warningMap = compiler.getWarnings();
for (final Integer lineNr : warningMap.keySet()) {
String warning = warningMap.get(lineNr);
synchronized (warningTable) {
warningTable.addTableItem(new ScriptErrorsTableLine(entry, lineNr, warning));
}
}
} catch (Exception e) {
synchronized (System.err) {
e.printStackTrace();
}
}
}
advanceProgress(false);
}
}
}