When SVN authentication is failed, suggest to clear authentication cache or change configuration directory right in the error message

for IDEA-92220 "Click to fix" link doesn't work if invalid SVN credentials was entered
This commit is contained in:
irengrig
2012-10-02 15:23:08 +04:00
parent 1ed15678f5
commit d855f41b6e
5 changed files with 127 additions and 34 deletions
@@ -15,7 +15,10 @@
*/
package com.intellij.openapi.vcs.ui;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationListener;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
@@ -23,11 +26,14 @@ import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.BalloonBuilder;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.NamedRunnable;
import com.intellij.openapi.vcs.changes.ui.ChangesViewContentManager;
import com.intellij.ui.awt.RelativePoint;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import java.awt.*;
import java.util.concurrent.TimeUnit;
@@ -43,32 +49,37 @@ public class VcsBalloonProblemNotifier implements Runnable {
private final String myMessage;
private final MessageType myMessageType;
private final boolean myShowOverChangesView;
@Nullable private final NamedRunnable[] myNotificationListener;
public VcsBalloonProblemNotifier(@NotNull final Project project, @NotNull final String message, final MessageType messageType) {
this(project, message, messageType, true);
this(project, message, messageType, true, null);
}
public VcsBalloonProblemNotifier(@NotNull final Project project, @NotNull final String message, final MessageType messageType, boolean showOverChangesView) {
public VcsBalloonProblemNotifier(@NotNull final Project project, @NotNull final String message, final MessageType messageType, boolean showOverChangesView,
@Nullable final NamedRunnable[] notificationListener) {
myProject = project;
myMessage = message;
myMessageType = messageType;
myShowOverChangesView = showOverChangesView;
myNotificationListener = notificationListener;
}
public static void showOverChangesView(@NotNull final Project project, @NotNull final String message, final MessageType type) {
show(project, message, type, true);
public static void showOverChangesView(@NotNull final Project project, @NotNull final String message, final MessageType type,
final NamedRunnable... notificationListener) {
show(project, message, type, true, notificationListener);
}
public static void showOverVersionControlView(@NotNull final Project project, @NotNull final String message, final MessageType type) {
show(project, message, type, false);
show(project, message, type, false, null);
}
private static void show(final Project project, final String message, final MessageType type, final boolean showOverChangesView) {
private static void show(final Project project, final String message, final MessageType type, final boolean showOverChangesView,
@Nullable final NamedRunnable[] notificationListener) {
final Application application = ApplicationManager.getApplication();
if (application.isHeadlessEnvironment()) return;
final Runnable showErrorAction = new Runnable() {
public void run() {
new VcsBalloonProblemNotifier(project, message, type, showOverChangesView).run();
new VcsBalloonProblemNotifier(project, message, type, showOverChangesView, notificationListener).run();
}
};
if (application.isDispatchThread()) {
@@ -80,7 +91,40 @@ public class VcsBalloonProblemNotifier implements Runnable {
}
public void run() {
NOTIFICATION_GROUP.createNotification(myMessage, myMessageType).notify(myProject.isDefault() ? null : myProject);
final Notification notification;
if (myNotificationListener != null && myNotificationListener.length > 0) {
final NotificationType type = myMessageType.toNotificationType();
final StringBuilder sb = new StringBuilder(myMessage);
for (NamedRunnable runnable : myNotificationListener) {
final String name = runnable.toString();
sb.append("<br/><a href=\"").append(name).append("\">").append(name).append("</a>");
}
notification = NOTIFICATION_GROUP.createNotification(type.name(), sb.toString(), myMessageType.toNotificationType(),
new NotificationListener() {
@Override
public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) {
if (HyperlinkEvent.EventType.ACTIVATED.equals(event.getEventType())) {
if (myNotificationListener.length == 1) {
myNotificationListener[0].run();
} else {
final String description = event.getDescription();
if (description != null) {
for (NamedRunnable runnable : myNotificationListener) {
if (description.equals(runnable.toString())) {
runnable.run();
break;
}
}
}
}
notification.expire();
}
}
});
} else {
notification = NOTIFICATION_GROUP.createNotification(myMessage, myMessageType);
}
notification.notify(myProject.isDefault() ? null : myProject);
}
public static void showBalloonForComponent(@NotNull JComponent component, @NotNull final String message, final MessageType type,
@@ -25,6 +25,7 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.popup.Balloon;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.NamedRunnable;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.impl.GenericNotifierImpl;
@@ -33,6 +34,7 @@ import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.wm.IdeFrame;
import com.intellij.openapi.wm.ex.WindowManagerEx;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Consumer;
import com.intellij.util.ThreeState;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -321,14 +323,37 @@ public class SvnAuthenticationNotifier extends GenericNotifierImpl<SvnAuthentica
} catch (SVNCancelException e) {
log(e); // auth canceled
return false;
} catch (SVNException e) {
} catch (final SVNException e) {
if (e.getErrorMessage().getErrorCode().isAuthentication()) {
log(e);
return false;
}
LOG.info("some other exc", e);
if (interactive) {
VcsBalloonProblemNotifier.showOverChangesView(project, "Authentication failed: " + e.getMessage(), MessageType.ERROR);
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
VcsBalloonProblemNotifier.showOverChangesView(project, "Authentication failed: " + e.getMessage(), MessageType.ERROR,
new NamedRunnable(SvnBundle.message("confirmation.title.clear.authentication.cache")) {
@Override
public void run() {
SvnConfigurable.clearAuthenticationCache(project, null, configuration.getConfigurationDirectory());
}
},
new NamedRunnable(SvnBundle.message("action.title.select.configuration.directory")) {
@Override
public void run() {
SvnConfigurable.selectConfigirationDirectory(configuration.getConfigurationDirectory(),
new Consumer<String>() {
@Override
public void consume(String s) {
configuration.setConfigurationDirectory(s);
}
}, project, null);
}
});
}
}, ModalityState.NON_MODAL, project.getDisposed());
}
return false; /// !!!! any exception means user should be notified that authorization failed
}
@@ -154,6 +154,7 @@ status.group.name.locked=Locked
checkbox.configure.use.system.default.configuration.directory=&Use system default Subversion configuration directory
label.configuration.configuration.directory=Subversion configuration &directory:
dialog.title.select.configuration.directory=Select Configuration Directory
action.title.select.configuration.directory=Change Configuration Directory
dialog.description.select.configuration.directory=Select Subversion configuration directory or create new one
button.text.clear.authentication.cache=&Clear Auth Cache
label.text.delete.stored.credentials=Delete all stored credentials for 'http', 'svn' and 'svn+ssh' protocols
@@ -32,8 +32,11 @@ import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.ui.MultiLineTooltipUI;
import com.intellij.util.Consumer;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.config.ConfigureProxiesListener;
import javax.swing.*;
@@ -93,37 +96,21 @@ public class SvnConfigurable implements Configurable {
myClearAuthButton.addActionListener(new ActionListener(){
public void actionPerformed(final ActionEvent e) {
String path = myConfigurationDirectoryText.getText();
if (path != null) {
int result = Messages.showYesNoDialog(myComponent, SvnBundle.message("confirmation.text.delete.stored.authentication.information"),
SvnBundle.message("confirmation.title.clear.authentication.cache"),
Messages.getWarningIcon());
if (result == 0) {
SvnConfiguration.RUNTIME_AUTH_CACHE.clear();
SvnConfiguration.getInstance(myProject).clearAuthenticationDirectory(myProject);
}
}
clearAuthenticationCache(myProject, myComponent, myConfigurationDirectoryText.getText());
}
});
final FileChooserDescriptor descriptor = createFileDescriptor();
myConfigurationDirectoryText.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
@NonNls String path = myConfigurationDirectoryText.getText().trim();
path = "file://" + path.replace(File.separatorChar, '/');
VirtualFile root = VirtualFileManager.getInstance().findFileByUrl(path);
String oldValue = PropertiesComponent.getInstance().getValue("FileChooser.showHiddens");
PropertiesComponent.getInstance().setValue("FileChooser.showHiddens", Boolean.TRUE.toString());
VirtualFile file = FileChooser.chooseFile(descriptor, myComponent, myProject, root);
PropertiesComponent.getInstance().setValue("FileChooser.showHiddens", oldValue);
if (file == null) {
return;
}
myConfigurationDirectoryText.setText(file.getPath().replace('/', File.separatorChar));
selectConfigirationDirectory(path, new Consumer<String>() {
@Override
public void consume(String s) {
myConfigurationDirectoryText.setText(s);
}
}, myProject, myComponent);
}
});
myConfigurationDirectoryText.setEditable(false);
@@ -150,6 +137,42 @@ public class SvnConfigurable implements Configurable {
myJavaHLInfo.setVisible(internal);
}
public static void selectConfigirationDirectory(@NotNull String path, @NotNull final Consumer<String> dirConsumer,
final Project project, @Nullable final Component component) {
final FileChooserDescriptor descriptor = createFileDescriptor();
path = "file://" + path.replace(File.separatorChar, '/');
VirtualFile root = VirtualFileManager.getInstance().findFileByUrl(path);
String oldValue = PropertiesComponent.getInstance().getValue("FileChooser.showHiddens");
PropertiesComponent.getInstance().setValue("FileChooser.showHiddens", Boolean.TRUE.toString());
VirtualFile file = FileChooser.chooseFile(descriptor, component, project, root);
PropertiesComponent.getInstance().setValue("FileChooser.showHiddens", oldValue);
if (file == null) {
return;
}
final String resultPath = file.getPath().replace('/', File.separatorChar);
dirConsumer.consume(resultPath);
}
public static void clearAuthenticationCache(@NotNull final Project project, final Component component, final String configDirPath) {
if (configDirPath != null) {
int result;
if (component == null) {
result = Messages.showYesNoDialog(project, SvnBundle.message("confirmation.text.delete.stored.authentication.information"),
SvnBundle.message("confirmation.title.clear.authentication.cache"),
Messages.getWarningIcon());
} else {
result = Messages.showYesNoDialog(component, SvnBundle.message("confirmation.text.delete.stored.authentication.information"),
SvnBundle.message("confirmation.title.clear.authentication.cache"),
Messages.getWarningIcon());
}
if (result == 0) {
SvnConfiguration.RUNTIME_AUTH_CACHE.clear();
SvnConfiguration.getInstance(project).clearAuthenticationDirectory(project);
}
}
}
private static FileChooserDescriptor createFileDescriptor() {
final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
descriptor.setShowFileSystemRoots(true);
@@ -488,7 +488,7 @@ public class SvnConfiguration implements PersistentStateComponent<Element> {
public static class AuthStorage implements ISVNAuthenticationStorage {
private final Map<String, Object> myStorage = new Hashtable<String, Object>();
private final Map<String, Object> myStorage = Collections.synchronizedMap(new HashMap<String, Object>());
public void clear() {
myStorage.clear();