diff --git a/plugin/META-INF/MANIFEST.MF b/plugin/META-INF/MANIFEST.MF index f2f5db3d..bb80724d 100644 --- a/plugin/META-INF/MANIFEST.MF +++ b/plugin/META-INF/MANIFEST.MF @@ -62,6 +62,7 @@ Bundle-Classpath: target/classes/, target/dependency/regions-2.28.26.jar, target/dependency/retries-2.28.26.jar, target/dependency/retries-spi-2.28.26.jar, + target/dependency/rxjava-3.1.5.jar, target/dependency/sdk-core-2.28.26.jar, target/dependency/third-party-jackson-core-2.28.26.jar, target/dependency/utils-2.28.26.jar diff --git a/plugin/pom.xml b/plugin/pom.xml index f6c73679..beaf9968 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -42,11 +42,21 @@ + + io.reactivex.rxjava3 + rxjava + 3.1.5 + jakarta.inject jakarta.inject-api 2.0.1 + + io.reactivex.rxjava3 + rxjava + 3.1.5 + com.fasterxml.jackson.core jackson-databind @@ -139,7 +149,7 @@ ${project.build.directory}/dependency - software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven + io.reactivex,software.amazon.awssdk,com.fasterxml.jackson,com.nimbusds,jakarta.inject,commons-codec,org.apache.httpcomponents,org.reactivestreams,org.apache.maven diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java new file mode 100644 index 00000000..a58f52c0 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/EventBroker.java @@ -0,0 +1,37 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.broker; + +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.functions.Consumer; +import io.reactivex.rxjava3.schedulers.Schedulers; +import io.reactivex.rxjava3.subjects.PublishSubject; +import io.reactivex.rxjava3.subjects.Subject; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; + +public final class EventBroker { + + private final Subject eventBus = PublishSubject.create().toSerialized(); + + public void post(final T event) { + if (event == null) { + return; + } + eventBus.onNext(event); + } + + public Disposable subscribe(final Class eventType, final EventObserver observer) { + Consumer consumer = new Consumer<>() { + @Override + public void accept(final T event) { + observer.onEvent(event); + } + }; + + return eventBus.ofType(eventType) + .observeOn(Schedulers.computation()) + .subscribe(consumer); + } + +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/api/EventObserver.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/api/EventObserver.java new file mode 100644 index 00000000..1785cf93 --- /dev/null +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/broker/api/EventObserver.java @@ -0,0 +1,9 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.broker.api; + +@FunctionalInterface +public interface EventObserver { + void onEvent(T event); +} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthSourceProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthSourceProvider.java index 41679e77..604c460e 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthSourceProvider.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthSourceProvider.java @@ -13,6 +13,8 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.services.ISourceProviderService; +import io.reactivex.rxjava3.disposables.Disposable; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; @@ -30,12 +32,13 @@ * @see software.aws.toolkits.eclipse.amazonq.toolbar * @see software.aws.toolkits.eclipse.amazonq.toolbar-unauthenticated */ -public final class AuthSourceProvider extends AbstractSourceProvider implements AuthStatusChangedListener { +public final class AuthSourceProvider extends AbstractSourceProvider implements EventObserver { public static final String IS_LOGGED_IN_VARIABLE_ID = "is_logged_in"; private boolean isLoggedIn = false; + private Disposable authStateSubscription; public AuthSourceProvider() { - AuthStatusProvider.addAuthStatusChangeListener(this); + authStateSubscription = Activator.getEventBroker().subscribe(AuthState.class, this); isLoggedIn = Activator.getLoginService().getAuthState().isLoggedIn(); } @@ -54,7 +57,7 @@ public void dispose() { // Notify listeners that this provider is being disposed fireSourceChanged(ISources.WORKBENCH, IS_LOGGED_IN_VARIABLE_ID, null); - AuthStatusProvider.removeAuthStatusChangeListener(this); + authStateSubscription.dispose(); } @Override @@ -69,7 +72,7 @@ public void setIsLoggedIn(final Boolean isLoggedIn) { public static AuthSourceProvider getProvider() { IWorkbench workbench = PlatformUI.getWorkbench(); - ISourceProviderService sourceProviderService = (ISourceProviderService) workbench + ISourceProviderService sourceProviderService = workbench .getService(ISourceProviderService.class); AuthSourceProvider provider = (AuthSourceProvider) sourceProviderService .getSourceProvider(AuthSourceProvider.IS_LOGGED_IN_VARIABLE_ID); @@ -77,7 +80,7 @@ public static AuthSourceProvider getProvider() { } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { boolean isLoggedIn = authState.isLoggedIn(); Display.getDefault().asyncExec(() -> { setIsLoggedIn(isLoggedIn); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusChangedListener.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusChangedListener.java deleted file mode 100644 index 9d621419..00000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusChangedListener.java +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.eclipse.amazonq.lsp.auth; - -import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; - -public interface AuthStatusChangedListener { - void onAuthStatusChanged(AuthState authState); -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusProvider.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusProvider.java deleted file mode 100644 index 7165d08b..00000000 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/AuthStatusProvider.java +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package software.aws.toolkits.eclipse.amazonq.lsp.auth; - -import java.util.ArrayList; -import java.util.List; - -import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; - -public final class AuthStatusProvider { - private static final List LISTENERS = new ArrayList<>(); - private static AuthState prevAuthState; - - private AuthStatusProvider() { - //prevent instantiation - } - - public static void addAuthStatusChangeListener(final AuthStatusChangedListener listener) { - LISTENERS.add(listener); - } - - public static void removeAuthStatusChangeListener(final AuthStatusChangedListener listener) { - LISTENERS.remove(listener); - } - - public static void notifyAuthStatusChanged(final AuthState authState) { - if (prevAuthState != null && prevAuthState.equals(authState)) { - return; - } - - prevAuthState = authState; - - for (AuthStatusChangedListener listener : LISTENERS) { - listener.onAuthStatusChanged(authState); - } - } -} diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java index 723a0c21..9f34ee44 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManager.java @@ -41,6 +41,7 @@ public final class DefaultAuthStateManager implements AuthStateManager { private LoginParams loginParams; // used in login's getSsoToken params private String issuerUrl; // used in AmazonQLspClientImpl.getConnectionMetadata() private String ssoTokenId; // used in logout's invalidateSsoToken params + private AuthState previousAuthState = null; public DefaultAuthStateManager(final PluginStore pluginStore) { this.authPluginStore = new AuthPluginStore(pluginStore); @@ -82,7 +83,6 @@ public void toExpired() { toLoggedOut(); return; } - updateState(AuthStateType.EXPIRED, loginType, loginParams, ssoTokenId); } @@ -119,7 +119,11 @@ private void updateState(final AuthStateType authStatusType, final LoginType log * This notification is critical for ensuring all plugin components reflect the current * authentication state. */ - AuthStatusProvider.notifyAuthStatusChanged(getAuthState()); + AuthState newAuthState = getAuthState(); + if (previousAuthState == null || newAuthState.authStateType() != previousAuthState.authStateType()) { + Activator.getEventBroker().post(newAuthState); + } + previousAuthState = newAuthState; } private void syncAuthStateWithPluginStore() { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java index 827081d9..b8396a7f 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/plugin/Activator.java @@ -6,6 +6,8 @@ import org.eclipse.ui.plugin.AbstractUIPlugin; import org.osgi.framework.BundleContext; +import software.aws.toolkits.eclipse.amazonq.broker.EventBroker; +import software.aws.toolkits.eclipse.amazonq.chat.ChatStateManager; import software.aws.toolkits.eclipse.amazonq.configuration.DefaultPluginStore; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; import software.aws.toolkits.eclipse.amazonq.lsp.auth.DefaultLoginService; @@ -14,11 +16,10 @@ import software.aws.toolkits.eclipse.amazonq.providers.LspProviderImpl; import software.aws.toolkits.eclipse.amazonq.telemetry.service.DefaultTelemetryService; import software.aws.toolkits.eclipse.amazonq.telemetry.service.TelemetryService; -import software.aws.toolkits.eclipse.amazonq.util.PluginLogger; -import software.aws.toolkits.eclipse.amazonq.chat.ChatStateManager; import software.aws.toolkits.eclipse.amazonq.util.CodeReferenceLoggingService; import software.aws.toolkits.eclipse.amazonq.util.DefaultCodeReferenceLoggingService; import software.aws.toolkits.eclipse.amazonq.util.LoggingService; +import software.aws.toolkits.eclipse.amazonq.util.PluginLogger; public class Activator extends AbstractUIPlugin { @@ -30,6 +31,7 @@ public class Activator extends AbstractUIPlugin { private static LoginService loginService; private static CodeReferenceLoggingService codeReferenceLoggingService; private static PluginStore pluginStore; + private static EventBroker eventBroker = new EventBroker(); public Activator() { super(); @@ -76,5 +78,8 @@ public static PluginStore getPluginStore() { public static CodeReferenceLoggingService getCodeReferenceLoggingService() { return codeReferenceLoggingService; } + public static EventBroker getEventBroker() { + return eventBroker; + } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java index 59772dec..ebe4b33d 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQChatWebview.java @@ -117,7 +117,7 @@ public void completed(final ProgressEvent event) { }); // Check if user is authenticated and build view accordingly - onAuthStatusChanged(authState); + onEvent(authState); } private Browser getAndUpdateStateManager() { @@ -127,7 +127,7 @@ private Browser getAndUpdateStateManager() { } @Override - public final void onAuthStatusChanged(final AuthState authState) { + public final void onEvent(final AuthState authState) { Display.getDefault().asyncExec(() -> { amazonQCommonActions.updateActionVisibility(authState, getViewSite()); if (authState.isExpired()) { diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java index d154c9d7..c6a01445 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/AmazonQView.java @@ -9,20 +9,22 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.ui.part.ViewPart; +import io.reactivex.rxjava3.disposables.Disposable; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.controllers.AmazonQViewController; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusChangedListener; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusProvider; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.ThemeDetector; import software.aws.toolkits.eclipse.amazonq.views.actions.AmazonQCommonActions; -public abstract class AmazonQView extends ViewPart implements AuthStatusChangedListener { +public abstract class AmazonQView extends ViewPart implements EventObserver { private AmazonQViewController viewController; private AmazonQCommonActions amazonQCommonActions; private static final ThemeDetector THEME_DETECTOR = new ThemeDetector(); + private Disposable authStateSubscription; + protected AmazonQView() { this.viewController = new AmazonQViewController(); } @@ -81,10 +83,10 @@ private void setupActions(final AuthState authState) { } private void setupAuthStatusListeners() { - AuthStatusProvider.addAuthStatusChangeListener(this); - AuthStatusProvider.addAuthStatusChangeListener(amazonQCommonActions.getSignoutAction()); - AuthStatusProvider.addAuthStatusChangeListener(amazonQCommonActions.getFeedbackDialogContributionAction()); - AuthStatusProvider.addAuthStatusChangeListener(amazonQCommonActions.getCustomizationDialogContributionAction()); + authStateSubscription = Activator.getEventBroker().subscribe(AuthState.class, this); + Activator.getEventBroker().subscribe(AuthState.class, amazonQCommonActions.getSignoutAction()); + Activator.getEventBroker().subscribe(AuthState.class, amazonQCommonActions.getFeedbackDialogContributionAction()); + Activator.getEventBroker().subscribe(AuthState.class, amazonQCommonActions.getCustomizationDialogContributionAction()); } @Override @@ -123,7 +125,7 @@ function waitForFunction(functionName, timeout = 30000) { */ @Override public void dispose() { - AuthStatusProvider.removeAuthStatusChangeListener(this); + authStateSubscription.dispose(); super.dispose(); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ReauthenticateView.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ReauthenticateView.java index d0635458..6a48a6e5 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ReauthenticateView.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ReauthenticateView.java @@ -18,18 +18,18 @@ import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Link; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusChangedListener; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusProvider; +import io.reactivex.rxjava3.disposables.Disposable; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.util.Constants; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.views.actions.SignoutAction; -import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; -public final class ReauthenticateView extends CallToActionView implements AuthStatusChangedListener { +public final class ReauthenticateView extends CallToActionView implements EventObserver { public static final String ID = "software.aws.toolkits.eclipse.amazonq.views.ReauthenticateView"; private static final String ICON_PATH = "icons/AmazonQ64.png"; @@ -38,10 +38,12 @@ public final class ReauthenticateView extends CallToActionView implements AuthSt private static final String BUTTON_LABEL = "Re-authenticate"; private static final String LINK_LABEL = "Sign out"; + private Disposable authStateSubscription; + public ReauthenticateView() { // It is necessary for this view to be an `AuthStatusChangedListener` to switch the view back to Q Chat after the authentication // flow is successful. Without this listener, the re-authentication will succeed but the view will remain present. - AuthStatusProvider.addAuthStatusChangeListener(this); + authStateSubscription = Activator.getEventBroker().subscribe(AuthState.class, this); } @Override @@ -98,7 +100,7 @@ public void widgetSelected(final SelectionEvent e) { } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { Display.getDefault().asyncExec(() -> { if (authState.isLoggedIn()) { ViewVisibilityManager.showChatView("update"); @@ -138,7 +140,7 @@ protected void updateButtonStyle(final Button button) { @Override public void dispose() { - AuthStatusProvider.removeAuthStatusChangeListener(this); + authStateSubscription.dispose(); } @Override diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java index 381459a7..f41b9a30 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/ToolkitLoginWebview.java @@ -92,11 +92,11 @@ public Object function(final Object[] arguments) { amazonQCommonActions = getAmazonQCommonActions(); // Check if user is authenticated and build view accordingly - onAuthStatusChanged(authState); + onEvent(authState); } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { var browser = getBrowser(); Display.getDefault().asyncExec(() -> { amazonQCommonActions.updateActionVisibility(authState, getViewSite()); diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/CustomizationDialogContributionItem.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/CustomizationDialogContributionItem.java index 7c9fb6a1..5e8e3cfa 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/CustomizationDialogContributionItem.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/CustomizationDialogContributionItem.java @@ -4,6 +4,7 @@ package software.aws.toolkits.eclipse.amazonq.views.actions; import java.util.Objects; + import org.eclipse.jface.action.ContributionItem; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; @@ -13,18 +14,19 @@ import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IViewSite; + import jakarta.inject.Inject; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusChangedListener; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; +import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.util.Constants; import software.aws.toolkits.eclipse.amazonq.views.CustomizationDialog; -import software.aws.toolkits.eclipse.amazonq.views.model.Customization; import software.aws.toolkits.eclipse.amazonq.views.CustomizationDialog.ResponseSelection; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; -import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; +import software.aws.toolkits.eclipse.amazonq.views.model.Customization; -public final class CustomizationDialogContributionItem extends ContributionItem implements AuthStatusChangedListener { +public final class CustomizationDialogContributionItem extends ContributionItem implements EventObserver { private static final String CUSTOMIZATION_MENU_ITEM_TEXT = "Select Customization"; @Inject @@ -45,7 +47,7 @@ public void updateVisibility(final AuthState authState) { } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { updateVisibility(authState); } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/FeedbackDialogContributionItem.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/FeedbackDialogContributionItem.java index e7986f15..172e3733 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/FeedbackDialogContributionItem.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/FeedbackDialogContributionItem.java @@ -5,12 +5,12 @@ import org.eclipse.ui.IViewSite; import jakarta.inject.Inject; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusChangedListener; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.views.DialogContributionItem; import software.aws.toolkits.eclipse.amazonq.views.FeedbackDialog; -public final class FeedbackDialogContributionItem implements AuthStatusChangedListener { +public final class FeedbackDialogContributionItem implements EventObserver { private static final String SHARE_FEEDBACK_MENU_ITEM_TEXT = "Share Feedback..."; @Inject @@ -40,7 +40,7 @@ public DialogContributionItem getDialogContributionItem() { } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { updateVisibility(authState); } } diff --git a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java index cd0e2a61..4eeae8ea 100644 --- a/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java +++ b/plugin/src/software/aws/toolkits/eclipse/amazonq/views/actions/SignoutAction.java @@ -2,16 +2,16 @@ import org.eclipse.jface.action.Action; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; import software.aws.toolkits.eclipse.amazonq.customization.CustomizationUtil; -import software.aws.toolkits.eclipse.amazonq.lsp.auth.AuthStatusChangedListener; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.plugin.Activator; +import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; import software.aws.toolkits.eclipse.amazonq.util.Constants; -import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; import software.aws.toolkits.eclipse.amazonq.util.PluginUtils; -import software.aws.toolkits.eclipse.amazonq.telemetry.UiTelemetryProvider; +import software.aws.toolkits.eclipse.amazonq.util.ThreadingUtils; -public final class SignoutAction extends Action implements AuthStatusChangedListener { +public final class SignoutAction extends Action implements EventObserver { public SignoutAction() { setText("Sign out"); } @@ -42,7 +42,7 @@ public void updateVisibility(final AuthState authState) { } @Override - public void onAuthStatusChanged(final AuthState authState) { + public void onEvent(final AuthState authState) { updateVisibility(authState); } } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java new file mode 100644 index 00000000..2fe0995a --- /dev/null +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/broker/EventBrokerTest.java @@ -0,0 +1,116 @@ +// Copyright 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.eclipse.amazonq.broker; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.reactivex.rxjava3.disposables.Disposable; +import software.aws.toolkits.eclipse.amazonq.broker.api.EventObserver; + +public final class EventBrokerTest { + + private record TestEvent(String message, int id) { + } + + private EventBroker eventBroker; + + @BeforeEach + void setupBeforeEach() { + eventBroker = new EventBroker(); + } + + @Test + void testEventDelivery() { + TestEvent testEvent = new TestEvent("test message 1", 1); + EventObserver mockObserver = mock(EventObserver.class); + + Disposable subscription = eventBroker.subscribe(TestEvent.class, mockObserver); + eventBroker.post(testEvent); + + verify(mockObserver, timeout(100)).onEvent(testEvent); + + subscription.dispose(); + } + + @Test + void testNullEventsIgnored() { + EventObserver mockObserver = mock(EventObserver.class); + + Disposable subscription = eventBroker.subscribe(String.class, mockObserver); + eventBroker.post(null); + + verify(mockObserver, never()).onEvent(any(String.class)); + + subscription.dispose(); + } + + @Test + void verifyEventOrderingMaintained() { + TestEvent firstEvent = new TestEvent("a message", 1); + TestEvent secondEvent = new TestEvent("another message", 2); + TestEvent thirdEvent = new TestEvent("a message", 3); + + EventObserver mockObserver = mock(EventObserver.class); + + Disposable subscription = eventBroker.subscribe(TestEvent.class, mockObserver); + eventBroker.post(firstEvent); + eventBroker.post(secondEvent); + eventBroker.post(thirdEvent); + + verify(mockObserver, timeout(100)).onEvent(firstEvent); + verify(mockObserver, timeout(100)).onEvent(secondEvent); + verify(mockObserver, timeout(100)).onEvent(thirdEvent); + + verifyNoMoreInteractions(mockObserver); + + subscription.dispose(); + } + + @Test + void testDifferentEventTypesIsolation() { + class OtherTestEvent { + private final int value; + + OtherTestEvent(final int value) { + this.value = value; + } + + public int getValue() { + return value; + } + } + + TestEvent testEvent = new TestEvent("test message", 1); + TestEvent secondEvent = new TestEvent("test message", 2); + OtherTestEvent otherEvent = new OtherTestEvent(42); + + EventObserver testEventObserver = mock(EventObserver.class); + EventObserver otherEventObserver = mock(EventObserver.class); + + Disposable testEventSubscription = eventBroker.subscribe(TestEvent.class, testEventObserver); + Disposable otherEventSubscription = eventBroker.subscribe(OtherTestEvent.class, otherEventObserver); + + eventBroker.post(testEvent); + eventBroker.post(otherEvent); + eventBroker.post(secondEvent); + + verify(testEventObserver, timeout(100).times(2)).onEvent(any()); + verify(otherEventObserver, timeout(100).times(1)).onEvent(any()); + + verifyNoMoreInteractions(testEventObserver); + verifyNoMoreInteractions(otherEventObserver); + + testEventSubscription.dispose(); + otherEventSubscription.dispose(); + } + +} diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java index 03e12b21..ae8d2d20 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/extensions/implementation/ActivatorStaticMockExtension.java @@ -16,6 +16,7 @@ import org.mockito.MockedStatic; import org.mockito.Mockito; +import software.aws.toolkits.eclipse.amazonq.broker.EventBroker; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; import software.aws.toolkits.eclipse.amazonq.extensions.api.StaticMockExtension; import software.aws.toolkits.eclipse.amazonq.lsp.auth.LoginService; @@ -44,6 +45,7 @@ public void beforeEach(final ExtensionContext context) { LoginService loginServiceMock = Mockito.mock(LoginService.class); PluginStore pluginStoreMock = Mockito.mock(PluginStore.class); CodeReferenceLoggingService codeReferenceLoggingServiceMock = Mockito.mock(CodeReferenceLoggingService.class); + EventBroker eventBrokerMock = Mockito.mock(EventBroker.class); IPreferenceStore mockPreferenceStore = Mockito.mock(IPreferenceStore.class); when(activatorMock.getPreferenceStore()).thenReturn(mockPreferenceStore); @@ -55,6 +57,7 @@ public void beforeEach(final ExtensionContext context) { activatorStaticMock.when(Activator::getLoginService).thenReturn(loginServiceMock); activatorStaticMock.when(Activator::getPluginStore).thenReturn(pluginStoreMock); activatorStaticMock.when(Activator::getCodeReferenceLoggingService).thenReturn(codeReferenceLoggingServiceMock); + activatorStaticMock.when(Activator::getEventBroker).thenReturn(eventBrokerMock); Map, Object> newMocksMap = Map.of( LoggingService.class, loggingServiceMock, @@ -63,7 +66,8 @@ public void beforeEach(final ExtensionContext context) { LspProvider.class, lspProviderMock, LoginService.class, loginServiceMock, PluginStore.class, pluginStoreMock, - CodeReferenceLoggingService.class, codeReferenceLoggingServiceMock + CodeReferenceLoggingService.class, codeReferenceLoggingServiceMock, + EventBroker.class, eventBrokerMock ); setMocksMap(newMocksMap); } diff --git a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java index 3aa3582a..ee94f131 100644 --- a/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java +++ b/plugin/tst/software/aws/toolkits/eclipse/amazonq/lsp/auth/DefaultAuthStateManagerTest.java @@ -3,41 +3,40 @@ package software.aws.toolkits.eclipse.amazonq.lsp.auth; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.Mock; -import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; import software.aws.toolkits.eclipse.amazonq.configuration.PluginStore; import software.aws.toolkits.eclipse.amazonq.exception.AmazonQPluginException; +import software.aws.toolkits.eclipse.amazonq.extensions.implementation.ActivatorStaticMockExtension; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthState; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.AuthStateType; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginIdcParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginParams; import software.aws.toolkits.eclipse.amazonq.lsp.auth.model.LoginType; -import software.aws.toolkits.eclipse.amazonq.plugin.Activator; import software.aws.toolkits.eclipse.amazonq.util.Constants; -import software.aws.toolkits.eclipse.amazonq.util.LoggingService; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.clearInvocations; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.lang.reflect.Field; class DefaultAuthStateManagerTest { @Mock private PluginStore pluginStore; - private MockedStatic mockedActivator; + + @RegisterExtension + private static ActivatorStaticMockExtension activatorStaticMockExtension = new ActivatorStaticMockExtension(); private DefaultAuthStateManager authStateManager; private LoginParams loginParams; @@ -48,8 +47,6 @@ void setUp() { closeable = MockitoAnnotations.openMocks(this); assertNotNull(pluginStore, "PluginStore mock should not be null"); - mockedActivator = mockStatic(Activator.class); - mockedActivator.when(Activator::getLogger).thenReturn(mock(LoggingService.class)); authStateManager = new DefaultAuthStateManager(pluginStore); loginParams = new LoginParams(); @@ -61,7 +58,6 @@ void setUp() { @AfterEach void tearDown() throws Exception { clearInvocations(pluginStore); - mockedActivator.close(); closeable.close(); }