How to wait for a reflection thread to finish - java

I have an application class which allows the user to download a jar file, this jar file is then accessed using reflection.
public void install() {
File app = new File("C:/Temp/" + this.name + ".jar");
if(!app.exists())
download();
URLClassLoader appLoader;
Class<?> appBuilder = null;
try {
appLoader = URLClassLoader.newInstance(new URL[] { new URL("C:/Temp/" + this.name + ".jar") });
appBuilder = appLoader.loadClass("iezon.app.App");
} catch (MalformedURLException | ClassNotFoundException e) {
WebSocket.addError(e);
}
this.application = appBuilder;
}
private void download() {
try {
FileUtils.copyURLToFile(new URL(this.downloadUrl), new File("C:/Temp/" + this.name + ".jar"));
} catch (IOException e) {
WebSocket.addError(e);
}
}
Here, I am creating a new instance of the jar file:
public void start() {
try {
App.createWindow(this.application.getClass());
} catch (InstantiationException | IllegalAccessException e) {
WebSocket.addError(e);
}
}
Window is custom extension of a JFrame that is used as a base template for GUI design.
public static int createWindow(Class<?> window) throws InstantiationException, IllegalAccessException {
factory.add((Window) window.newInstance());
factory.get(factory.size() - 1).windowId = factory.size() - 1;
factory.get(factory.size() - 1).run();
return factory.size() - 1;
}
Since the jar file, once instanced, cannot access this code to load the home screen window on exit, I was wondering how I can wait for the jar file instance to be closed and then relaunch the home screen:
ie in suedo code:
when (create App.createWindow(this.application.getClass()))
dies
create App.createWindow(HomeScreen.class)
Is there a way I can use wait() and notify() methods? Or possibly add a listener when instancing the Class like factory.get(factory.size() - 1).addSomeExitListener.....() ?

You can forget about the reflection part, it's not really relevant since the same would be true if you were given a regularly constructed instance.
You'll need to design your Window class in a way that it works as desired, e.g. let it callback you when it's done. Since it is a JFrame you could also use it's window close listener like:
public static int createWindow(Class<?> window) throws InstantiationException, IllegalAccessException {
Window instance = (Window) window.newInstance();
factory.add(instance);
instance.windowId = factory.size() - 1;
if (window != HomeScreen.class) {
// instance.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE); // ?
instance.addWindowListener(new WindowAdapter() {
#Override
public void windowClosed(WindowEvent e) {
App.createWindow(HomeScreen.class);
}
});
}
instance.run();
return factory.size() - 1;
}

Related

How do I pause my main thread so my JFrame can operate properly

