ADDITIONAL SYSTEM INFORMATION :
Windows 10 x64
A DESCRIPTION OF THE PROBLEM :
The drag and drop operation from Java application to Windows explorer hangs the application.
This worked before JDK 8u301 and JDK 11.0.12. It seems like this issue is caused by https://bugs.openjdk.java.net/browse/JDK-8262446
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
1. Start the attached application
2. Drag a file from the attached application
3. Drop it in a Windows explorer
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
The dragged file should appear in the Windows explorer
ACTUAL -
The dragged file does not show up in the drop area and the client is locked.
---------- BEGIN SOURCE ----------
package com.test.testing;
import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.TransferHandler;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
public class App implements Runnable {
// Set this to true to reproduce the problem
private static final boolean ENABLE_PROBLEM = true;
private DefaultMutableTreeNode root;
private DefaultTreeModel treeModel;
private JTree tree;
@Override
public void run() {
JFrame frame = new JFrame("File Browser");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
File fileRoot = File.listRoots()[0];
root = new DefaultMutableTreeNode(new FileNode(fileRoot));
treeModel = new DefaultTreeModel(root);
tree = new JTree(treeModel);
tree.setShowsRootHandles(true);
tree.setDragEnabled(true);
tree.setTransferHandler(new FileTransferHandler());
JScrollPane scrollPane = new JScrollPane(tree);
frame.add(scrollPane);
frame.setLocationByPlatform(true);
frame.setSize(640, 480);
frame.setVisible(true);
CreateChildNodes ccn = new CreateChildNodes(fileRoot, root);
new Thread(ccn).start();
}
public class CreateChildNodes implements Runnable {
private DefaultMutableTreeNode root;
private File fileRoot;
public CreateChildNodes(File fileRoot, DefaultMutableTreeNode root) {
this.fileRoot = fileRoot;
this.root = root;
}
@Override
public void run() {
createChildren(fileRoot, root);
}
private void createChildren(File fileRoot, DefaultMutableTreeNode node) {
File[] files = fileRoot.listFiles();
if (files == null)
return;
for (File file : files) {
DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(new FileNode(file));
node.add(childNode);
if (file.isDirectory()) {
createChildren(file, childNode);
}
}
}
}
public class FileNode {
private File file;
public FileNode(File file) {
this.file = file;
}
@Override
public String toString() {
String name = file.getName();
if (name.equals("")) {
return file.getAbsolutePath();
} else {
return name;
}
}
}
private class FileTransferHandler extends TransferHandler {
private static final long serialVersionUID = 1L;
@Override
protected Transferable createTransferable(JComponent c) {
JTree list = (JTree) c;
List<File> files = new ArrayList<File>();
for (TreePath path : list.getSelectionPaths()) {
files.add(new File(Arrays.stream(path.getPath()).map(Object::toString)
.collect(Collectors.joining(File.separator))));
}
return new FileTransferable(files);
}
@Override
public int getSourceActions(JComponent c) {
return COPY;
}
}
private class FileTransferable implements Transferable {
// Just a list of files that should be "downloaded"
private List<File> files;
private AtomicReference<List<File>> mDownloadedFiles = new AtomicReference<List<File>>();
private boolean mHasFetchedFiles;
public FileTransferable(List<File> files) {
this.files = files;
}
public DataFlavor[] getTransferDataFlavors() {
return new DataFlavor[] { DataFlavor.javaFileListFlavor };
}
public boolean isDataFlavorSupported(DataFlavor flavor) {
return flavor.equals(DataFlavor.javaFileListFlavor);
}
public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
if (!isDataFlavorSupported(flavor)) {
throw new UnsupportedFlavorException(flavor);
}
List<File> data = mDownloadedFiles.get();
if (data == null) {
data = fetchFileData();
if (data == null) {
System.out.println("Unsupported operation!");
throw new UnsupportedFlavorException(flavor);
}
}
return data;
}
private synchronized List<File> fetchFileData() {
if (!mHasFetchedFiles) {
mHasFetchedFiles = true;
try {
DownloadableWorker downloadable = new DownloadableWorker(files);
// Start the download in background
downloadable.execute();
// Wait for the download to complete
mDownloadedFiles.set(new EventDispatchThreadUtil().futureGet(downloadable));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
return mDownloadedFiles.get();
}
}
private static class DownloadableWorker extends SwingWorker<List<File>, Object> {
private List<File> files;
public DownloadableWorker(List<File> files) {
this.files = files;
// Adding this will alter the events so we will block the application for ever
// when dropping file into a windows folder
if (ENABLE_PROBLEM)
{
MyProgressDialog myProgressDialog = new MyProgressDialog();
myProgressDialog.setVisible(true);
}
}
@Override
protected List<File> doInBackground() throws Exception {
// We have a list of files that needs to be downloaded, simulated here with a
// thread sleep
try {
System.out.println("-- Starting download");
Thread.sleep(2000);
System.out.println("-- Files downloaded");
} catch (InterruptedException e) {
e.printStackTrace();
}
return files;
}
}
private static class MyProgressDialog extends JDialog {
private static final long serialVersionUID = 1L;
public MyProgressDialog() {
JTextField textField = new JTextField("Some progress...");
getContentPane().add(textField);
}
}
private static class EventDispatchThreadUtil
{
private static final Runnable NOOP_RUNNABLE = new Runnable()
{
@Override
public void run()
{
// Do nothing
}
};
private static final Method DISPATCH_EVENT_METHOD;
static
{
Method m = null;
try
{
m = EventQueue.class.getDeclaredMethod("dispatchEvent", AWTEvent.class);
m.setAccessible(true);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
DISPATCH_EVENT_METHOD = m;
}
/**
* Gets one event from the event queue and then dispatches it. If the
* event queue is empty then the call will block until an event is
* available. This method must be called by the event dispatch thread.
*/
public void pumpOneEvent() throws InterruptedException
{
if (!SwingUtilities.isEventDispatchThread())
{
String msg = "Must be called from the event dispatch thread.";
throw new IllegalStateException(msg);
}
EventQueue eventQueue = Toolkit.getDefaultToolkit().getSystemEventQueue();
// blocks when the event queue is empty.
AWTEvent event = eventQueue.getNextEvent();
try
{
DISPATCH_EVENT_METHOD.invoke(eventQueue, event);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
}
/**
* If a call to this method is made from the event dispatch thread then it
* will simulate a blocking call to Future#get() by creating a helper thread
* that blocks on the get call. While the call is being performed the event
* queue is pumped by the thread calling this method. When the call is done
* the helper thread notifies this thread which then returns. If the call is
* made by another thread then the get-method of the given future will be
* called directly.
* @param <T> the generic type of the future
* @param future the future to get the result from
* @return the result of the future
*/
public <T> T futureGet(final Future<T> future)
throws InterruptedException, ExecutionException
{
if (!EventQueue.isDispatchThread())
{
return future.get();
}
final Semaphore semaphore = new Semaphore(1);
semaphore.acquire();
// start a helper thread that will block for us
Thread t = new Thread(new Runnable()
{
@Override
public void run()
{
try
{
future.get();
}
catch (Exception e)
{
e.printStackTrace();
}
finally
{
release(semaphore);
}
}
});
t.start();
acquire(semaphore);
// our helper thread finished successfully
T result = future.get();
return result;
}
/**
* Will block in the call until a permit is acquired from the semaphore.
* May be called from any thread. If called from the event dispatch thread
* then we will as long as we fail to tryAcquire a permit, pump one event on
* the event dispatch thread and the repeat the process until a permit
* has been acquired. However, when we pump the event dispatch queue, if
* there is no events on the queue then we will block on the queue until
* an event arrives. This method should be used together with
* {@link #release(Semaphore)} that additionally to releasing a permit also
* posts a noop event on the event dispatch queue and by that wakes us up
* if we're blocking.
* @param semaphore the semaphore to acquire a permit from
* @throws InterruptedException if the blocking thread is interrupted
*/
public void acquire(Semaphore semaphore) throws InterruptedException
{
if (!EventQueue.isDispatchThread())
{
semaphore.acquire();
return;
}
// pump the event queue until the thread exits controlled
while (!semaphore.tryAcquire())
{
pumpOneEvent();
}
}
/**
* Releases one permit on the given semaphore and pokes the event dispatch
* thread. May be called from any thread. Will also post a noop event to
* the event dispatch queue. Should be used together with
* {@link #acquire(Semaphore)}.
* @param semaphore
*/
public void release(Semaphore semaphore)
{
semaphore.release();
// unblock the event dispatch thread by dispatching
// a noop runnable on the event dispatch thread
EventQueue.invokeLater(NOOP_RUNNABLE);
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new App());
}
}
---------- END SOURCE ----------
FREQUENCY : always