JDK-8274751 : Drag And Drop hangs on Windows
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.awt
  • Affected Version: 8u301,11.0.12,16,17
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_10
  • CPU: x86_64
  • Submitted: 2021-09-28
  • Updated: 2022-04-29
  • Resolved: 2022-01-24
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 11 JDK 15 JDK 17 JDK 18 JDK 19 JDK 8 Other
11.0.16-oracleFixed 15.0.8Fixed 17.0.4-oracleFixed 18.0.1Fixed 19 b07Fixed 8u341Fixed openjdk8u342Fixed
Related Reports
Relates :  
Description
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



Comments
A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk15u-dev/pull/188 Date: 2022-03-30 02:32:26 +0000
30-03-2022

Fix Request (15u) - Justification: The changes fix the regression introduced by JDK-8262446 - Risk Analysis: Low, trivial changes in the native part of the DnD feature - Testing: The fix verified by the test from the description, the jdk_desktop tests are green The patch from jdk-dev (19) applies cleanly to 15u
30-03-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk8u-dev/pull/23 Date: 2022-03-28 05:01:41 +0000
28-03-2022

Fix Request (8u) - Parity with 8u341 - Justification: The changes fix the regression introduced by JDK-8262446 - Risk Analysis: Low, trivial changes in the native part of the DnD feature - Testing: The fix verified by the test from the description, the jdk_desktop tests are green The patch from jdk-dev (19) applies cleanly to 8u (the one conflict is in the copyright date).
28-03-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk11u-dev/pull/942 Date: 2022-03-24 01:25:56 +0000
24-03-2022

Fix Request (11u) - Parity with 11.0.16-oracle - Justification: The changes fix the regression introduced by JDK-8262446 - Risk Analysis: Low, trivial changes in the native part of the DnD feature - Testing: The fix verified by the test from the description, the jdk_desktop tests are green The patch from jdk-dev (19) applies cleanly to 11u (the one conflict is in the copyright date).
24-03-2022

Thanks. Please add the fix-request tag again after that point and I'll approve it right away.
10-02-2022

[~serb], this fix is very new. The problem though is in 17 since first release. I would prefer to wait with this for 17.0.4. Is there any reason to bring this to 17.0.3?
09-02-2022

17.0.4 is fine, I can wait the March 2 to integrate.
09-02-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk17u-dev/pull/142 Date: 2022-02-02 04:42:55 +0000
02-02-2022

Fix Request (17u) - Parity with 17.0.4-oracle - Justification: The changes fix the regression introduced by JDK-8262446 - Risk Analysis: Low, trivial changes in the native part of the DnD feature - Testing: The fix verified by the test from the description, the jdk_desktop tests are green The patch from jdk-dev (19) applies cleanly to 17u.
02-02-2022

Working on backporting this fix to the OpenJDK 17, 11, and 8.
31-01-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk18u/pull/7 Date: 2022-01-24 13:34:52 +0000
24-01-2022

Changeset: 7a0a6c95 Author: Dmitry Markov <dmarkov@openjdk.org> Date: 2022-01-24 10:55:13 +0000 URL: https://git.openjdk.java.net/jdk/commit/7a0a6c95a53c6cb3340328d6543a97807320b740
24-01-2022

Fix Request (18u) - Justification: The changes fix the regression introduced by JDK-8262446 - Risk Analysis: Low, trivial changes in native part of DnD feature - Testing: The fix may be verified using the test attached to the bug (see steps to reproduce above) The patch from jdk-dev (19) applies cleanly to 18u.
24-01-2022

A pull request was submitted for review. URL: https://git.openjdk.java.net/jdk/pull/7125 Date: 2022-01-18 12:51:56 +0000
18-01-2022

When an object is moved from one window (DnD source) to another window (DnD target) the first window may also be considered as a DnD target and the native OS sends corresponding events to it. That event processing clears isInDoDragDropLoop flag, which is currently used to indicate DnD operation, and causes a hang. It is necessary to distinguish events related to the DnD source and events related to the DnD target, (i.e. use two indication flags).
17-01-2022

I can reproduce the issue. It is necessary to use "--add-opens" option in the command line to avoid the exception, (i.e. java --add-opens java.desktop/java.awt=ALL-UNNAMED App).
17-01-2022

According to the test results the hang takes on JDK 11.0.12 and 8u301; on other JDK branches 16/17/18 it fails with ExceptiionInInitializer Error (which might indicate some problems in the test). Based on this I am lowering the priority to 3 and re-targeting the bug to 19.
11-12-2021

Reported with JDK 8u301, The drag and drop operation from Java application to Windows explorer hangs the application. Checked this with reported version and could confirm the issue. To verify, run the attached test case with respective JDK versions. Drag a file on the Droppable Desktop Pane. Result: ======== JDK 11.0.11 - OK JDK 11.0.12 - Fail JDK 8u301 - Fail JDK 8u291 - OK JDK 17 - Fails with Exception in thread "AWT-EventQueue-0" java.lang.ExceptionInInitializerError JDK 18 ea b17: Fails with exception.. JDK 16.0.2 - Fails with Exception in thread "AWT-EventQueue-0" java.lang.ExceptionInInitializerError JDK 16.0.1 - Fails with exception To verify, run the attached test case with respective JDK versions. This seems a regression introduced with JDK-8262446 fix.
05-10-2021