I read a couple questions related to pausing main and both gave answers I didn't understand, and frankly I don't think are applicable.
I have a JFrame that makes use of a database I'm setting up in my driver class.
The JFrame will launch and the window opens; however when I try to make use of the database it fails; because back in main the program just keeps running and shuts down the connection, and closes it.
I tried just removing the connection.close() code just to see if my database methods work in the JFrame, and they do, so I just need to learn how to halt main while my JFrame is running.
public static void main(String[] args) {
File dbPropertiesFile = new File(DbConstants.DB_PROPERTIES_FILENAME);
if (!dbPropertiesFile.exists()) {
showUsage();
System.exit(-1);
}
try {
new Lab9(dbPropertiesFile).run(args);
} catch (Exception e) {
LOG.error(e.getMessage());
e.printStackTrace();
} finally {
shutdown();
}
}
private static void configureLogging() {
ConfigurationSource source;
try {
source = new ConfigurationSource(new FileInputStream(LOG4J_CONFIG_FILENAME));
Configurator.initialize(null, source);
} catch (IOException e) {
System.out.println(
String.format("Can't find the log4j logging configuration file %s.", LOG4J_CONFIG_FILENAME));
}
}
private static void shutdown() {
LOG.info("Shutting down");
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
LOG.error(e.getMessage());
e.printStackTrace();
}
}
}
private static void showUsage() {
System.err.println(
String.format("Program cannot start because %s cannot be found.", DbConstants.DB_PROPERTIES_FILENAME));
}
private Lab9(File file) throws IOException {
properties = new Properties();
properties.load(new FileInputStream(file));
database = new Database(properties);
}
/**
* Where the computer start making a lot of noise.
*
* #param args
* #throws Exception
*/
private void run(String[] args) throws Exception {
LOG.info("Running");
LOG.info("Loading database properties from: " + DbConstants.DB_PROPERTIES_FILENAME + ".");
LOG.info(properties.getProperty("db.driver"));
LOG.info("Driver loaded");
LOG.info("DB URL = " + properties.getProperty("db.url"));
LOG.info("DB USER = " + properties.getProperty("db.user"));
LOG.info("DB PASSWORD = " + properties.getProperty("db.password"));
connect();
Statement statement = connection.createStatement();
try {
// If the user enters the -drop switch
if (args[0].equalsIgnoreCase(DROP_COMMAND)) {
LOG.info("Table " + CustomerDao.TABLE_NAME + "is being DROPPED!");
customerDao.drop();
LOG.info("Table has been DROPPED!");
}
// Check to see if the table is already made; if its not then make it, and fill
// it.
if (Database.tableExists(CustomerDao.TABLE_NAME) == false) {
createTables(statement);
LOG.info("Created the table: " + CustomerDao.TABLE_NAME + ".");
LOG.info("Inserting Customer objects into table: " + CustomerDao.TABLE_NAME + ".");
insertCustomers();
LOG.info("Inserted customer info into table from file: [" + CUSTOMER_DATA + "].");
}
createUI();
// I NEED MAIN
// TO STOP
// AROUND HERE!
}catch(SQLException e){
e.printStackTrace();
LOG.error(e.getMessage());
}finally{
connection.close();
}
}
public static void createUI() {
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
DatabaseControlFrame frame = new DatabaseControlFrame(customerDao);
frame.setVisible(true);
// OR MAYBE I NEED MAIN
// TO STOP
// AROUND HERE!
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
private void connect() throws SQLException {
connection = database.getConnection();
customerDao = new CustomerDao(database);
}
}
Any ideas? I tried using a while(frame.isVisilbe()){ wait(600) }; But the compiler had a spas when I tried to use wait().
You'll note I'm passing a customerDAO object to my JFrame constructor; but I'm beginning to wonder could I make a connection inside the JFrame so that when main's connection closes; my JFrame's doesn't? Is that a good idea? Is that even possible I'm not super SQL savvy I'm going to need to study up on it more.
You could use Thread.sleep() - I've found that useful with JFrame before, though I'm not 100% sure it would fit what you're looking for. If you want it to wait indefinitely, put it in a while loop:
while(//condition)
{
Thread.sleep(500); //pauses for .5 sec, then loops back to check condition
}
JFrame event handler runs on a different thread than main thread, so you need to shutdown on that thread.
Here is a example, Using JDBC with GUI API.
This example call connection.close() on received window-closing-event.
public class MyFrame extends JFrame {
public MyFrame() {
// ...
addWindowListener(new WindowAdapter() {
#Override
public void windowClosing(final WindowEvent e) {
shutdown();
System.exit(0);
}
});
}
// ...
}

Java WatchService watches not terminated

I'm working on an application that uses the Java watchservice (Java 8) under Linux Mint. One interesting problem I am encountering is running out of inotify watches.
I'm developing under Eclipse and the behavior is as follows:
When the app starts, it recurses a directory structure, putting a watch on each directory found. The current test case uses 13,660 paths. My maximum is set to 16384.
If I stop and restart the app several (20+ times), it seems to function normally. Eventually, however, I will get a cascade of system errors indicating the maximum number of watches has been reached. However, if I restart Eclipse, the issue goes away.
Obviously, the Watch Service isn't releasing all of it's resources, but of the 13,660 watches it acquires, only a few (I'm guessing less than a hundred) are retained. It appears they aren't released unless I shut down Eclipse's Java instance and restart it.
To address this, I've ensured the watch service's close method is called when the application shuts down and the watch service task is cancelled.
The only other thing that I'm doing differently is I'm running two separate watch services for two different purposes. I'm told that you shouldn't need to run more than one, and perhaps this is the problem, but I'd rather not run one watch service if I can help it.
That said, are there any thoughts or suggestions on how I might be able to determine the cause of this bug?
Apologies for the massive code posting. This is my implementation of the WatchService class.
A few notes:
The pathFinder runs in a separate thread and is just a file visitor - walking the directory tree and returning paths to all dirs / files found.
Register is called only when changes are posted to the pathsChanged property (from the pathFinder's onSucceeded callback).
The pathsChanged property is always updated by a setAll() call. It only posts the latest changes and is not meant to be cumulative. Beyond the watchservice, other classes listen to these properties and respond accordingly.
public final class LocalWatchService extends BaseTask {
private final static String TAG = "LocalWatchService";
//watch service task
private WatchService watcher;
//path finding task and associated executor
private LocalPathFinder finder;
//root path where the watch service begins
private final Path mRootPath;
private final ExecutorService pathFinderExecutor =
createExecutor ("pathFinder", false);
//class hash map which keys watched paths to generated watch keys
private final Map<WatchKey, Path> keys = new HashMap<WatchKey, Path>();
//reference to model property of watched paths.
private final SimpleListProperty <SyncPath> mChangedPaths =
new SimpleListProperty <SyncPath>
(FXCollections.<SyncPath> observableArrayList());
public LocalWatchService (String rootPath) {
super ();
mRootPath = Paths.get(rootPath);
//create the watch service
try {
this.watcher = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
e.printStackTrace();
}
setOnCancelled(new EventHandler() {
#Override
public void handle(Event arg0) {
pathFinderExecutor.shutdown();
}
});
mChangedPaths.addListener(new ListChangeListener <SyncPath> (){
#Override
public void onChanged(
javafx.collections.ListChangeListener.Change<? extends SyncPath>
arg0) {
for (SyncPath path: arg0.getList()) {
//call register only when a directory is found
if (path.getFile() == null) {
try {
register (path.getPath());
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
});
};
public SimpleListProperty<SyncPath> changedPaths() { return mChangedPaths; }
public void initializeWatchPaths() {
ArrayList <Path> paths = new ArrayList <Path> ();
//create a DirectoryStream filter that finds only directories
//and symlinks
DirectoryStream.Filter<Path> filter =
new DirectoryStream.Filter<Path>() {
public boolean accept(Path file) throws IOException {
return (Files.isDirectory(file) ||
Files.isSymbolicLink(file));
}
};
//apply the filter to a directory stream opened on the root path
//and save everything returned.
paths.addAll(utils.getFiles(mRootPath, filter));
runPathFinder (paths);
}
private void runPathFinder (ArrayList <Path> paths) {
//need to add blocking code / mechanism in case a path finder is
//currently running (rare case)
finder = new LocalPathFinder();
finder.setPaths (paths);
//callbacks on successful completion of pathfinder
EventHandler <WorkerStateEvent> eh =
new EventHandler <WorkerStateEvent> () {
ArrayList <SyncPath> paths = new ArrayList <SyncPath>();
#Override
public void handle(WorkerStateEvent arg0) {
for (Path p: finder.getPaths()) {
paths.add(
new SyncPath(mRootPath, p, SyncType.SYNC_NONE));
}
addPaths(paths);
}
};
finder.setOnSucceeded(eh);
pathFinderExecutor.execute (finder);
}
private void addPath(Path path, SyncType syncType) {
mChangedPaths.setAll(new SyncPath(mRootPath, path, syncType));
}
private void addPaths(ArrayList<SyncPath> paths) {
mChangedPaths.setAll(paths);
}
/**
* Register the given directory with the WatchService
* #throws InterruptedException
*/
public final void register(Path dir)
throws IOException, InterruptedException {
//register the key with the watch service
WatchKey key =
dir.register (watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
if (!keys.isEmpty()) {
Path prev = keys.get(key);
if (prev == null) {
//This is a new key
}
else if (!dir.equals(prev)) {
//This is an update
}
}
keys.put(key, dir);
}
private void processWatchEvent (WatchKey key, Path dir) throws IOException, InterruptedException {
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (kind == OVERFLOW) {
System.out.println ("Overflow encountered");
}
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path target = dir.resolve(ev.context());
if (kind == ENTRY_DELETE) {
ArrayList <Path> finderList = new ArrayList <Path> ();
if (Files.isDirectory(target)) {
//directory deletion is not implemented apart from
//file deletion
}
else
addPath (target, SyncType.SYNC_DELETE);
} else if (kind == ENTRY_CREATE) {
/*
* Added paths are passed to the pathfinder service for
* subdirectory discovery. Path and subpaths are then added
* to the AddedPaths property via an event listener on
* service's onSucceeded() event.
*
* Added files are added directly to the AddedPaths property
*/
ArrayList <Path> finderList = new ArrayList <Path> ();
if (Files.isDirectory(target)) {
finderList.add (target);
runPathFinder (finderList);
}
//add files directly to the addedPaths property
else {
//a newly created file may not be immediately readable
if (Files.isReadable(target)) {
addPath (target, SyncType.SYNC_CREATE);
}
else
System.err.println ("File " + target + " cannot be read");
}
} else if (kind == ENTRY_MODIFY) {
System.out.println ("File modified: " + target.toString());
}
boolean valid = key.reset();
if (!valid)
break;
}
}
#SuppressWarnings("unchecked")
<T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
#Override
protected Void call () throws IOException, InterruptedException {
boolean interrupted = false;
register (mRootPath);
initializeWatchPaths();
try {
// enter watch cycle
while (!interrupted) {
//watch for a key change. Thread blocks until a change occurs
WatchKey key = null;
interrupted = isCancelled();
//thread blocks until a key change occurs
// (whether a new path is processed by finder or a watched item changes otherwise)
try {
key = watcher.take();
} catch (InterruptedException e) {
interrupted = true;
try {
watcher.close();
} catch (IOException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
// fall through and retry
}
Path dir = keys.get (key);
if (dir == null) {
System.out.println ("Null directory key encountered.");
continue;
}
//process key change once it occurs
processWatchEvent(key, dir);
// reset key and remove from set if directory no longer accessible
if (!key.reset()) {
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty())
break;
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
keys.clear();
watcher.close();
return null;
};
}

SWTException: Invalid thread access

I work on a wizard for creation of a java project and get a invalid thread access exception if I run it in the empty workspace for the first time. I try to implement my wizard similar to JavaProjectWizard, but I don't need the second page, so I try to perform finish from the first page and to initialize the second page in advance:
import org.eclipse.jdt.ui.wizards.NewJavaProjectWizardPageTwo;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.ui.INewWizard;
public class SomeNewWizard
extends Wizard
implements INewWizard {
private SomeWizardPageTwo javaWizardPageTwo;
#Override
public void addPages() {
if (javaWizardPageTwo == null)
someWizardPageTwo = new SomeWizardPageTwo(newSeeAppWizardPageOne);
}
#Override
public boolean performFinish() {
/*line 109*/someWizardPageTwo .createProvisonalProject();
final IWorkspaceRunnable op = new IWorkspaceRunnable() {
public void run(IProgressMonitor monitor)
throws CoreException, OperationCanceledException {
try {
someWizardPageTwo.performFinish(new SubProgressMonitor(monitor, 1));
}
catch (InterruptedException e) {
throw new OperationCanceledException(e.getMessage());
}
finally {
monitor.done();
}
}
};
try {
rule = null;
Job job = Job.getJobManager().currentJob();
if (job != null)
rule = job.getRule();
IRunnableWithProgress runnable = new IRunnableWithProgress() {
#Override
public void run(IProgressMonitor monitor)
throws InvocationTargetException, InterruptedException {
try {
JavaCore.run(op, rule, monitor);
}
catch (OperationCanceledException e) {
throw new InterruptedException(e.getMessage());
}
catch (CoreException e) {
throw new InvocationTargetException(e);
}
}
};
getContainer().run(true, true, runnable);
}
catch (InvocationTargetException e) {
handleFinishException(getShell(), e);
return false;
}
catch (InterruptedException e) {
return false;
}
return true;
}
public class SomeWizardPageTwo
extends NewJavaProjectWizardPageTwo
{
private NewJavaProjectWizardPageOne mainPage;
public SomeWizardPageTwo(NewJavaProjectWizardPageOne mainPage)
{
super(mainPage);
this.mainPage = mainPage;
}
#Override
public IProject createProvisonalProject()
{
return super.createProvisonalProject();
}
#Override
protected IWizardContainer getContainer()
{
if (mainPage == null)
return null;
return mainPage.getWizard().getContainer();
}
}
The stacktrace can be found here.
The root of the issue seems that ImageDescriptorRegistry is created from the wrong thread because the corresponding IRunnableWithProgress runs forked. But I wonder why does it work for the normal JavaProjectWizard then? And the main question is: how to make it work for my wizard?
getContainer().run(true, true, runnable); causes the runnable to be executed in a separate thread. The first parameter fork is responsible therefore.
The call to javaWizardPageTwo.performFinish() atempts to access the UI thread and causes the invalid thread access exception.
If you set the fork parameter to false, the code will be executed on the current thread.
Why don't you call javaWizardPageTwo.performFinish() directly?
There is not a bug in Display.checkDevice.
Your call to NewJavaProjectWizardPageTwo.performFinish is running in a background thread, but the code is using ImageDescriptorRegistry which needs to be initialized on the User Interface thread.
This is intermittent because sometimes something else that you do will have already initialized the registry.

FEST: start/stop external SWING program which uses System.exit() without affecting VM

In my Cucumber-jvm scenarios I need to run an external jar program before each scenario, interact with it using the FEST library in the steps, and finally shut the program down to clean the slate for the next scenario. The particular external program I need uses System.exit() to quit when closed. In turn I cannot just quit the program in my tests as that would terminate the entire VM. Instead I use the custom SecurityManager built into FEST to override System.exit() call. However, I cannot get it to work correctly.
The code in example 1 below tries to start the external program in a Cucumber #Before hook and shut it down in an #After hook. It works perfectly fine with only one scenario when I run mvn verify. However, with two or more scenarios maven just hangs on the lines:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running test.acceptance.CucumberRunner
Nothing happens afterwards. I can see that the external program is launched and closed once, but the second time its launched it doesn't close. When I close it manually maven outputs the following:
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-failsafe-
plugin:2.16:integration-test (default) on project acceptance-tests: Execution default of
goal org.apache.maven.plugins:maven-failsafe-plugin:2.16:integration-test failed: The
forked VM terminated without saying properly goodbye. VM crash or System.exit called ?
Does anyone have any idea of what's going on here? It seems like the problem is that the external program is not terminated at all - maybe the fault of the NoExitSecurityManagerInstaller I am using. However, I don't know how else to prevent the call to System.exit terminating the entire VM. Somehow I just want to exit the program I started without affecting the VM in which it is running. Is that not possible?
Update - Solution found!
After having played around with the code for several hours, I accidentally discovered that the Robot class used by the WindowFinder has a cleanUp method that: "Cleans up any used resources (keyboard, mouse, open windows and {#link ScreenLock}) used by this robot.". I tried using this instead of frame.close() and it turns out it works! It doesn't even need the custom SecurityManager.
The problem seems to be that the BasicRobot.robotWithCurrentAwtHierarchy() call aquires a lock on the screen which is NOT release by frame.close(). So when the next call to BasicRobot.robotWithCurrentAwtHierarchy() is made in the second scenario/test the call will block waiting for the lock to be released, and effectively creating a deadlock. The solution is to manually release the lock using robot.cleanUp (which also closes and disposes any open windows). However, why frame.close doesn't do this when it closes the last frame is beyond me.
Example 1
public class CucumberHooks {
private FrameFixture frame;
#Before
public void setup() throws InterruptedException, IOException {
Thread t = new Thread(new Runnable() {
public void run() {
File file = new File(System.getProperty("external-jar"));
URLClassLoader cl = null;
try {
cl = new URLClassLoader( new URL[]{file.toURI().toURL()} );
}
catch (MalformedURLException e) {}
Class<?> clazz = null;
try {
clazz = cl.loadClass("MainClass");
}
catch (ClassNotFoundException e) {}
Method main = null;
try {
main = clazz.getMethod("main", String[].class);
}
catch (NoSuchMethodException e) {}
try {
main.invoke(null, new Object[]{new String[]{}});
}
catch (Exception e) {}
}
});
t.start();
GenericTypeMatcher<JFrame> matcher = new GenericTypeMatcher<JFrame>(JFrame.class) {
protected boolean isMatching(JFrame frame) {
return "External Jar Title".equals(frame.getTitle()) && frame.isShowing();
}
};
frame = WindowFinder.findFrame(matcher).using(BasicRobot.robotWithCurrentAwtHierarchy());
}
#After
public void shutDown() throws InterruptedException {
NoExitSecurityManagerInstaller i = NoExitSecurityManagerInstaller.installNoExitSecurityManager();
frame.close();
i.uninstall();
}
}
Example 2
public class CucumberHooks {
private FrameFixture frame;
private Robot robot;
#Before
public void setup() throws InterruptedException, IOException {
Thread t = new Thread(new Runnable() {
public void run() {
File file = new File(System.getProperty("external-jar"));
URLClassLoader cl = null;
try {
cl = new URLClassLoader( new URL[]{file.toURI().toURL()} );
}
catch (MalformedURLException e) {}
Class<?> clazz = null;
try {
clazz = cl.loadClass("MainClass");
}
catch (ClassNotFoundException e) {}
Method main = null;
try {
main = clazz.getMethod("main", String[].class);
}
catch (NoSuchMethodException e) {}
try {
main.invoke(null, new Object[]{new String[]{}});
}
catch (Exception e) {}
}
});
t.start();
GenericTypeMatcher<JFrame> matcher = new GenericTypeMatcher<JFrame>(JFrame.class) {
protected boolean isMatching(JFrame frame) {
return "External Jar Title".equals(frame.getTitle()) && frame.isShowing();
}
};
robot = BasicRobot.robotWithCurrentAwtHierarchy();
frame = WindowFinder.findFrame(matcher).using(robot);
}
#After
public void shutDown() {
robot.cleanUp();
}
}
It's just a guess: you have to install the NoExitSecurityManagerInstaller before you start your thread. See http://docs.codehaus.org/display/FEST/Handling+System.exit

How can this SwingWorker code be made testable

Consider this code:
public void actionPerformed(ActionEvent e) {
setEnabled(false);
new SwingWorker<File, Void>() {
private String location = url.getText();
#Override
protected File doInBackground() throws Exception {
File file = new File("out.txt");
Writer writer = null;
try {
writer = new FileWriter(file);
creator.write(location, writer);
} finally {
if (writer != null) {
writer.close();
}
}
return file;
}
#Override
protected void done() {
setEnabled(true);
try {
File file = get();
JOptionPane.showMessageDialog(FileInputFrame.this,
"File has been retrieved and saved to:\n"
+ file.getAbsolutePath());
Desktop.getDesktop().open(file);
} catch (InterruptedException ex) {
logger.log(Level.INFO, "Thread interupted, process aborting.", ex);
Thread.currentThread().interrupt();
} catch (ExecutionException ex) {
Throwable cause = ex.getCause() == null ? ex : ex.getCause();
logger.log(Level.SEVERE, "An exception occurred that was "
+ "not supposed to happen.", cause);
JOptionPane.showMessageDialog(FileInputFrame.this, "Error: "
+ cause.getClass().getSimpleName() + " "
+ cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
} catch (IOException ex) {
logger.log(Level.INFO, "Unable to open file for viewing.", ex);
}
}
}.execute();
url is a JTextField and 'creator' is an injected interface for writing the file (so that part is under test). The location where the file is written is hard coded on purpose because this is intended as an example. And java.util.logging is used simply to avoid an external dependency.
How would you chunk this up to make it unit-testable (including abandoning SwingWorker if needed, but then replacing its functionality, at least as used here).
The way I look at it, the doInBackground is basically alright. The fundamental mechanics are creating a writer and closing it, which is almost too simple to test and the real work is under test. However, the done method is quote problematic, including its coupling with the actionPerformed method the parent class and coordinating the enabling and disabling of the button.
However, pulling that apart is not obvious. Injecting some kind of SwingWorkerFactory makes capturing the GUI fields a lot harder to maintain (it is hard to see how it would be a design improvement). The JOpitonPane and the Desktop have all the "goodness" of Singletons, and exception handling makes it impossible to wrap the get easily.
So what would be a good solution to bring this code under test?
IMHO, that's complicated for an anonymous class. My approach would be to refactor the anonymous class to something like this:
public class FileWriterWorker extends SwingWorker<File, Void> {
private final String location;
private final Response target;
private final Object creator;
public FileWriterWorker(Object creator, String location, Response target) {
this.creator = creator;
this.location = location;
this.target = target;
}
#Override
protected File doInBackground() throws Exception {
File file = new File("out.txt");
Writer writer = null;
try {
writer = new FileWriter(file);
creator.write(location, writer);
}
finally {
if (writer != null) {
writer.close();
}
}
return file;
}
#Override
protected void done() {
try {
File file = get();
target.success(file);
}
catch (InterruptedException ex) {
target.failure(new BackgroundException(ex));
}
catch (ExecutionException ex) {
target.failure(new BackgroundException(ex));
}
}
public interface Response {
void success(File f);
void failure(BackgroundException ex);
}
public class BackgroundException extends Exception {
public BackgroundException(Throwable cause) {
super(cause);
}
}
}
That allows the file writing functionality to be tested independent of a GUI
Then, the actionPerformed becomes something like this:
public void actionPerformed(ActionEvent e) {
setEnabled(false);
Object creator;
new FileWriterWorker(creator, url.getText(), new FileWriterWorker.Response() {
#Override
public void failure(FileWriterWorker.BackgroundException ex) {
setEnabled(true);
Throwable bgCause = ex.getCause();
if (bgCause instanceof InterruptedException) {
logger.log(Level.INFO, "Thread interupted, process aborting.", bgCause);
Thread.currentThread().interrupt();
}
else if (cause instanceof ExecutionException) {
Throwable cause = bgCause.getCause() == null ? bgCause : bgCause.getCause();
logger.log(Level.SEVERE, "An exception occurred that was "
+ "not supposed to happen.", cause);
JOptionPane.showMessageDialog(FileInputFrame.this, "Error: "
+ cause.getClass().getSimpleName() + " "
+ cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
#Override
public void success(File f) {
setEnabled(true);
JOptionPane.showMessageDialog(FileInputFrame.this,
"File has been retrieved and saved to:\n"
+ file.getAbsolutePath());
try {
Desktop.getDesktop().open(file);
}
catch (IOException iOException) {
logger.log(Level.INFO, "Unable to open file for viewing.", ex);
}
}
}).execute();
}
Additionally, the instance of FileWriterWorker.Response can be assigned to a variable and tested independent of FileWriterWorker.
The current implementation couples together threading concerns, UI and file writing - and as you've discovered that coupling makes it hard to test the individual components in isolation.
This is quite a long response, but it boils down to pulling out these three concerns from the current implementation into separate classes with a defined interface.
Factor out Application Logic
To start with, focus on the core application logic and move that into a separate class/interface. An interface allows easier mocking, and use of other swing-threading frameworks. The separation means you can test your application logic entirely independently from the other concerns.
interface FileWriter
{
void writeFile(File outputFile, String location, Creator creator)
throws IOException;
// you could also create your own exception type to avoid the checked exception.
// a request object allows all the params to be encapsulated in one object.
// this makes chaining services easier. See later.
void writeFile(FileWriteRequest writeRequest);
}
class FileWriteRequest
{
File outputFile;
String location;
Creator creator;
// constructor, getters etc..
}
class DefualtFileWriter implements FileWriter
{
// this is basically the code from doInBackground()
public File writeFile(File outputFile, String location, Creator creator)
throws IOException
{
Writer writer = null;
try {
writer = new FileWriter(outputFile);
creator.write(location, writer);
} finally {
if (writer != null) {
writer.close();
}
}
return file;
}
public void writeFile(FileWriterRequest request) {
writeFile(request.outputFile, request.location, request.creator);
}
}
Separate out UI
With the application logic now separate, we then factor out the success and error handling. This means that the UI can be tested without actually doing the file writing. In particular, error handling can be tested without actually need to provoke those errors. Here, the errors are quite simple, but often some errors can be very difficult to provoke. By separating out the error handling, there is also chance for reuse, or replacing how the errors are handled. E.g. using a JXErrorPane later.
interface FileWriterHandler {
void done();
void handleFileWritten(File file);
void handleFileWriteError(Throwable t);
}
class FileWriterJOptionPaneOpenDesktopHandler implements FileWriterHandler
{
private JFrame owner;
private JComponent enableMe;
public void done() { enableMe.setEnabled(true); }
public void handleFileWritten(File file) {
try {
JOptionPane.showMessageDialog(owner,
"File has been retrieved and saved to:\n"
+ file.getAbsolutePath());
Desktop.getDesktop().open(file);
}
catch (IOException ex) {
handleDesktopOpenError(ex);
}
}
public void handleDesktopOpenError(IOException ex) {
logger.log(Level.INFO, "Unable to open file for viewing.", ex);
}
public void handleFileWriteError(Throwable t) {
if (t instanceof InterruptedException) {
logger.log(Level.INFO, "Thread interupted, process aborting.", ex);
// no point interrupting the EDT thread
}
else if (t instanceof ExecutionException) {
Throwable cause = ex.getCause() == null ? ex : ex.getCause();
handleGeneralError(cause);
}
else
handleGeneralError(t);
}
public void handleGeneralError(Throwable cause) {
logger.log(Level.SEVERE, "An exception occurred that was "
+ "not supposed to happen.", cause);
JOptionPane.showMessageDialog(owner, "Error: "
+ cause.getClass().getSimpleName() + " "
+ cause.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
Separate out Threading
Finally, we can also separate out the threading concerns with a FileWriterService. Using a FileWriteRequest above makes coding this simpler.
interface FileWriterService
{
// rather than have separate parms for file writing, it is
void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler);
}
class SwingWorkerFileWriterService
implements FileWriterService
{
void handleWriteRequest(FileWriteRequest request, FileWriter writer, FileWriterHandler handler) {
Worker worker = new Worker(request, fileWriter, fileWriterHandler);
worker.execute();
}
static class Worker extends SwingWorker<File,Void> {
// set in constructor
private FileWriter fileWriter;
private FileWriterHandler fileWriterHandler;
private FileWriterRequest fileWriterRequest;
protected File doInBackground() {
return fileWriter.writeFile(fileWriterRequest);
}
protected void done() {
fileWriterHandler.done();
try
{
File f = get();
fileWriterHandler.handleFileWritten(f);
}
catch (Exception ex)
{
// you could also specifically unwrap the ExecutorException here, since that
// is specific to the service implementation using SwingWorker/Executors.
fileWriterHandler.handleFileError(ex);
}
}
}
}
Each part of the system is separately testable - the application logic, the presentation (success and error handling) and the threading implementation is also a separate concern.
This may seem like a lot of interfaces, but the implementation is mostly cut-and-paste from your original code. The interfaces provide the separation that is needed to make these classes testable.
I'm not much of a fan of SwingWorker's so keeping them behind an interface helps keep the clutter they produce out of the code. It also allows you to use a different implementation for implementing the separate UI/background threads. For example, to use Spin, you only need to provide a new implementation of FileWriterService.
Easy solution : a simple timer is best ; you lanch your timer, you launch your actionPerformed, and at the timeout the bouton must be enabled and so on.
Here is an very littel exemple with a java.util.Timer :
package goodies;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JButton;
public class SWTest
{
static class WithButton
{
JButton button = new JButton();
class Worker extends javax.swing.SwingWorker<Void, Void>
{
#Override
protected Void doInBackground() throws Exception
{
synchronized (this)
{
wait(4000);
}
return null;
}
#Override
protected void done()
{
button.setEnabled(true);
}
}
void startWorker()
{
Worker work = new Worker();
work.execute();
}
}
public static void main(String[] args)
{
final WithButton with;
TimerTask verif;
with = new WithButton();
with.button.setEnabled(false);
Timer tim = new Timer();
verif = new java.util.TimerTask()
{
#Override
public void run()
{
if (!with.button.isEnabled())
System.out.println("BAD");
else
System.out.println("GOOD");
System.exit(0);
}};
tim.schedule(verif, 5000);
with.startWorker();
}
}
Supposed Expert solution : a Swing Worker is a RunnableFuture, inside it a FutureTask imbeded in a callable, so you can use your own executor to launch it (the RunableFuture). To do that, you need a SwingWorker with a name class, not an anonymous. With your own executor and a name class, you can test all you want, the supposed expert says.

Categories