diff --git a/.gitignore b/.gitignore index 005aa207e16..8ac9fbab45d 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,8 @@ work/ !/.idea/runConfigurations *.iml .vscode +.cursor +.zed # OS specific files .DS_Store diff --git a/exist-core/src/main/java/org/exist/client/ClientDocumentEditSupport.java b/exist-core/src/main/java/org/exist/client/ClientDocumentEditSupport.java new file mode 100644 index 00000000000..94ef3b98bef --- /dev/null +++ b/exist-core/src/main/java/org/exist/client/ClientDocumentEditSupport.java @@ -0,0 +1,57 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.client; + +import org.exist.xmldb.XmldbURI; +import org.xmldb.api.base.Resource; +import org.xmldb.api.base.XMLDBException; + +/** + * Loads document content for the Java Admin Client editor off the EDT. + * UI construction stays in {@link InteractiveClient#scheduleEditResource(org.exist.xmldb.XmldbURI)} (#4355). + */ +public final class ClientDocumentEditSupport { + + private ClientDocumentEditSupport() { + } + + /** + * Result of loading a resource for editing (XML:DB work only). + */ + public record DocumentEditPayload(XmldbURI name, Resource resource) { + } + + /** + * Retrieves a resource to be shown in {@link DocumentView}. + */ + @FunctionalInterface + public interface DocumentRetriever { + Resource retrieve() throws XMLDBException; + } + + /** + * Performs XML:DB retrieval only; must be called from a background thread. + */ + public static DocumentEditPayload load(final XmldbURI name, final DocumentRetriever retriever) throws XMLDBException { + return new DocumentEditPayload(name, retriever.retrieve()); + } +} diff --git a/exist-core/src/main/java/org/exist/client/ClientFrame.java b/exist-core/src/main/java/org/exist/client/ClientFrame.java index cf73d669bc5..2df946490b6 100644 --- a/exist-core/src/main/java/org/exist/client/ClientFrame.java +++ b/exist-core/src/main/java/org/exist/client/ClientFrame.java @@ -75,6 +75,8 @@ import java.util.*; import java.util.List; import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.prefs.Preferences; @@ -142,9 +144,9 @@ public ClientFrame(final InteractiveClient client, final XmldbURI path, final Pr this.properties = properties; this.client = client; this.processRunnable = new ProcessRunnable(); - this.processThread = client.newClientThread("process", processRunnable); + this.processThread = Thread.ofVirtual().name("java-admin-client.process").unstarted(processRunnable); - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); setupComponents(); addWindowListener(new WindowAdapter() { @@ -536,6 +538,10 @@ public void setPath(final XmldbURI currentPath) { } protected void displayPrompt() { + ClientSwingEdt.invokeAndWaitIfNeeded(this::displayPromptOnEdt); + } + + private void displayPromptOnEdt() { final String pathString = path.getCollectionPath(); try { commandStart = doc.getLength(); @@ -551,6 +557,10 @@ protected void displayPrompt() { } protected void display(final String message) { + ClientSwingEdt.invokeAndWaitIfNeeded(() -> displayOnEdt(message)); + } + + private void displayOnEdt(final String message) { try { commandStart = doc.getLength(); if (commandStart > MAX_DISPLAY_LENGTH) { @@ -567,11 +577,11 @@ protected void display(final String message) { } protected void setResources(final List rows) { - resources.setData(rows); + ClientSwingEdt.invokeAndWaitIfNeeded(() -> resources.setData(rows)); } protected void setStatus(final String message) { - statusbar.setText(message); + ClientSwingEdt.invokeLaterIfNeeded(() -> statusbar.setText(message)); } protected void setEditable(final boolean enabled) { @@ -709,39 +719,52 @@ private void removeAction(final ActionEvent ev) { if (JOptionPane.showConfirmDialog(this, Messages.getString("ClientFrame.104") + Messages.getString("ClientFrame.105"), //$NON-NLS-1$ //$NON-NLS-2$ Messages.getString("ClientFrame.106"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { //$NON-NLS-1$ - final Runnable removeTask = () -> { - final ProgressMonitor monitor = new ProgressMonitor(ClientFrame.this, Messages.getString("ClientFrame.107"), Messages.getString("ClientFrame.108"), 1, res.length); //$NON-NLS-1$ //$NON-NLS-2$ - monitor.setMillisToDecideToPopup(500); - monitor.setMillisToPopup(500); - for (int i = 0; i < res.length; i++) { - final ResourceDescriptor resource = res[i]; - if (resource.isCollection()) { - try { - final EXistCollectionManagementService mgtService = removeRootCollection - .getService(EXistCollectionManagementService.class); - mgtService - .removeCollection(resource.getName()); - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + final ProgressMonitor monitor = new ProgressMonitor(ClientFrame.this, Messages.getString("ClientFrame.107"), Messages.getString("ClientFrame.108"), 1, res.length); //$NON-NLS-1$ //$NON-NLS-2$ + monitor.setMillisToDecideToPopup(500); + monitor.setMillisToPopup(500); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + for (int i = 0; i < res.length; i++) { + if (isCancelled()) { + break; } - } else { - try { - final Resource res1 = removeRootCollection - .getResource(resource.getName().toString()); - removeRootCollection.removeResource(res1); - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + final ResourceDescriptor resource = res[i]; + if (resource.isCollection()) { + try { + final EXistCollectionManagementService mgtService = removeRootCollection + .getService(EXistCollectionManagementService.class); + mgtService.removeCollection(resource.getName()); + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); + } + } else { + try { + final Resource res1 = removeRootCollection + .getResource(resource.getName().toString()); + removeRootCollection.removeResource(res1); + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); + } } + publish(i + 1); } - monitor.setProgress(i + 1); - if (monitor.isCanceled()) { - return; - } + + ClientAction.call(client::getResources, e -> showErrorMessage(e.getMessage(), e)); + return null; } - ClientAction.call(client::getResources, e -> showErrorMessage(e.getMessage(), e)); - }; - client.newClientThread("remove", removeTask).start(); + @Override + protected void process(final List chunks) { + for (final Integer p : chunks) { + monitor.setProgress(p); + if (monitor.isCanceled()) { + cancel(false); + break; + } + } + } + }.execute(); } } @@ -768,24 +791,27 @@ private void moveAction(final ActionEvent ev) { } final XmldbURI destinationPath = ((PrettyXmldbURI) val).getTargetURI(); - final Runnable moveTask = () -> { - try { - final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); - for (ResourceDescriptor re : res) { - setStatus(Messages.getString("ClientFrame.115") + re.getName() + Messages.getString("ClientFrame.116") + destinationPath + Messages.getString("ClientFrame.117")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (re.isCollection()) { - service.move(re.getName(), destinationPath, null); - } else { - service.moveResource(re.getName(), destinationPath, null); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + try { + final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); + for (ResourceDescriptor re : res) { + setStatus(Messages.getString("ClientFrame.115") + re.getName() + Messages.getString("ClientFrame.116") + destinationPath + Messages.getString("ClientFrame.117")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (re.isCollection()) { + service.move(re.getName(), destinationPath, null); + } else { + service.moveResource(re.getName(), destinationPath, null); + } } + client.reloadCollection(); + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); } - client.reloadCollection(); - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + setStatus(Messages.getString("ClientFrame.118")); //$NON-NLS-1$ + return null; } - setStatus(Messages.getString("ClientFrame.118")); //$NON-NLS-1$ - }; - client.newClientThread("move", moveTask).start(); + }.execute(); } private void renameAction(final ActionEvent ev) { @@ -809,30 +835,33 @@ private void renameAction(final ActionEvent ev) { return; } final XmldbURI destinationFilename = parseIt; - final Runnable renameTask = () -> { - try { - final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); - boolean changed = false; - for (final ResourceDescriptor re : res) { - if (!re.getName().equals(destinationFilename)) { - setStatus(Messages.getString("ClientFrame.124") + re.getName() + Messages.getString("ClientFrame.125") + destinationFilename + Messages.getString("ClientFrame.126")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (re.isCollection()) { - service.move(re.getName(), null, destinationFilename); - } else { - service.moveResource(re.getName(), null, destinationFilename); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + try { + final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); + boolean changed = false; + for (final ResourceDescriptor re : res) { + if (!re.getName().equals(destinationFilename)) { + setStatus(Messages.getString("ClientFrame.124") + re.getName() + Messages.getString("ClientFrame.125") + destinationFilename + Messages.getString("ClientFrame.126")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (re.isCollection()) { + service.move(re.getName(), null, destinationFilename); + } else { + service.moveResource(re.getName(), null, destinationFilename); + } + changed = true; } - changed = true; } + if (changed) { + client.reloadCollection(); + } + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); } - if (changed) { - client.reloadCollection(); - } - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + setStatus(Messages.getString("ClientFrame.127")); //$NON-NLS-1$ + return null; } - setStatus(Messages.getString("ClientFrame.127")); //$NON-NLS-1$ - }; - client.newClientThread("rename", renameTask).start(); + }.execute(); } private void copyAction(final ActionEvent ev) { @@ -859,34 +888,37 @@ private void copyAction(final ActionEvent ev) { final XmldbURI destinationPath = ((PrettyXmldbURI) val).getTargetURI(); - final Runnable moveTask = () -> { - try { - final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); - for (ResourceDescriptor re : res) { - - //TODO - //what happens if the source and destination paths are the same? - //we need to check and prompt the user to either skip or choose a new name - //this function can copy multiple resources/collections selected by the user, - //so may need to prompt the user multiple times? is in this thread the correct - //place to do it? also need to do something similar for moveAction() - // - //Its too late and brain hurts - deliriumsky - - setStatus(Messages.getString("ClientFrame.132") + re.getName() + Messages.getString("ClientFrame.133") + destinationPath + Messages.getString("ClientFrame.134")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - if (re.isCollection()) { - service.copy(re.getName(), destinationPath, null); - } else { - service.copyResource(re.getName(), destinationPath, null); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + try { + final EXistCollectionManagementService service = client.current.getService(EXistCollectionManagementService.class); + for (ResourceDescriptor re : res) { + + //TODO + //what happens if the source and destination paths are the same? + //we need to check and prompt the user to either skip or choose a new name + //this function can copy multiple resources/collections selected by the user, + //so may need to prompt the user multiple times? is in this thread the correct + //place to do it? also need to do something similar for moveAction() + // + //Its too late and brain hurts - deliriumsky + + setStatus(Messages.getString("ClientFrame.132") + re.getName() + Messages.getString("ClientFrame.133") + destinationPath + Messages.getString("ClientFrame.134")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + if (re.isCollection()) { + service.copy(re.getName(), destinationPath, null); + } else { + service.copyResource(re.getName(), destinationPath, null); + } } + client.reloadCollection(); + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); } - client.reloadCollection(); - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + setStatus(Messages.getString("ClientFrame.135")); //$NON-NLS-1$ + return null; } - setStatus(Messages.getString("ClientFrame.135")); //$NON-NLS-1$ - }; - client.newClientThread("move", moveTask).start(); + }.execute(); } private ArrayList getCollections(final Collection root, final ArrayList collectionsList) throws XMLDBException { @@ -936,23 +968,30 @@ private void reindexAction(final ActionEvent ev) { if (JOptionPane.showConfirmDialog(this, Messages.getString("ClientFrame.138"), //$NON-NLS-1$ Messages.getString("ClientFrame.139"), JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { //$NON-NLS-1$ - final ResourceDescriptor collections[] = res; - final Runnable reindexThread = () -> { - ClientFrame.this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - final IndexQueryService service; - try { - service = client.current.getService(IndexQueryService.class); - for (final ResourceDescriptor next : collections) { - setStatus(Messages.getString("ClientFrame.142") + next.getName() + Messages.getString("ClientFrame.143")); //$NON-NLS-1$ //$NON-NLS-2$ - service.reindexCollection(next.getName()); + final ResourceDescriptor[] collections = res; + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + final IndexQueryService service; + try { + service = client.current.getService(IndexQueryService.class); + for (final ResourceDescriptor next : collections) { + setStatus(Messages.getString("ClientFrame.142") + next.getName() + Messages.getString("ClientFrame.143")); //$NON-NLS-1$ //$NON-NLS-2$ + service.reindexCollection(next.getName()); + } + setStatus(Messages.getString("ClientFrame.144")); //$NON-NLS-1$ + } catch (final XMLDBException e) { + showErrorMessage(e.getMessage(), e); } - setStatus(Messages.getString("ClientFrame.144")); //$NON-NLS-1$ - } catch (final XMLDBException e) { - showErrorMessage(e.getMessage(), e); + return null; + } + + @Override + protected void done() { + setCursor(Cursor.getDefaultCursor()); } - ClientFrame.this.setCursor(Cursor.getDefaultCursor()); - }; - client.newClientThread("reindex", reindexThread).start(); + }.execute(); } } @@ -973,14 +1012,17 @@ private void uploadAction(final ActionEvent ev) { private void uploadFiles(final List files) { if (files != null && !files.isEmpty()) { - final Runnable uploadTask = () -> { - final UploadDialog upload = new UploadDialog(); - final Consumer failureAction = e -> - showErrorMessage(Messages.getString("ClientFrame.147") + e.getMessage(), e); - ClientAction.call(() -> client.parse(files, upload), failureAction); - ClientAction.call(client::getResources, failureAction); - }; - client.newClientThread("upload", uploadTask).start(); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + final UploadDialog upload = new UploadDialog(); + final Consumer failureAction = e -> + showErrorMessage(Messages.getString("ClientFrame.147") + e.getMessage(), e); + ClientAction.call(() -> client.parse(files, upload), failureAction); + ClientAction.call(client::getResources, failureAction); + return null; + } + }.execute(); } } @@ -1087,50 +1129,57 @@ private void restoreAction(final ActionEvent ev) { private void doRestore(final GuiRestoreServiceTaskListener listener, final String username, final String password, final String dbaPassword, final Path f, final String uri, final boolean overwriteApps) { - final Runnable restoreTask = () -> { + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { - try { - final XmldbURI dbUri; - if(!uri.endsWith(XmldbURI.ROOT_COLLECTION)) { - dbUri = XmldbURI.xmldbUriFor(uri + XmldbURI.ROOT_COLLECTION); - } else { - dbUri = XmldbURI.xmldbUriFor(uri); - } + try { + final XmldbURI dbUri; + if (!uri.endsWith(XmldbURI.ROOT_COLLECTION)) { + dbUri = XmldbURI.xmldbUriFor(uri + XmldbURI.ROOT_COLLECTION); + } else { + dbUri = XmldbURI.xmldbUriFor(uri); + } - final Collection collection = DatabaseManager.getCollection(dbUri.toString(), username, password); - final EXistRestoreService service = collection.getService(EXistRestoreService.class); - service.restore(f.toAbsolutePath().toString(), dbaPassword, listener, overwriteApps); + final Collection collection = DatabaseManager.getCollection(dbUri.toString(), username, password); + final EXistRestoreService service = collection.getService(EXistRestoreService.class); + service.restore(f.toAbsolutePath().toString(), dbaPassword, listener, overwriteApps); - if (JOptionPane.showConfirmDialog(null, Messages.getString("ClientFrame.223"), Messages.getString("ClientFrame.224"), - JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION) { - setStatus(Messages.getString("ClientFrame.225")); - repairRepository(client.getCollection()); - setStatus(Messages.getString("ClientFrame.226")); - } + final AtomicBoolean repairChoice = new AtomicBoolean(false); + ClientSwingEdt.invokeAndWaitIfNeeded(() -> repairChoice.set( + JOptionPane.showConfirmDialog(null, Messages.getString("ClientFrame.223"), Messages.getString("ClientFrame.224"), + JOptionPane.YES_NO_OPTION) == JOptionPane.YES_OPTION)); + if (repairChoice.get()) { + setStatus(Messages.getString("ClientFrame.225")); + try { + repairRepository(client.getCollection()); + } catch (final XMLDBException xe) { + xe.printStackTrace(); + } + setStatus(Messages.getString("ClientFrame.226")); + } - listener.enableDismissDialogButton(); + ClientSwingEdt.invokeLaterIfNeeded(listener::enableDismissDialogButton); - if (properties.getProperty(InteractiveClient.USER, DBA_USER).equals(DBA_USER) && dbaPassword != null) { - properties.setProperty(InteractiveClient.PASSWORD, dbaPassword); - } + if (properties.getProperty(InteractiveClient.USER, DBA_USER).equals(DBA_USER) && dbaPassword != null) { + properties.setProperty(InteractiveClient.PASSWORD, dbaPassword); + } - SwingUtilities.invokeAndWait(() -> { try { client.reloadCollection(); } catch (final XMLDBException xe) { xe.printStackTrace(); } - }); - } catch (final Exception e) { - showErrorMessage(Messages.getString("ClientFrame.181") + e.getMessage(), e); //$NON-NLS-1$ - } finally { - if (listener.hasProblems()) { - showErrorMessage(Messages.getString("ClientFrame.181") + listener.getAllProblems(), null); + } catch (final Exception e) { + showErrorMessage(Messages.getString("ClientFrame.181") + e.getMessage(), e); //$NON-NLS-1$ + } finally { + if (listener.hasProblems()) { + showErrorMessage(Messages.getString("ClientFrame.181") + listener.getAllProblems(), null); + } } + return null; } - }; - - client.newClientThread("restore", restoreTask).start(); + }.execute(); } public static void repairRepository(Collection collection) throws XMLDBException { @@ -1709,6 +1758,10 @@ protected static Properties getLoginData(final Properties props) { } public static void showErrorMessage(final String message, final Throwable t) { + ClientSwingEdt.invokeAndWaitIfNeeded(() -> showErrorMessageOnEdt(message, t)); + } + + private static void showErrorMessageOnEdt(final String message, final Throwable t) { JScrollPane scroll = null; final JTextArea msgArea = new JTextArea(message); msgArea.setBorder(BorderFactory.createTitledBorder(Messages.getString("ClientFrame.214"))); //$NON-NLS-1$ @@ -1736,6 +1789,15 @@ public static void showErrorMessage(final String message, final Throwable t) { } public static int showErrorMessageQuery(final String message, final Throwable t) { + if (SwingUtilities.isEventDispatchThread()) { + return showErrorMessageQueryOnEdt(message, t); + } + final AtomicInteger result = new AtomicInteger(); + ClientSwingEdt.invokeAndWaitIfNeeded(() -> result.set(showErrorMessageQueryOnEdt(message, t))); + return result.get(); + } + + private static int showErrorMessageQueryOnEdt(final String message, final Throwable t) { final JTextArea msgArea = new JTextArea(message); msgArea.setLineWrap(true); msgArea.setWrapStyleWord(true); @@ -1895,24 +1957,7 @@ public boolean accept(final File f) { } } - private class FileListDropTargetListener implements DropTargetListener { - - @Override - public void dragEnter(final DropTargetDragEvent dtde) { - } - - @Override - public void dragOver(final DropTargetDragEvent dtde) { - } - - @Override - public void dropActionChanged(final DropTargetDragEvent dtde) { - } - - @Override - public void dragExit(final DropTargetEvent dte) { - - } + private class FileListDropTargetListener extends DropTargetAdapter { @Override public void drop(final DropTargetDropEvent dtde) { diff --git a/exist-core/src/main/java/org/exist/client/ClientSwingEdt.java b/exist-core/src/main/java/org/exist/client/ClientSwingEdt.java new file mode 100644 index 00000000000..cc49d6eed12 --- /dev/null +++ b/exist-core/src/main/java/org/exist/client/ClientSwingEdt.java @@ -0,0 +1,76 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.client; + +import javax.swing.SwingUtilities; +import java.lang.reflect.InvocationTargetException; + +/** + * Helpers for running work on the Swing event dispatch thread (EDT). + * Used by the Java Admin Client to satisfy Swing single-thread rules + * (#4355). + */ +final class ClientSwingEdt { + + private ClientSwingEdt() { + } + + /** + * Runs {@code task} on the EDT, blocking the caller until it completes. + * If the caller is already on the EDT, {@code task} runs immediately. + */ + static void invokeAndWaitIfNeeded(final Runnable task) { + if (SwingUtilities.isEventDispatchThread()) { + task.run(); + } else { + try { + SwingUtilities.invokeAndWait(task); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final InvocationTargetException e) { + final Throwable cause = e.getCause(); + if (cause instanceof RuntimeException runtimeException) { + throw runtimeException; + } + if (cause instanceof Error error) { + throw error; + } + if (cause != null) { + throw new IllegalStateException("EDT task failed", cause); + } + throw new IllegalStateException("EDT task failed", e); + } + } + } + + /** + * Runs {@code task} asynchronously on the EDT when the caller is not on the EDT. + * If the caller is already on the EDT, {@code task} runs immediately. + */ + static void invokeLaterIfNeeded(final Runnable task) { + if (SwingUtilities.isEventDispatchThread()) { + task.run(); + } else { + SwingUtilities.invokeLater(task); + } + } +} diff --git a/exist-core/src/main/java/org/exist/client/ClientSwingXmlWorker.java b/exist-core/src/main/java/org/exist/client/ClientSwingXmlWorker.java new file mode 100644 index 00000000000..6100dc19e52 --- /dev/null +++ b/exist-core/src/main/java/org/exist/client/ClientSwingXmlWorker.java @@ -0,0 +1,74 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.client; + +import javax.swing.SwingWorker; +import java.util.concurrent.ExecutionException; + +/** + * Shared {@link SwingWorker} pattern for XML:DB work off the EDT with EDT completion (#4355). + * + * @param result type produced in {@link #loadInBackground()} + */ +public abstract class ClientSwingXmlWorker extends SwingWorker { + + /** + * Runs off the EDT; perform XML:DB / blocking I/O here. + */ + protected abstract T loadInBackground() throws Exception; + + @Override + protected final T doInBackground() throws Exception { + return loadInBackground(); + } + + /** + * Runs on the EDT after {@link #loadInBackground()} completed successfully. + * Default implementation is intentionally empty; subclasses override to update the UI. + */ + protected void onSuccess(final T result) { + // no-op — override in subclasses + } + + /** + * Runs on the EDT when {@link #loadInBackground()} failed. + */ + protected void onFailure(final Throwable t) { + if (t instanceof Exception ex) { + ClientFrame.showErrorMessage(ex.getMessage(), ex); + } else { + ClientFrame.showErrorMessage(t.toString(), new Exception(t)); + } + } + + @Override + protected final void done() { + try { + onSuccess(get()); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final ExecutionException e) { + final Throwable cause = e.getCause() != null ? e.getCause() : e; + onFailure(cause); + } + } +} diff --git a/exist-core/src/main/java/org/exist/client/ConnectionDialog.java b/exist-core/src/main/java/org/exist/client/ConnectionDialog.java index fc1c02769b8..7d15e3e3bb3 100644 --- a/exist-core/src/main/java/org/exist/client/ConnectionDialog.java +++ b/exist-core/src/main/java/org/exist/client/ConnectionDialog.java @@ -71,7 +71,7 @@ public ConnectionDialog(final java.awt.Frame parent, final boolean modal, final this.defaultConnectionSettings = defaultConnectionSettings; this.config = Path.of(defaultConnectionSettings.getConfiguration()); this.disableEmbeddedConnectionType = disableEmbeddedConnectionType; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); initComponents(); if (disableEmbeddedConnectionType) { @@ -239,7 +239,7 @@ protected final void paintTabBorder(final java.awt.Graphics g, final int tabPlac setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); setTitle("Database Connection"); - lblExistLogo.setIcon(InteractiveClient.getExistIcon(getClass())); + InteractiveClient.setExistImageIcon(getClass(), lblExistLogo::setIcon); lblUsername.setText(getLabelText("LoginPanel.2")); diff --git a/exist-core/src/main/java/org/exist/client/DocumentView.java b/exist-core/src/main/java/org/exist/client/DocumentView.java index 1867bac300d..5d4e5b8eaa1 100644 --- a/exist-core/src/main/java/org/exist/client/DocumentView.java +++ b/exist-core/src/main/java/org/exist/client/DocumentView.java @@ -56,6 +56,7 @@ import javax.swing.JTextField; import javax.swing.JToolBar; import javax.swing.KeyStroke; +import javax.swing.SwingWorker; import javax.swing.border.BevelBorder; import javax.xml.transform.OutputKeys; @@ -100,7 +101,7 @@ public DocumentView(InteractiveClient client, XmldbURI resourceName, Resource re this.resourceName = resourceName; this.resource = resource; this.client = client; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); this.collection = client.getCollection(); this.properties = properties; getContentPane().setLayout(new BorderLayout()); @@ -341,65 +342,94 @@ public void actionPerformed(ActionEvent e) { } private void save() { - final Runnable saveTask = () -> { - try { - statusMessage.setText(Messages.getString("DocumentView.36") + URIUtils.urlDecodeUtf8(resource.getId())); //$NON-NLS-1$ + final String content = text.getText(); + statusMessage.setText(Messages.getString("DocumentView.36") + URIUtils.urlDecodeUtf8(resourceName.lastSegment())); //$NON-NLS-1$ + progress.setIndeterminate(true); + progress.setVisible(true); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { if (collection instanceof Observable observable) { - observable - .addObserver(new ProgressObserver()); + observable.addObserver(new ProgressObserver()); } - progress.setIndeterminate(true); - progress.setVisible(true); - resource.setContent(text.getText()); - collection.storeResource(resource); - if (collection instanceof Observable observable) { - observable.deleteObservers(); + try { + resource.setContent(content); + collection.storeResource(resource); + } finally { + if (collection instanceof Observable observable) { + observable.deleteObservers(); + } } - } catch (final XMLDBException e) { - ClientFrame.showErrorMessage(Messages.getString("DocumentView.37") //$NON-NLS-1$ - + e.getMessage(), e); - } finally { + return null; + } + + @Override + protected void done() { progress.setVisible(false); + try { + get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final java.util.concurrent.ExecutionException e) { + final Throwable t = e.getCause() != null ? e.getCause() : e; + if (t instanceof XMLDBException xe) { + ClientFrame.showErrorMessage(Messages.getString("DocumentView.37") + xe.getMessage(), xe); //$NON-NLS-1$ + } else { + ClientFrame.showErrorMessage(t.getMessage(), t); + } + } } - }; - client.newClientThread("save", saveTask).start(); + }.execute(); } private void saveAs() { - final Runnable saveAsTask = () -> { - - //Get the name to save the resource as - final String nameres = JOptionPane.showInputDialog(null, Messages.getString("DocumentView.38")); //$NON-NLS-1$ - if (nameres != null) { + final String nameres = JOptionPane.showInputDialog(this, Messages.getString("DocumentView.38")); //$NON-NLS-1$ + if (nameres == null) { + return; + } + final String content = text.getText(); + statusMessage.setText(Messages.getString("DocumentView.39") + nameres); //$NON-NLS-1$ + progress.setIndeterminate(true); + progress.setVisible(true); + new SwingWorker() { + @Override + protected Void doInBackground() throws Exception { + if (collection instanceof Observable observable) { + observable.addObserver(new ProgressObserver()); + } try { - //Change status message and display a progress dialog - statusMessage.setText(Messages.getString("DocumentView.39") + nameres); //$NON-NLS-1$ - if (collection instanceof Observable observable) { - observable.addObserver(new ProgressObserver()); - } - progress.setIndeterminate(true); - progress.setVisible(true); - - //Create a new resource as named, set the content, store the resource - XMLResource result = null; - result = collection.createResource(URIUtils.encodeXmldbUriFor(nameres).toString(), XMLResource.class); - result.setContent(text.getText()); + final XMLResource result = collection.createResource( + URIUtils.encodeXmldbUriFor(nameres).toString(), XMLResource.class); + result.setContent(content); collection.storeResource(result); - client.reloadCollection(); //reload the client collection + client.reloadCollection(); + } finally { if (collection instanceof Observable observable) { observable.deleteObservers(); } - } catch (final XMLDBException e) { - ClientFrame.showErrorMessage(Messages.getString("DocumentView.40") + e.getMessage(), e); //$NON-NLS-1$ - } catch (final URISyntaxException e) { - ClientFrame.showErrorMessage(Messages.getString("DocumentView.41") + e.getMessage(), e); //$NON-NLS-1$ - } finally { - //hide the progress dialog - progress.setVisible(false); } + return null; } - }; - client.newClientThread("save-as", saveAsTask).start(); + + @Override + protected void done() { + progress.setVisible(false); + try { + get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final java.util.concurrent.ExecutionException e) { + final Throwable t = e.getCause() != null ? e.getCause() : e; + if (t instanceof XMLDBException xe) { + ClientFrame.showErrorMessage(Messages.getString("DocumentView.40") + xe.getMessage(), xe); //$NON-NLS-1$ + } else if (t instanceof URISyntaxException urie) { + ClientFrame.showErrorMessage(Messages.getString("DocumentView.41") + urie.getMessage(), urie); //$NON-NLS-1$ + } else { + ClientFrame.showErrorMessage(t.getMessage(), t); + } + } + } + }.execute(); } private void export() throws XMLDBException { @@ -449,7 +479,12 @@ public void setText(String content) throws XMLDBException { } class ProgressObserver implements Observer { - public void update(Observable o, Object arg) { + @Override + public void update(final Observable o, final Object arg) { + ClientSwingEdt.invokeLaterIfNeeded(() -> updateProgressOnEdt(o, arg)); + } + + private void updateProgressOnEdt(final Observable o, final Object arg) { progress.setIndeterminate(false); final ProgressIndicator ind = (ProgressIndicator) arg; progress.setValue(ind.getPercentage()); diff --git a/exist-core/src/main/java/org/exist/client/IndexDialog.java b/exist-core/src/main/java/org/exist/client/IndexDialog.java index e7fd9bbf560..ef3d3eaee38 100644 --- a/exist-core/src/main/java/org/exist/client/IndexDialog.java +++ b/exist-core/src/main/java/org/exist/client/IndexDialog.java @@ -78,7 +78,7 @@ public IndexDialog(String title, InteractiveClient client) { super(title); this.client = client; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); //capture the frame's close event final WindowListener windowListener = new WindowAdapter() { diff --git a/exist-core/src/main/java/org/exist/client/InteractiveClient.java b/exist-core/src/main/java/org/exist/client/InteractiveClient.java index eae34bcf4da..fc4a1f9128a 100644 --- a/exist-core/src/main/java/org/exist/client/InteractiveClient.java +++ b/exist-core/src/main/java/org/exist/client/InteractiveClient.java @@ -22,6 +22,7 @@ package org.exist.client; import java.awt.Dimension; +import java.awt.Image; import java.io.*; import java.lang.reflect.Field; import java.net.URISyntaxException; @@ -35,6 +36,7 @@ import java.util.*; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BinaryOperator; +import java.util.function.Consumer; import java.util.function.UnaryOperator; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -255,7 +257,7 @@ public static void main(final String[] args) { } catch (final StartException e) { if (e.getMessage() != null && !e.getMessage().isEmpty()) { - consoleErr(e.getMessage()); + consoleErr(e.getMessage(), e); } System.exit(e.getErrorCode()); @@ -264,20 +266,20 @@ public static void main(final String[] args) { System.exit(SystemExitCodes.INVALID_ARGUMENT_EXIT_CODE); } catch (final Exception e) { - e.printStackTrace(); + consoleErr(e.getMessage() != null ? e.getMessage() : e.toString(), e); System.exit(SystemExitCodes.CATCH_ALL_GENERAL_ERROR_EXIT_CODE); // return non-zero exit status on exception } } /** - * Create a new thread for this client instance. - * - * @param threadName the name of the thread - * @param runnable the function to execute on the thread - * @return the thread + * Run {@code task} on the Swing EDT when the admin GUI is active. + * Used from background workers (e.g. shell {@link ClientFrame.ProcessRunnable}) so + * {@link ClientFrame} updates stay EDT-owned (#4355). */ - Thread newClientThread(final String threadName, final Runnable runnable) { - return new Thread(runnable, "java-admin-client." + threadName); + private void runOnFrameEdt(final Runnable task) { + if (options.startGUI && frame != null) { + ClientSwingEdt.invokeAndWaitIfNeeded(task); + } } /** @@ -290,7 +292,7 @@ protected void connect() throws Exception { final String uri = properties.getProperty(InteractiveClient.URI); if (options.startGUI && frame != null) { - frame.setStatus("connecting to " + uri); + runOnFrameEdt(() -> frame.setStatus("connecting to " + uri)); } // Create database @@ -313,7 +315,7 @@ protected void connect() throws Exception { final String collectionUri = uri + path; current = DatabaseManager.getCollection(collectionUri, properties.getProperty(USER), properties.getProperty(PASSWORD)); if (options.startGUI && frame != null) { - frame.setStatus("connected to " + uri + " as user " + properties.getProperty(USER)); + runOnFrameEdt(() -> frame.setStatus("connected to " + uri + " as user " + properties.getProperty(USER))); } if (database.getProperty(CONFIGURATION) != null) { @@ -475,7 +477,7 @@ protected void more(final String str) { consoleOut(line); } } catch (final IOException ioe) { - consoleErr("IOException: " + ioe); + consoleErr("IOException: " + ioe, ioe); } } @@ -486,9 +488,7 @@ protected void more(final String str) { * @return true if command != quit */ protected boolean process(final String line) { - if (options.startGUI) { - frame.setPath(path); - } + runOnFrameEdt(() -> frame.setPath(path)); final String args[]; if (line.startsWith("find")) { args = new String[2]; @@ -570,9 +570,8 @@ protected boolean process(final String line) { current.close(); current = temp; newPath = collectionPath.toCollectionPathURI(); - if (options.startGUI) { - frame.setPath(collectionPath.toCollectionPathURI()); - } + final XmldbURI pathForFrame = collectionPath.toCollectionPathURI(); + runOnFrameEdt(() -> frame.setPath(pathForFrame)); } else { messageln("no such collection."); } @@ -603,7 +602,7 @@ protected boolean process(final String line) { errorln("could not parse resource name into a valid URI: " + e.getMessage()); return false; } - editResource(resource); + scheduleEditResource(resource); } else { messageln("Please specify a resource."); } @@ -619,9 +618,11 @@ protected boolean process(final String line) { errorln("could not parse resource name into a valid URI: " + e.getMessage()); return false; } - final Resource res = retrieve(resource); - // display document - if (res != null) { + final Resource retrieved = retrieve(resource); + if (retrieved == null) { + return true; + } + try (final EXistResource res = (EXistResource) retrieved) { final String data; if (XML_RESOURCE.equals(res.getResourceType())) { data = (String) res.getContent(); @@ -629,12 +630,14 @@ protected boolean process(final String line) { data = new String((byte[]) res.getContent()); } if (options.startGUI) { - frame.setEditable(false); - frame.display(data); - frame.setEditable(true); + final String contentForShell = data; + runOnFrameEdt(() -> { + frame.setEditable(false); + frame.display(contentForShell); + frame.setEditable(true); + }); } else { - final String content = data; - more(content); + more(data); } } return true; @@ -713,7 +716,8 @@ protected boolean process(final String line) { for (int i = start; i < start + count; i++) { final Resource r = result.getResource(i); if (options.startGUI) { - frame.display((String) r.getContent()); + final String lineContent = (String) r.getContent(); + runOnFrameEdt(() -> frame.display(lineContent)); } else { more((String) r.getContent()); } @@ -870,8 +874,7 @@ protected boolean process(final String line) { mgtService.addAccount(user); messageln("User '" + user.getName() + "' created."); } catch (final Exception e) { - errorln("ERROR: " + e.getMessage()); - e.printStackTrace(); + errorln("ERROR: " + e.getMessage(), e); } } else if ("users".equalsIgnoreCase(args[0])) { final UserManagementService mgtService = current.getService(UserManagementService.class); @@ -920,8 +923,7 @@ protected boolean process(final String line) { mgtService.updateAccount(user); properties.setProperty(PASSWORD, p1); } catch (final Exception e) { - errorln("ERROR: " + e.getMessage()); - e.printStackTrace(); + errorln("ERROR: " + e.getMessage(), e); } } else if ("chmod".equalsIgnoreCase(args[0])) { if (args.length < 2) { @@ -1104,26 +1106,43 @@ protected boolean process(final String line) { if (options.startGUI) { ClientFrame.showErrorMessage(getExceptionMessage(e), e); } else { - errorln(getExceptionMessage(e)); - e.printStackTrace(); + errorln(getExceptionMessage(e), e); } return true; } } /** - * @param name + * Loads the resource off the EDT, then opens {@link DocumentView} on the EDT (#4355). */ - private void editResource(final XmldbURI name) { - try { - final Resource doc = retrieve(name, properties.getProperty(OutputKeys.INDENT, "yes")); //$NON-NLS-1$ - final DocumentView view = new DocumentView(this, name, doc, properties); - view.setSize(new Dimension(640, 400)); - view.viewDocument(); - } catch (final XMLDBException ex) { - errorln("XMLDB error: " + ex.getMessage()); - ex.printStackTrace(); - } + private void scheduleEditResource(final XmldbURI name) { + final String indent = properties.getProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ + new ClientSwingXmlWorker() { + @Override + protected ClientDocumentEditSupport.DocumentEditPayload loadInBackground() throws Exception { + return ClientDocumentEditSupport.load(name, () -> retrieve(name, indent)); + } + + @Override + protected void onSuccess(final ClientDocumentEditSupport.DocumentEditPayload loaded) { + try { + final DocumentView view = new DocumentView(InteractiveClient.this, loaded.name(), loaded.resource(), InteractiveClient.this.properties); + view.setSize(new Dimension(640, 400)); + view.viewDocument(); + } catch (final XMLDBException ex) { + errorln("XMLDB error: " + ex.getMessage(), ex); + } + } + + @Override + protected void onFailure(final Throwable t) { + if (t instanceof XMLDBException xe) { + errorln("XMLDB error: " + xe.getMessage(), xe); + } else { + super.onFailure(t); + } + } + }.execute(); } private Optional getTraceWriter() { @@ -1138,7 +1157,7 @@ private Optional getTraceWriter() { traceWriter.write("" + EOL); this.lazyTraceWriter = Optional.of(traceWriter); } catch (final IOException ioe) { - errorln("Cannot open file " + options.traceQueriesFile.get()); + errorln("Cannot open file " + options.traceQueriesFile.get(), ioe); return Optional.empty(); } } @@ -2057,7 +2076,7 @@ private void connectToDatabase() { connect(); } catch (final Exception cnf) { if (options.startGUI && frame != null) { - frame.setStatus("Connection to database failed; message: " + cnf.getMessage()); + runOnFrameEdt(() -> frame.setStatus("Connection to database failed; message: " + cnf.getMessage())); } else { consoleErr("Connection to database failed; message: " + cnf.getMessage()); } @@ -2122,7 +2141,7 @@ public boolean run() throws Exception { if (current == null) { if (options.startGUI && frame != null) { - frame.setStatus("Could not retrieve collection " + path); + runOnFrameEdt(() -> frame.setStatus("Could not retrieve collection " + path)); } else { consoleErr("Could not retrieve collection " + path); } @@ -2233,7 +2252,7 @@ final boolean initializeGui() throws ClassNotFoundException, InstantiationExcept } else if (!errorMessage.isEmpty()) { // No pattern match, but we have an error. stop here - frame.dispose(); + runOnFrameEdt(frame::dispose); return true; } else { // No error message, continue startup. @@ -2501,15 +2520,36 @@ static final void consoleOut(final String msg) { } final void errorln(final String msg) { + errorln(msg, null); + } + + /** + * Log an error line to the shell or admin frame; optional {@code t} is traced to stderr when non-null. + */ + final void errorln(final String msg, final Throwable t) { if (options.startGUI && frame != null) { frame.display(msg + EOL); } else { - consoleErr(msg); + consoleErr(msg, t); + return; + } + if (t != null) { + t.printStackTrace(System.err); } } static final void consoleErr(final String msg) { + consoleErr(msg, null); + } + + /** + * Write to stderr; optional {@code t} is printed after the message. + */ + static final void consoleErr(final String msg, final Throwable t) { System.err.println(msg); //NOSONAR this has to go to the console + if (t != null) { + t.printStackTrace(System.err); + } } private Collection resolveCollection(final XmldbURI path) throws XMLDBException { @@ -2620,7 +2660,7 @@ public static Properties getSystemProperties() { try { sysProperties.load(InteractiveClient.class.getClassLoader().getResourceAsStream("org/exist/system.properties")); } catch (final IOException e) { - consoleErr("Unable to load system.properties from class loader"); + consoleErr("Unable to load system.properties from class loader", e); } return sysProperties; @@ -2629,4 +2669,18 @@ public static Properties getSystemProperties() { public static ImageIcon getExistIcon(final Class clazz) { return new javax.swing.ImageIcon(clazz.getResource("/org/exist/client/icons/x.png")); } + + /** + * Loads the eXist icon and applies the frame/window image (e.g. {@link javax.swing.JFrame#setIconImage}). + */ + public static void setExistImage(final Class clazz, final Consumer setter) { + setter.accept(getExistIcon(clazz).getImage()); + } + + /** + * Loads the eXist icon for a label or button (e.g. {@link javax.swing.JLabel#setIcon}). + */ + public static void setExistImageIcon(final Class clazz, final Consumer setter) { + setter.accept(getExistIcon(clazz)); + } } diff --git a/exist-core/src/main/java/org/exist/client/QueryDialog.java b/exist-core/src/main/java/org/exist/client/QueryDialog.java index 188010222e0..08ba8d6e3b4 100644 --- a/exist-core/src/main/java/org/exist/client/QueryDialog.java +++ b/exist-core/src/main/java/org/exist/client/QueryDialog.java @@ -34,7 +34,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.swing.BorderFactory; import javax.swing.Box; @@ -94,8 +93,6 @@ public class QueryDialog extends JFrame { private static final String QUERY_DIALOG_COMPILATION = "QueryDialog.Compilation"; private static final String QUERY_DIALOG_EXECUTION = "QueryDialog.Execution"; - private static final AtomicInteger QUERY_THREAD_ID = new AtomicInteger(); - private InteractiveClient client; private Collection collection; private Properties properties; @@ -112,7 +109,7 @@ public class QueryDialog extends JFrame { private JProgressBar progress; private JButton submitButton; private JButton killButton; - private QueryRunnable queryRunnable = null; + private QuerySwingWorker queryWorker = null; private Resource resource = null; private QueryDialog(final InteractiveClient client, final Collection collection, final Properties properties, boolean loadedFromDb) { @@ -120,7 +117,7 @@ private QueryDialog(final InteractiveClient client, final Collection collection, this.collection = collection; this.properties = properties; this.client = client; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); setupComponents(loadedFromDb); pack(); } @@ -226,7 +223,10 @@ private void setupComponents(boolean loadedFromDb) { if (collection instanceof LocalCollection) { killButton.setEnabled(true); } - queryRunnable = doQuery(); + queryWorker = doQuery(); + if (queryWorker == null) { + submitButton.setEnabled(true); + } }); toolbar.addSeparator(); @@ -236,11 +236,11 @@ private void setupComponents(boolean loadedFromDb) { toolbar.add(killButton); killButton.setEnabled(false); killButton.addActionListener(e -> { - if (queryRunnable != null) { - queryRunnable.killQuery(); + if (queryWorker != null) { + queryWorker.killQuery(); killButton.setEnabled(false); - queryRunnable = null; + queryWorker = null; } }); @@ -476,17 +476,16 @@ private void save(String stringToSave, String fileCategory) { } } - private QueryRunnable doQuery() { + private QuerySwingWorker doQuery() { final String xpath = query.getText(); if (xpath.isEmpty()) { return null; } resultDisplay.setText(""); - final QueryRunnable queryTask = new QueryRunnable(xpath); - final Thread queryThread = client.newClientThread("query-" + QUERY_THREAD_ID.getAndIncrement(), queryTask); - queryThread.start(); - return queryTask; + final QuerySwingWorker worker = new QuerySwingWorker(xpath); + worker.execute(); + return worker; } @@ -530,15 +529,15 @@ private void compileQuery() { setCursor(Cursor.getDefaultCursor()); } - private class QueryRunnable implements Runnable { + private final class QuerySwingWorker extends SwingWorker { private final String xpath; private final AtomicReference runningContext = new AtomicReference<>(); - public QueryRunnable(final String query) { + QuerySwingWorker(final String query) { this.xpath = query; } - public boolean killQuery() { + boolean killQuery() { final XQueryContext contextRef = runningContext.get(); if (contextRef != null) { final XQueryWatchDog xwd = contextRef.getWatchDog(); @@ -555,11 +554,13 @@ public boolean killQuery() { } @Override - public void run() { - statusMessage.setText(Messages.getString("QueryDialog.processingquerymessage")); - progress.setVisible(true); - progress.setIndeterminate(true); - setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + protected Void doInBackground() throws Exception { + publish(() -> { + statusMessage.setText(Messages.getString("QueryDialog.processingquerymessage")); + progress.setVisible(true); + progress.setIndeterminate(true); + setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + }); long tResult = 0; long tCompiled = 0; ResourceSet result = null; @@ -575,17 +576,16 @@ public void run() { final CompiledExpression compiled = service.compile(xpath); final long t1 = System.currentTimeMillis(); - // Check could also be collection instanceof LocalCollection if (compiled instanceof CompiledXQuery xQuery) { context = xQuery.getContext(); runningContext.set(context); } tCompiled = t1 - t0; - // In this way we can see the parsed structure meanwhile the query is StringWriter writer = new StringWriter(); service.dump(compiled, writer); - exprDisplay.setText(writer.toString()); + final String exprAfterCompile = writer.toString(); + publish(() -> exprDisplay.setText(exprAfterCompile)); result = service.execute(compiled); tResult = System.currentTimeMillis() - t1; @@ -595,22 +595,25 @@ public void run() { throw new XMLDBException(ErrorCodes.UNKNOWN_ERROR, "Query returned 'null' which it should never do, this is likely a bug that should be reported"); } - // jmfg: Is this still needed? I don't think so writer = new StringWriter(); service.dump(compiled, writer); - exprDisplay.setText(writer.toString()); + final String exprAfterExecute = writer.toString(); + publish(() -> exprDisplay.setText(exprAfterExecute)); - statusMessage.setText(Messages.getString("QueryDialog.retrievingmessage")); + publish(() -> statusMessage.setText(Messages.getString("QueryDialog.retrievingmessage"))); final int howmany = count.getNumber().intValue(); - progress.setIndeterminate(false); - progress.setMinimum(1); - progress.setMaximum(howmany); + publish(() -> { + progress.setIndeterminate(false); + progress.setMinimum(1); + progress.setMaximum(howmany); + }); int j = 0; int select = -1; final StringBuilder contents = new StringBuilder(); for (final ResourceIterator i = result.getIterator(); i.hasMoreResources() && j < howmany; j++) { + final int row = j; try (Resource processedResource = i.nextResource()) { - progress.setValue(j); + publish(() -> progress.setValue(row)); if (XML_RESOURCE.equals(processedResource.getResourceType())) { contents.append((String) processedResource.getContent()); } else { @@ -626,15 +629,24 @@ public void run() { } } } - resultTabs.setSelectedComponent(resultDisplayScrollPane); - resultDisplay.setText(contents.toString()); - resultDisplay.setCaretPosition(0); - statusMessage.setText(Messages.getString("QueryDialog.Found") + " " + result.getSize() + " " + Messages.getString("QueryDialog.items") + "." + - " " + Messages.getString(QUERY_DIALOG_COMPILATION) + ": " + tCompiled + "ms, " + Messages.getString(QUERY_DIALOG_EXECUTION) + ": " + tResult + "ms"); + final String resultText = contents.toString(); + final long tCompiledFinal = tCompiled; + final long tResultFinal = tResult; + final long resultSize = result.getSize(); + publish(() -> { + resultTabs.setSelectedComponent(resultDisplayScrollPane); + resultDisplay.setText(resultText); + resultDisplay.setCaretPosition(0); + statusMessage.setText(Messages.getString("QueryDialog.Found") + " " + resultSize + " " + Messages.getString("QueryDialog.items") + "." + + " " + Messages.getString(QUERY_DIALOG_COMPILATION) + ": " + tCompiledFinal + "ms, " + Messages.getString(QUERY_DIALOG_EXECUTION) + ": " + tResultFinal + "ms"); + }); } catch (final XMLDBException e) { - statusMessage.setText(Messages.getString(QUERY_DIALOG_ERROR) + ": " + InteractiveClient.getExceptionMessage(e) + ". " + Messages.getString(QUERY_DIALOG_COMPILATION) + ": " + tCompiled + "ms, " + Messages.getString(QUERY_DIALOG_EXECUTION) + ": " + tResult + "ms"); - progress.setVisible(false); - + final long tCompiledFinal = tCompiled; + final long tResultFinal = tResult; + publish(() -> { + statusMessage.setText(Messages.getString(QUERY_DIALOG_ERROR) + ": " + InteractiveClient.getExceptionMessage(e) + ". " + Messages.getString(QUERY_DIALOG_COMPILATION) + ": " + tCompiledFinal + "ms, " + Messages.getString(QUERY_DIALOG_EXECUTION) + ": " + tResultFinal + "ms"); + progress.setVisible(false); + }); ClientFrame.showErrorMessageQuery( Messages.getString("QueryDialog.queryrunerrormessage") + ": " @@ -643,23 +655,44 @@ public void run() { if (context != null) { context.runCleanupTasks(); } - context = null; - if (result != null) + if (result != null) { try { result.clear(); } catch (final XMLDBException e) { // ignore error } + } } - if (client.queryHistory.isEmpty() || !client.queryHistory.getLast().equals(xpath)) { - client.addToHistory(xpath); - client.writeQueryHistory(); - addQuery(xpath); + return null; + } + + @Override + protected void process(final List chunks) { + for (final Runnable chunk : chunks) { + chunk.run(); + } + } + + @Override + protected void done() { + try { + get(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (final java.util.concurrent.ExecutionException e) { + // surfaced via dialogs in doInBackground + } finally { + if (client.queryHistory.isEmpty() || !client.queryHistory.getLast().equals(xpath)) { + client.addToHistory(xpath); + client.writeQueryHistory(); + addQuery(xpath); + } + setCursor(Cursor.getDefaultCursor()); + progress.setVisible(false); + killButton.setEnabled(false); + submitButton.setEnabled(true); + queryWorker = null; } - setCursor(Cursor.getDefaultCursor()); - progress.setVisible(false); - killButton.setEnabled(false); - submitButton.setEnabled(true); } } diff --git a/exist-core/src/main/java/org/exist/client/TriggersDialog.java b/exist-core/src/main/java/org/exist/client/TriggersDialog.java index 398aeddae41..5a478d51c58 100644 --- a/exist-core/src/main/java/org/exist/client/TriggersDialog.java +++ b/exist-core/src/main/java/org/exist/client/TriggersDialog.java @@ -74,7 +74,7 @@ class TriggersDialog extends JFrame { public TriggersDialog(final String title, final InteractiveClient client) { super(title); this.client = client; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); //capture the frame's close event final WindowListener windowListener = new WindowAdapter() { @Override diff --git a/exist-core/src/main/java/org/exist/client/UploadDialog.java b/exist-core/src/main/java/org/exist/client/UploadDialog.java index 6de37f8a9cb..27a9b2004cd 100644 --- a/exist-core/src/main/java/org/exist/client/UploadDialog.java +++ b/exist-core/src/main/java/org/exist/client/UploadDialog.java @@ -63,7 +63,7 @@ public UploadDialog() { c.insets = new Insets(5, 5, 5, 5); JLabel label = new JLabel(Messages.getString("UploadDialog.1")); //$NON-NLS-1$ - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); c.gridx = 0; c.gridy = 0; c.anchor = GridBagConstraints.WEST; diff --git a/exist-core/src/main/java/org/exist/client/security/AccessControlEntryDialog.java b/exist-core/src/main/java/org/exist/client/security/AccessControlEntryDialog.java index 8a33af7e75b..e18441b0e63 100644 --- a/exist-core/src/main/java/org/exist/client/security/AccessControlEntryDialog.java +++ b/exist-core/src/main/java/org/exist/client/security/AccessControlEntryDialog.java @@ -53,7 +53,7 @@ public class AccessControlEntryDialog extends javax.swing.JFrame implements Dial public AccessControlEntryDialog(final UserManagementService userManagementService, final String title) throws XMLDBException { this.userManagementService = userManagementService; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); allUsernames = new HashSet<>(); for(final Account account : userManagementService.getAccounts()) { allUsernames.add(account.getName()); diff --git a/exist-core/src/main/java/org/exist/client/security/EditPropertiesDialog.java b/exist-core/src/main/java/org/exist/client/security/EditPropertiesDialog.java index 5768b37cc1f..c4e7aa01a06 100644 --- a/exist-core/src/main/java/org/exist/client/security/EditPropertiesDialog.java +++ b/exist-core/src/main/java/org/exist/client/security/EditPropertiesDialog.java @@ -90,7 +90,7 @@ public EditPropertiesDialog(final UserManagementService userManagementService, f this.mode = mode; this.acl = acl; this.applyTo = applyTo; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); initComponents(); setFormProperties(); } diff --git a/exist-core/src/main/java/org/exist/client/security/UserDialog.java b/exist-core/src/main/java/org/exist/client/security/UserDialog.java index 76f1a62578f..ff096aeff6f 100644 --- a/exist-core/src/main/java/org/exist/client/security/UserDialog.java +++ b/exist-core/src/main/java/org/exist/client/security/UserDialog.java @@ -56,7 +56,7 @@ public class UserDialog extends javax.swing.JFrame { public UserDialog(final UserManagementService userManagementService) { this.userManagementService = userManagementService; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); initComponents(); } diff --git a/exist-core/src/main/java/org/exist/client/security/UserManagerDialog.java b/exist-core/src/main/java/org/exist/client/security/UserManagerDialog.java index fee1069c1cc..1e145a2c505 100644 --- a/exist-core/src/main/java/org/exist/client/security/UserManagerDialog.java +++ b/exist-core/src/main/java/org/exist/client/security/UserManagerDialog.java @@ -67,7 +67,7 @@ public UserManagerDialog(final UserManagementService userManagementService, fina this.userManagementService = userManagementService; this.currentUser = currentUser; this.client = client; - this.setIconImage(InteractiveClient.getExistIcon(getClass()).getImage()); + InteractiveClient.setExistImage(getClass(), this::setIconImage); initComponents(); tblUsers.setDefaultRenderer(Object.class, new HighlightedTableCellRenderer()); tblGroups.setDefaultRenderer(Object.class, new HighlightedTableCellRenderer()); diff --git a/exist-core/src/test/java/org/exist/client/ClientDocumentEditSupportTest.java b/exist-core/src/test/java/org/exist/client/ClientDocumentEditSupportTest.java new file mode 100644 index 00000000000..fd3bdf58174 --- /dev/null +++ b/exist-core/src/test/java/org/exist/client/ClientDocumentEditSupportTest.java @@ -0,0 +1,49 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.client; + +import org.exist.xmldb.XmldbURI; +import org.junit.jupiter.api.Test; +import org.xmldb.api.base.ErrorCodes; +import org.xmldb.api.base.XMLDBException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ClientDocumentEditSupportTest { + + @Test + void loadPropagatesXmlDbException() { + final XmldbURI name = XmldbURI.create("/db/test.xml"); + assertThatThrownBy(() -> ClientDocumentEditSupport.load(name, () -> { + throw new XMLDBException(ErrorCodes.VENDOR_ERROR, "retrieve failed"); + })).isInstanceOf(XMLDBException.class).hasMessageContaining("retrieve failed"); + } + + @Test + void loadReturnsPayloadFromRetriever() throws XMLDBException { + final XmldbURI name = XmldbURI.create("/db/doc.xml"); + final ClientDocumentEditSupport.DocumentEditPayload p = ClientDocumentEditSupport.load(name, () -> null); + assertThat(p.name()).isSameAs(name); + assertThat(p.resource()).isNull(); + } +} diff --git a/exist-core/src/test/java/org/exist/client/ClientSwingEdtTest.java b/exist-core/src/test/java/org/exist/client/ClientSwingEdtTest.java new file mode 100644 index 00000000000..404791c00ff --- /dev/null +++ b/exist-core/src/test/java/org/exist/client/ClientSwingEdtTest.java @@ -0,0 +1,163 @@ +/* + * eXist-db Open Source Native XML Database + * Copyright (C) 2001 The eXist-db Authors + * + * info@exist-db.org + * http://www.exist-db.org + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ +package org.exist.client; + +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; + +import javax.swing.SwingUtilities; +import java.awt.GraphicsEnvironment; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link ClientSwingEdt}. + */ +class ClientSwingEdtTest { + + @Test + void invokeLaterIfNeededRunsOnEdtWhenCalledFromBackground() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicBoolean ranOnEdt = new AtomicBoolean(); + final CountDownLatch latch = new CountDownLatch(1); + + final Thread background = new Thread(() -> { + ClientSwingEdt.invokeLaterIfNeeded(() -> { + ranOnEdt.set(SwingUtilities.isEventDispatchThread()); + latch.countDown(); + }); + }); + background.start(); + background.join(); + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + SwingUtilities.invokeAndWait(() -> { + }); + assertThat(ranOnEdt.get()).isTrue(); + } + + @Test + void invokeLaterIfNeededRunsInlineWhenAlreadyOnEdt() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicReference edtThread = new AtomicReference<>(); + SwingUtilities.invokeAndWait(() -> edtThread.set(Thread.currentThread())); + final AtomicReference runThread = new AtomicReference<>(); + SwingUtilities.invokeAndWait(() -> ClientSwingEdt.invokeLaterIfNeeded(() -> runThread.set(Thread.currentThread()))); + + assertThat(runThread.get()).isSameAs(edtThread.get()); + } + + @Test + void invokeAndWaitIfNeededRunsOnEdtFromBackground() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicBoolean ranOnEdt = new AtomicBoolean(); + final Thread background = new Thread(() -> ClientSwingEdt.invokeAndWaitIfNeeded(() -> ranOnEdt.set(SwingUtilities.isEventDispatchThread()))); + background.start(); + background.join(); + + assertThat(ranOnEdt.get()).isTrue(); + } + + @Test + void invokeAndWaitIfNeededRunsInlineWhenAlreadyOnEdt() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicReference edtThread = new AtomicReference<>(); + SwingUtilities.invokeAndWait(() -> edtThread.set(Thread.currentThread())); + final AtomicReference runThread = new AtomicReference<>(); + SwingUtilities.invokeAndWait(() -> ClientSwingEdt.invokeAndWaitIfNeeded(() -> runThread.set(Thread.currentThread()))); + + assertThat(runThread.get()).isSameAs(edtThread.get()); + } + + @Test + void invokeAndWaitIfNeededPropagatesRuntimeExceptionFromBackground() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicReference uncaught = new AtomicReference<>(); + final CountDownLatch finished = new CountDownLatch(1); + final Thread background = new Thread(() -> { + try { + ClientSwingEdt.invokeAndWaitIfNeeded(() -> { + throw new IllegalStateException("boom"); + }); + } finally { + finished.countDown(); + } + }); + background.setUncaughtExceptionHandler((t, ex) -> uncaught.set(ex)); + background.start(); + assertThat(finished.await(5, TimeUnit.SECONDS)).isTrue(); + background.join(); + assertThat(uncaught.get()).isInstanceOf(IllegalStateException.class).hasMessage("boom"); + } + + @Test + void invokeAndWaitIfNeededPropagatesErrorFromBackground() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final AtomicReference uncaught = new AtomicReference<>(); + final CountDownLatch finished = new CountDownLatch(1); + final Thread background = new Thread(() -> { + try { + ClientSwingEdt.invokeAndWaitIfNeeded(() -> { + throw new AssertionError("fatal"); + }); + } finally { + finished.countDown(); + } + }); + background.setUncaughtExceptionHandler((t, ex) -> uncaught.set(ex)); + background.start(); + assertThat(finished.await(5, TimeUnit.SECONDS)).isTrue(); + background.join(); + assertThat(uncaught.get()).isInstanceOf(AssertionError.class).hasMessage("fatal"); + } + + @Test + void invokeLaterIfNeededPreservesOrderFromBackground() throws Exception { + Assumptions.assumeFalse(GraphicsEnvironment.isHeadless()); + + final List order = Collections.synchronizedList(new ArrayList<>()); + final Thread background = new Thread(() -> { + for (int i = 1; i <= 3; i++) { + final int n = i; + ClientSwingEdt.invokeLaterIfNeeded(() -> order.add(n)); + } + }); + background.start(); + background.join(); + SwingUtilities.invokeAndWait(() -> { + }); + assertThat(order).containsExactly(1, 2, 3); + } +}