Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.InjectionException;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.ui.PlatformUI;
Expand Down Expand Up @@ -334,7 +336,7 @@ private boolean loadHandler() {
// Load the handler.
try {
if (configurationElement != null) {
handler = (IHandler) configurationElement.createExecutableExtension(handlerAttributeName);
handler = adapt(configurationElement.createExecutableExtension(handlerAttributeName));
handler.addHandlerListener(getHandlerListener());
if (handler instanceof IObjectWithState) {
for (String id : getStateIds()) {
Expand All @@ -346,8 +348,9 @@ private boolean loadHandler() {
return true;
}

} catch (final ClassCastException e) {
final String message = "The proxied handler was the wrong class"; //$NON-NLS-1$
} catch (final InjectionException e) {
final String message = "The proxied handler '" //$NON-NLS-1$
+ getConfigurationElementAttribute() + "' could not be injected"; //$NON-NLS-1$
final IStatus status = new Status(IStatus.ERROR, WorkbenchPlugin.PI_WORKBENCH, 0, message, e);
WorkbenchPlugin.log(message, status);
configurationElement = null;
Expand All @@ -367,6 +370,18 @@ private boolean loadHandler() {
return true;
}

/**
* Wraps handler contributions that do not implement {@link IHandler} so that
* their {@code @Execute} and {@code @CanExecute} methods are dispatched through
* dependency injection.
*/
private static IHandler adapt(Object contribution) {
if (contribution instanceof IHandler handler) {
return handler;
}
return new PojoHandlerAdapter(contribution, PlatformUI.getWorkbench().getService(IEclipseContext.class));
}

private IHandlerListener getHandlerListener() {
if (handlerListener == null) {
handlerListener = handlerEvent -> fireHandlerChanged(new HandlerEvent(HandlerProxy.this,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
/*******************************************************************************
* Copyright (c) 2026 Lars Vogel and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/

package org.eclipse.ui.internal.handlers;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.commands.NotHandledException;
import org.eclipse.core.expressions.IEvaluationContext;
import org.eclipse.e4.core.commands.ExpressionContext;
import org.eclipse.e4.core.commands.internal.HandlerServiceImpl;
import org.eclipse.e4.core.contexts.ContextInjectionFactory;
import org.eclipse.e4.core.contexts.EclipseContextFactory;
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.InjectionException;
import org.eclipse.e4.core.di.annotations.CanExecute;
import org.eclipse.e4.core.di.annotations.Execute;
import org.eclipse.swt.widgets.Event;
import org.eclipse.ui.internal.WorkbenchPlugin;

/**
* Adapts a handler contribution that does not implement
* {@link org.eclipse.core.commands.IHandler} to the legacy handler API by
* dispatching to its {@link Execute} and {@link CanExecute} methods through
* dependency injection.
*/
class PojoHandlerAdapter extends AbstractHandler {

private static final Object MISSING_EXECUTE = new Object();

private final Object handler;

/** The context the handler was injected from, may be <code>null</code>. */
private final IEclipseContext injectionContext;

/**
* Injects the given handler and prepares it for dispatch.
*
* @throws InjectionException if the handler cannot be injected
*/
PojoHandlerAdapter(Object handler, IEclipseContext injectionContext) {
this.handler = handler;
this.injectionContext = injectionContext;
if (injectionContext != null) {
ContextInjectionFactory.inject(handler, injectionContext);
}
}

@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
IEclipseContext executionContext = executionContextOf(event.getApplicationContext());
if (executionContext == null) {
throw new ExecutionException("No IEclipseContext available to execute " + handler.getClass().getName()); //$NON-NLS-1$
}
IEclipseContext staticContext = EclipseContextFactory.create();
try {
staticContext.set(HandlerServiceImpl.PARM_MAP, event.getParameters());
staticContext.set(ExecutionEvent.class, event);
if (event.getTrigger() instanceof Event trigger) {
staticContext.set(Event.class, trigger);
}
Object result = ContextInjectionFactory.invoke(handler, Execute.class, executionContext, staticContext,
MISSING_EXECUTE);
if (result == MISSING_EXECUTE) {
throw new ExecutionException(handler.getClass().getName() + " handler is missing @Execute", //$NON-NLS-1$
new NotHandledException(handler.getClass().getName()));
}
return result;
} catch (InjectionException e) {
if (e.getCause() instanceof ExecutionException cause) {
throw cause;
}
throw new ExecutionException("Error executing " + handler.getClass().getName(), e); //$NON-NLS-1$
} finally {
staticContext.dispose();
}
}

@Override
public void setEnabled(Object evaluationContext) {
IEclipseContext executionContext = executionContextOf(evaluationContext);
if (executionContext == null) {
return;
}
IEclipseContext staticContext = EclipseContextFactory.create();
try {
Object result = ContextInjectionFactory.invoke(handler, CanExecute.class, executionContext, staticContext,
Boolean.TRUE);
// a @CanExecute that does not return boolean leaves enablement untouched
if (result instanceof Boolean enabled) {
setBaseEnabled(enabled.booleanValue());
}
} catch (InjectionException e) {
WorkbenchPlugin.log("Error while evaluating @CanExecute of " + handler.getClass().getName(), e); //$NON-NLS-1$
setBaseEnabled(false);
} finally {
staticContext.dispose();
}
}

/**
* Unwraps the {@link IEclipseContext} the given evaluation object was created
* from, falling back to the active leaf of the injection context.
*/
private IEclipseContext executionContextOf(Object evaluationObject) {
if (evaluationObject instanceof IEclipseContext context) {
return context;
}
if (evaluationObject instanceof ExpressionContext context) {

Check warning on line 121 in bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/handlers/PojoHandlerAdapter.java

View check run for this annotation

Jenkins - Eclipse Platform / API Tools

ILLEGAL_REFERENCE

NORMAL: PojoHandlerAdapter illegally references field ExpressionContext.eclipseContext
return context.eclipseContext;
}
if (evaluationObject instanceof IEvaluationContext context) {
return executionContextOf(context.getParent());
}
return injectionContext == null ? null : injectionContext.getActiveLeaf();
}

@Override
public void dispose() {
if (injectionContext != null) {
try {
ContextInjectionFactory.uninject(handler, injectionContext);
} catch (InjectionException e) {
WorkbenchPlugin.log("Error while uninjecting " + handler.getClass().getName(), e); //$NON-NLS-1$
}
}
super.dispose();
}

Object getHandler() {
return handler;
}

@Override
public String toString() {
return handler.toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
ToggleStateTest.class,
RadioStateTest.class,
WorkbenchStateTest.class,
E4CommandImageTest.class
E4CommandImageTest.class,
PojoHandlerTest.class
})
public final class CommandsTestSuite {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*******************************************************************************
* Copyright (c) 2026 Lars Vogel and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.tests.commands;

import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.di.annotations.CanExecute;
import org.eclipse.e4.core.di.annotations.Execute;

import jakarta.inject.Inject;

/**
* A handler contributed to <code>org.eclipse.ui.handlers</code> that implements
* neither <code>IHandler</code> nor any other platform interface.
*/
public class PojoHandler {

public static boolean executed;

public static boolean canExecute = true;

public static IEclipseContext injectedContext;

@Inject
void setContext(IEclipseContext context) {
injectedContext = context;
}

@CanExecute
public boolean canExecute() {
return canExecute;
}

@Execute
public Object execute() {
executed = true;
return "executed"; //$NON-NLS-1$
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*******************************************************************************
* Copyright (c) 2026 Lars Vogel and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.ui.tests.commands;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.NotEnabledException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.handlers.IHandlerService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

/**
* Tests that a handler contributed to <code>org.eclipse.ui.handlers</code> is
* dispatched through dependency injection when it does not implement
* <code>IHandler</code>.
*/
public class PojoHandlerTest {

private static final String COMMAND_ID = "org.eclipse.ui.tests.commands.pojoHandler";

private IHandlerService handlerService;

private ICommandService commandService;

@BeforeEach
public void setUp() {
handlerService = PlatformUI.getWorkbench().getService(IHandlerService.class);
commandService = PlatformUI.getWorkbench().getService(ICommandService.class);
PojoHandler.executed = false;
PojoHandler.canExecute = true;
}

@AfterEach
public void tearDown() {
PojoHandler.executed = false;
PojoHandler.canExecute = true;
}

@Test
public void executeDispatchesToExecuteAnnotatedMethod() throws Exception {
Object result = handlerService.executeCommand(COMMAND_ID, null);

assertTrue(PojoHandler.executed, "@Execute method was not called");
assertEquals("executed", result, "the return value of @Execute was not passed back");
}

@Test
public void canExecuteControlsEnablement() throws Exception {
PojoHandler.canExecute = false;

assertThrows(NotEnabledException.class, () -> handlerService.executeCommand(COMMAND_ID, null));
assertFalse(PojoHandler.executed, "@Execute was called although @CanExecute returned false");

PojoHandler.canExecute = true;
handlerService.executeCommand(COMMAND_ID, null);
assertTrue(PojoHandler.executed, "@Execute was not called after @CanExecute turned true");
}

@Test
public void handlerIsInjected() throws Exception {
handlerService.executeCommand(COMMAND_ID, null);

assertNotNull(PojoHandler.injectedContext, "the handler was not injected");
}

@Test
public void commandIsHandled() throws Exception {
Command command = commandService.getCommand(COMMAND_ID);

assertTrue(command.isDefined(), "the test command is not defined");
handlerService.executeCommand(COMMAND_ID, null);
assertTrue(command.isHandled(), "the command is not handled by the POJO");
}
}
15 changes: 15 additions & 0 deletions tests/org.eclipse.ui.tests/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4962,4 +4962,19 @@
point="org.eclipse.core.resources.markers">
</extension>

<!-- Handler contribution that does not implement IHandler -->
<extension
point="org.eclipse.ui.commands">
<command
id="org.eclipse.ui.tests.commands.pojoHandler"
name="POJO Handler Test" />
</extension>
<extension
point="org.eclipse.ui.handlers">
<handler
commandId="org.eclipse.ui.tests.commands.pojoHandler"
class="org.eclipse.ui.tests.commands.PojoHandler">
</handler>
</extension>

</plugin>
Loading