/* * Copyright 2000-2020 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.teamcity; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.Pair; import com.thoughtworks.xstream.XStreamException; import jetbrains.buildServer.*; import jetbrains.buildServer.delayedCommit.RemoteRunSessionData; import jetbrains.buildServer.messages.XStreamHolder; import jetbrains.buildServer.notification.NotificationMessage; import jetbrains.buildServer.util.CollectionsUtil; import jetbrains.buildServer.xstream.XStreamWrapper; import jetbrains.teamcity.core.Debug; import jetbrains.teamcity.core.ITeamCityIDEListener; import jetbrains.teamcity.core.ProjectStateDispatcher; import jetbrains.teamcity.core.ProjectStateDispatcher.IProjectStateChangedListener; import jetbrains.teamcity.core.Util; import jetbrains.teamcity.core.util.Converters; import jetbrains.teamcity.ui.notifications.NotificationsManager; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.SafeRunnable; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.concurrent.CopyOnWriteArrayList; public class NotificationDispatcher { private static Logger LOG = Debug.Category.DEBUG_TEAMCITY.getLogger(NotificationDispatcher.class); public static enum Scope { ALL, WORKSPACE, PREFERENCE } private final class ActiveConfigurationMappingJob extends Job { private ActiveConfigurationMappingJob() { super("Collecting Suitable Configurations for opened projects"); } @Override protected IStatus run(IProgressMonitor monitor) { try { final Collection activeTeamcityResources = Activator.getDefault().getProvider().getActiveTeamcityResources(monitor); //map to configurations final Pair, Collection> suitableConfigurations = Util.getApplicableConfigurations(activeTeamcityResources, monitor); setActiveConfigurations(new ArrayList(suitableConfigurations.getSecond())); Activator.getDefault().getIDE().setSamplePaths(activeTeamcityResources); } catch (Exception e) { return Util.createStatus(IStatus.WARNING, e); } return Status.OK_STATUS; } @Override public boolean belongsTo(Object family) { return family instanceof ActiveConfigurationMappingJob || super.belongsTo(family); } } @NotNull private Collection myActiveConfigurations = Collections.emptyList(); @NotNull private Set myActiveConfigurationsIds = Collections.emptySet(); private final Map> myListeners; private final ITeamCityIDEListener myTeamCityIDEListener; private final StateChangedListener myTeamCityStateChangedListener; private final IProjectStateChangedListener myProjectStateChangedListener; public NotificationDispatcher() { // Initialize listeners map myListeners = new EnumMap>(Scope.class); for (Scope scope : Scope.values()) { myListeners.put(scope, new CopyOnWriteArrayList()); } loadPendingNotifications(); myTeamCityIDEListener = new ITeamCityIDEListener() { /** * ITeamCityIDEListener */ public boolean isActive() { for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { if (listener.isActive()) { return true; } } } return false; } public void updateStatus() { for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { listener.updateStatus(); } }); } } } public void updateSummary(final @NotNull TeamServerSummaryProvider summaryProvider) { for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { listener.updateSummary(summaryProvider); } }); } } } /** * NotificationListener */ public void onMessageReceived(final @NotNull NotificationMessage message, final boolean silent) { final ArrayList out = new ArrayList(); if (message.isImportant() && !silent) { out.add(message); } else if (isActual(message)) { out.add(message); } else { if (NotificationsManager.isShowOnlyProjectRelated()) { //store as pending push(message); } else { out.add(message); } // load pending final Collection pending = pop(); out.addAll(pending); } dispatch(out); } public void onSystemMessage(final String messageText) { for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { listener.onSystemMessage(messageText); } }); } } } public void processChangeSucceeded(final @NotNull RemoteRunSessionData arg0, final @NotNull UserChangeInfo arg1) { // final Build[] builds = arg1.getAllInstances().toArray(new Build[0]); for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { listener.processChangeSucceeded(arg0, arg1); } }); } } } public void processChangeFailed(final @NotNull RemoteRunSessionData arg0, final @NotNull UserChangeInfo arg1) { // final Build[] builds = arg1.getAllInstances().toArray(new Build[0]); for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { listener.processChangeFailed(arg0, arg1); } }); } } } }; myTeamCityStateChangedListener = new StateChangedListener() { public void stateChanged(final @NotNull StateChangedEvent event) { if (TeamCityState.LOGGED_IN == event.getNewState()) { fireProjectMapping(); } } }; myProjectStateChangedListener = new IProjectStateChangedListener() { /** * IProjectStateChangedListener */ public void projectOpened(IProject... project) { fireProjectMapping(); } public void projectClosed(IProject... project) { fireProjectMapping(); } }; ProjectStateDispatcher.getInstance().addListener(myProjectStateChangedListener); //listen projects state Activator.getDefault().getIDE().addListener(myTeamCityIDEListener);//listen teamcity messages Activator.getDefault().getIDE().addListener(myTeamCityStateChangedListener);//listen logged state } private void loadPendingNotifications() { final IPreferenceStore store = Activator.getDefault().getPreferenceStore(); final String messages = store.getString(Constants.Preferences.NOTIFICATIONS_PENDING_MESSAGES); if (messages != null) { final Map pending = fromXMLString(messages); if (pending != null) { myPendingMessages.putAll(pending); } } } private void savePendingNotifications() { final IPreferenceStore store = Activator.getDefault().getPreferenceStore(); final String messages = toXMLString(myPendingMessages); store.setValue(Constants.Preferences.NOTIFICATIONS_PENDING_MESSAGES, messages); } public void dispose() { savePendingNotifications(); ProjectStateDispatcher.getInstance().removeListener(myProjectStateChangedListener); Activator.getDefault().getIDE().removeListener(myTeamCityStateChangedListener); Activator.getDefault().getIDE().removeListener(myTeamCityIDEListener); } public void addListener(final @NotNull ITeamCityIDEListener listener, final @NotNull Scope scope) { myListeners.get(scope).add(listener); } public void removeListener(final @Nullable ITeamCityIDEListener listener) { if (listener == null) { return; } for (final Collection listeners : myListeners.values()) { listeners.remove(listener); } } private void fireProjectMapping() { //collect all opened final TeamCityState currentState = Activator.getDefault().getSnapshot().getState(); if (TeamCityState.LOGGED_IN == currentState) { final Job mappingJob = new ActiveConfigurationMappingJob(); Util.cancelJobs(Job.getJobManager().find(mappingJob));//drop obsolete scheduled jobs mappingJob.schedule(); mappingJob.addJobChangeListener(new JobChangeAdapter() { @Override public void done(IJobChangeEvent event) { final Collection pending = pop(); dispatch(pending); } }); } else { setActiveConfigurations(Collections.emptyList()); LOG.debug("Discard Active configurations, caused by " + currentState); } } private void setActiveConfigurations(@Nullable final Collection values) { myActiveConfigurations = values != null ? Collections.unmodifiableCollection(values) : Collections.emptyList(); myActiveConfigurationsIds = CollectionsUtil.convertSet(myActiveConfigurations, Converters.BUILD_TYPE_ID); LOG.debug(String.format("Active configurations set to '%s' ", myActiveConfigurations)); } private void dispatch(@NotNull final Collection pendingMessages) { if (pendingMessages.isEmpty()) { return; } for (final Collection listeners : myListeners.values()) { for (final ITeamCityIDEListener listener : listeners) { SafeRunnable.run(new SafeRunnable() { public void run() throws Exception { for (NotificationMessage pendingMessage : pendingMessages) { listener.onMessageReceived(pendingMessage, false); } } }); } } } @NotNull private Collection pop() { if (!NotificationsManager.isShowOnlyProjectRelated()) { final Collection ret = new ArrayList(myPendingMessages.values()); myPendingMessages.clear(); return ret; } final ArrayList out = new ArrayList(); for (final BuildType cfg : myActiveConfigurations) { String key; NotificationMessage lastPendingMessage; key = cfg.getBuildTypeId(); lastPendingMessage = myPendingMessages.get(key); if (lastPendingMessage != null) { out.add(lastPendingMessage); myPendingMessages.remove(key); } // Do not remove. Saved for backward compatibility. key = cfg.getName(); lastPendingMessage = myPendingMessages.get(key); if (lastPendingMessage != null) { out.add(lastPendingMessage); myPendingMessages.remove(key); } } LOG.debug(String.format("Loaded pending notifications: %s", out)); return out; } private final HashMap myPendingMessages = new HashMap(); private void push(@NotNull final NotificationMessage message) { myPendingMessages.put(message.getBuildTypeId(), message); } private boolean isActual(@NotNull final NotificationMessage message) { return myActiveConfigurationsIds.contains(message.getBuildTypeId()); } @NotNull private static String toXMLString(@NotNull final Map map) { try { return XStreamWrapper.serializeObject(map, new XStreamHolder()); } catch (Exception e) { return ""; } } @Nullable private static Map fromXMLString(@NotNull final String xml) { try { return XStreamWrapper.deserializeObject(NotificationDispatcher.class.getClassLoader(), xml, new XStreamHolder()); } catch (XStreamException e) { return null; } } }