Accepted certificates are shown in tree, grouped by organization name, like in browsers. Not valid certificates look strikeout.

This commit is contained in:
Mikhail Golubev
2014-02-05 20:50:03 +04:00
parent 58da780741
commit 47d2598716
5 changed files with 319 additions and 82 deletions
@@ -72,7 +72,7 @@
<properties>
<firstComponent value="4918"/>
<orientation value="true"/>
<proportion value="0.2"/>
<proportion value="0.3"/>
<secondComponent value="9a985"/>
<showDividerControls value="false"/>
</properties>
@@ -7,10 +7,12 @@ import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.options.SearchableConfigurable;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.*;
import com.intellij.ui.AnActionButton;
import com.intellij.ui.AnActionButtonRunnable;
import com.intellij.ui.ToolbarDecorator;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.Consumer;
import com.intellij.util.containers.HashSet;
import com.intellij.util.ui.UIUtil;
@@ -19,11 +21,11 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@@ -42,9 +44,12 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
private JPanel myCertificatesListPanel;
private JPanel myDetailsPanel;
private JBList myCertificatesList;
private MutableTrustManager myTrustManager;
private Tree myTree = new Tree();
private CertificateTreeBuilder myTreeBuilder;
private Set<X509Certificate> myCertificates;
public CertificateConfigurable() {
// not fully functional by now
@@ -55,20 +60,12 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
// show newly added certificates
myTrustManager.addListener(this);
myCertificatesList = new JBList(new CollectionListModel<X509Certificate>());
myCertificatesList.getEmptyText().setText("No certificates");
myCertificatesList.setCellRenderer(new ListCellRendererWrapper<X509Certificate>() {
@Override
public void customize(JList list, X509Certificate certificate, int index, boolean selected, boolean hasFocus) {
if (!new CertificateWrapper(certificate).isValid()) {
setForeground(UIUtil.getLabelDisabledForeground());
}
setText(getCommonName(certificate));
}
});
myTree.getEmptyText().setText("No certificates");
myTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
myTree.setRootVisible(false);
//myTree.setShowsRootHandles(false);
ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myCertificatesList).disableUpDownActions();
decorator.setVisibleRowCount(5);
ToolbarDecorator decorator = ToolbarDecorator.createDecorator(myTree).disableUpDownActions();
decorator.setAddAction(new AnActionButtonRunnable() {
@Override
public void run(AnActionButton button) {
@@ -81,13 +78,14 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
if (certificate == null) {
Messages.showErrorDialog(myRootPanel, "Malformed X509 server certificate", "Not Imported");
}
else if (getCertificates().contains(certificate)) {
else if (myCertificates.contains(certificate)) {
Messages.showWarningDialog(myRootPanel, "Certificate already exists", "Not Imported");
}
else {
getListModel().add(certificate);
myCertificates.add(certificate);
myTreeBuilder.addCertificate(certificate);
addCertificatePanel(certificate);
myCertificatesList.setSelectedValue(certificate, true);
myTreeBuilder.selectCertificate(certificate);
}
}
});
@@ -96,28 +94,24 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
@Override
public void run(AnActionButton button) {
// allow to delete several certificates at once
CollectionListModel<X509Certificate> model = getListModel();
// In JDK 1.7 getSelectedValuesList can be used instead
List<X509Certificate> selected = new ArrayList<X509Certificate>();
for (int i : myCertificatesList.getSelectedIndices()) {
selected.add((X509Certificate)myCertificatesList.getModel().getElementAt(i));
for (X509Certificate certificate : myTreeBuilder.getSelectedCertificates()) {
myCertificates.remove(certificate);
myTreeBuilder.removeCertificate(certificate);
}
for (X509Certificate certificate : selected) {
model.remove(certificate);
}
if (getListModel().getSize() > 0) {
myCertificatesList.setSelectedIndex(0);
} else {
if (myCertificates.isEmpty()) {
myDetailsPanel.removeAll();
myDetailsPanel.repaint();
}
else {
myTreeBuilder.selectFirstCertificate();
}
}
});
myCertificatesList.addListSelectionListener(new ListSelectionListener() {
myTree.addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(ListSelectionEvent e) {
X509Certificate certificate = getSelectedCertificate();
public void valueChanged(TreeSelectionEvent e) {
X509Certificate certificate = myTreeBuilder.getFirstSelectedCertificate();
if (certificate != null) {
String uniqueName = getCardName(certificate);
((CardLayout)myDetailsPanel.getLayout()).show(myDetailsPanel, uniqueName);
@@ -140,19 +134,6 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
return certificate.getSubjectX500Principal().getName();
}
private CollectionListModel<X509Certificate> getListModel() {
//noinspection unchecked
return (CollectionListModel<X509Certificate>)myCertificatesList.getModel();
}
private List<X509Certificate> getCertificates() {
return getListModel().getItems();
}
private X509Certificate getSelectedCertificate() {
return (X509Certificate)myCertificatesList.getSelectedValue();
}
@NotNull
@Override
public String getId() {
@@ -188,18 +169,18 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
CertificatesManager.Config state = CertificatesManager.getInstance().getState();
return myCheckHostname.isSelected() != state.checkHostname ||
myCheckValidityPeriod.isSelected() != state.checkValidity ||
!getCertificates().equals(myTrustManager.getCertificates());
!myCertificates.equals(new HashSet<X509Certificate>(myTrustManager.getCertificates()));
}
@Override
public void apply() throws ConfigurationException {
List<X509Certificate> existing = myTrustManager.getCertificates();
Set<X509Certificate> added = new HashSet<X509Certificate>(getCertificates());
Set<X509Certificate> added = new HashSet<X509Certificate>(myCertificates);
added.removeAll(existing);
Set<X509Certificate> removed = new HashSet<X509Certificate>(existing);
removed.removeAll(getCertificates());
removed.removeAll(myCertificates);
for (X509Certificate certificate : added) {
if (!myTrustManager.addCertificate(certificate)) {
@@ -212,22 +193,6 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
throw new ConfigurationException("Cannot remove certificate for " + getCommonName(certificate), "Cannot Remove Certificate");
}
}
//Set<X509Certificate> old = new HashSet<X509Certificate>(existing);
//for (X509Certificate certificate : getListModel().getItems()) {
// if (old.contains(certificate)) {
// old.remove(certificate);
// }
// else if (!myTrustManager.addCertificate(certificate)) {
// throw new ConfigurationException("Cannot add certificate", "Cannot Add Certificate");
// }
//}
//// all that remains were removed
//for (X509Certificate certificate : old) {
// if (!myTrustManager.removeCertificate(certificate)) {
// throw new ConfigurationException("Cannot remove certificate", "Cannot Remove Certificate");
// }
//}
CertificatesManager.Config state = CertificatesManager.getInstance().getState();
state.checkHostname = myCheckHostname.isSelected();
state.checkValidity = myCheckValidityPeriod.isSelected();
@@ -235,15 +200,14 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
@Override
public void reset() {
//noinspection unchecked
myCertificatesList.setModel(new CollectionListModel<X509Certificate>(myTrustManager.getCertificates()));
List<X509Certificate> original = myTrustManager.getCertificates();
myTreeBuilder = new CertificateTreeBuilder(myTree, original);
myCertificates = new HashSet<X509Certificate>(original);
// fill lower panel with cards
for (X509Certificate certificate : getCertificates()) {
for (X509Certificate certificate : original) {
addCertificatePanel(certificate);
}
if (!getCertificates().isEmpty()) {
myCertificatesList.setSelectedIndex(0);
}
CertificatesManager.Config state = CertificatesManager.getInstance().getState();
myCheckHostname.setSelected(state.checkHostname);
myCheckValidityPeriod.setSelected(state.checkValidity);
@@ -251,7 +215,7 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
@Override
public void disposeUIResources() {
// do nothing
myTrustManager.removeListener(this);
}
@Override
@@ -259,9 +223,9 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
UIUtil.invokeLaterIfNeeded(new Runnable() {
@Override
public void run() {
CollectionListModel<X509Certificate> model = getListModel();
if (model.getElementIndex(certificate) < 0) {
model.add(certificate);
if (!myCertificates.contains(certificate)) {
myCertificates.add(certificate);
myTreeBuilder.addCertificate(certificate);
}
addCertificatePanel(certificate);
}
@@ -272,9 +236,9 @@ public class CertificateConfigurable implements SearchableConfigurable, Configur
public void certificateRemoved(final X509Certificate certificate) {
UIUtil.invokeLaterIfNeeded(new Runnable() {
public void run() {
CollectionListModel<X509Certificate> model = getListModel();
if (model.getElementIndex(certificate) >= 0) {
model.remove(certificate);
if (myCertificates.contains(certificate)) {
myCertificates.remove(certificate);
myTreeBuilder.removeCertificate(certificate);
}
}
});
@@ -0,0 +1,260 @@
package com.intellij.util.net.ssl;
import com.intellij.ide.projectView.PresentationData;
import com.intellij.ide.util.treeView.AbstractTreeBuilder;
import com.intellij.ide.util.treeView.AbstractTreeStructure;
import com.intellij.ide.util.treeView.NodeDescriptor;
import com.intellij.ide.util.treeView.PresentableNodeDescriptor;
import com.intellij.ui.SimpleTextAttributes;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import static com.intellij.util.net.ssl.CertificateWrapper.CommonField.COMMON_NAME;
import static com.intellij.util.net.ssl.CertificateWrapper.CommonField.ORGANIZATION;
/**
* @author Mikhail Golubev
*/
public class CertificateTreeBuilder extends AbstractTreeBuilder {
private static final SimpleTextAttributes STRIKEOUT_ATTRIBUTES = new SimpleTextAttributes(SimpleTextAttributes.STYLE_STRIKEOUT, null);
private static final RootDescriptor ROOT_DESCRIPTOR = new RootDescriptor();
private final MultiMap<String, CertificateWrapper> myCertificates = new MultiMap<String, CertificateWrapper>();
public CertificateTreeBuilder(@NotNull Tree tree, @NotNull Collection<X509Certificate> certificates) {
for (X509Certificate certificate : certificates) {
addCertificate(certificate);
}
init(tree, new DefaultTreeModel(new DefaultMutableTreeNode()), new MyTreeStructure(), new Comparator<NodeDescriptor>() {
@Override
public int compare(NodeDescriptor o1, NodeDescriptor o2) {
if (o1 instanceof OrganizationDescriptor && o2 instanceof OrganizationDescriptor) {
return ((String)o1.getElement()).compareTo((String)o2.getElement());
}
else if (o1 instanceof CertificateDescriptor && o2 instanceof CertificateDescriptor) {
String cn1 = ((CertificateDescriptor)o1).getElement().getSubjectField(COMMON_NAME);
String cn2 = ((CertificateDescriptor)o2).getElement().getSubjectField(COMMON_NAME);
return cn1.compareTo(cn2);
}
return 0;
}
}, true);
initRootNode();
}
public void addCertificate(@NotNull X509Certificate certificate) {
CertificateWrapper wrapper = new CertificateWrapper(certificate);
myCertificates.putValue(wrapper.getSubjectField(ORGANIZATION), wrapper);
queueUpdateFrom(RootDescriptor.ROOT, true);
}
/**
* Remove specified certificate and corresponding organization, if after removal it contains no certificates.
*/
public void removeCertificate(@NotNull X509Certificate certificate) {
CertificateWrapper wrapper = new CertificateWrapper(certificate);
myCertificates.remove(wrapper.getSubjectField(ORGANIZATION), wrapper);
queueUpdateFrom(RootDescriptor.ROOT, true);
}
public List<X509Certificate> getCertificates() {
return ContainerUtil.map(myCertificates.values(), new Function<CertificateWrapper, X509Certificate>() {
@Override
public X509Certificate fun(CertificateWrapper wrapper) {
return wrapper.getCertificate();
}
});
}
public boolean isEmpty() {
return myCertificates.isEmpty();
}
public void selectCertificate(@NotNull X509Certificate certificate) {
select(new CertificateWrapper(certificate));
}
public void selectFirstCertificate() {
if (!isEmpty()) {
Tree tree = (Tree)getTree();
TreePath path = TreeUtil.getFirstLeafNodePath(tree);
tree.addSelectionPath(path);
}
}
/**
* Returns certificates selected in the tree. If organization node is selected, all its certificates
* will be returned.
*
* @return - selected certificates
*/
@NotNull
public Set<X509Certificate> getSelectedCertificates() {
Set<X509Certificate> selected = new HashSet<X509Certificate>();
for (Object o : getSelectedElements()) {
if (o instanceof CertificateWrapper) {
selected.add(((CertificateWrapper)o).getCertificate());
}
else if (o instanceof String) {
selected.addAll(getCertificatesByOrganization((String)o));
}
}
return selected;
}
@Nullable
public X509Certificate getFirstSelectedCertificate() {
Set<X509Certificate> certificates = getSelectedCertificates();
return certificates.isEmpty() ? null : certificates.iterator().next();
}
@NotNull
public List<X509Certificate> getCertificatesByOrganization(@NotNull String organizationName) {
Collection<CertificateWrapper> wrappers = myCertificates.get(organizationName);
return extract(wrappers);
}
private static List<X509Certificate> extract(Collection<CertificateWrapper> wrappers) {
return ContainerUtil.map(wrappers, new Function<CertificateWrapper, X509Certificate>() {
@Override
public X509Certificate fun(CertificateWrapper wrapper) {
return wrapper.getCertificate();
}
});
}
@Override
protected Object transformElement(Object object) {
if (object instanceof CertificateWrapper) {
return ((CertificateWrapper)object).getCertificate();
}
return object;
}
@Override
protected boolean isAutoExpandNode(NodeDescriptor nodeDescriptor) {
return super.isAutoExpandNode(nodeDescriptor) || nodeDescriptor instanceof OrganizationDescriptor;
}
class MyTreeStructure extends AbstractTreeStructure {
@Override
public Object getRootElement() {
return RootDescriptor.ROOT;
}
@Override
public Object[] getChildElements(Object element) {
if (element == RootDescriptor.ROOT) {
return ArrayUtil.toStringArray(myCertificates.keySet());
}
else if (element instanceof String) {
return ArrayUtil.toObjectArray(myCertificates.get((String)element));
}
return ArrayUtil.EMPTY_OBJECT_ARRAY;
}
@Nullable
@Override
public Object getParentElement(Object element) {
if (element == RootDescriptor.ROOT) {
return null;
}
else if (element instanceof String) {
return RootDescriptor.ROOT;
}
return ((CertificateWrapper)element).getSubjectField(ORGANIZATION);
}
@NotNull
@Override
public NodeDescriptor createDescriptor(Object element, NodeDescriptor parentDescriptor) {
if (element == RootDescriptor.ROOT) {
return ROOT_DESCRIPTOR;
}
else if (element instanceof String) {
return new OrganizationDescriptor(parentDescriptor, (String)element);
}
return new CertificateDescriptor(parentDescriptor, (CertificateWrapper)element);
}
@Override
public void commit() {
// do nothing
}
@Override
public boolean hasSomethingToCommit() {
return false;
}
}
// Auxiliary node descriptors
static abstract class MyNodeDescriptor<T> extends PresentableNodeDescriptor<T> {
private final T myObject;
MyNodeDescriptor(@Nullable NodeDescriptor parentDescriptor, @NotNull T object) {
super(null, parentDescriptor);
myObject = object;
}
@Override
public T getElement() {
return myObject;
}
}
static class RootDescriptor extends MyNodeDescriptor<Object> {
public static final Object ROOT = new Object();
private RootDescriptor() {
super(null, ROOT);
}
@Override
protected void update(PresentationData presentation) {
presentation.addText("<root>", SimpleTextAttributes.REGULAR_ATTRIBUTES);
}
}
static class OrganizationDescriptor extends MyNodeDescriptor<String> {
private OrganizationDescriptor(@Nullable NodeDescriptor parentDescriptor, @NotNull String object) {
super(parentDescriptor, object);
}
@Override
protected void update(PresentationData presentation) {
presentation.addText(getElement(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES);
}
}
static class CertificateDescriptor extends MyNodeDescriptor<CertificateWrapper> {
private CertificateDescriptor(@Nullable NodeDescriptor parentDescriptor, @NotNull CertificateWrapper object) {
super(parentDescriptor, object);
}
@Override
protected void update(PresentationData presentation) {
CertificateWrapper wrapper = getElement();
SimpleTextAttributes attr = wrapper.isValid() ? SimpleTextAttributes.REGULAR_ATTRIBUTES : STRIKEOUT_ATTRIBUTES;
presentation.addText(wrapper.getSubjectField(COMMON_NAME), attr);
}
}
}
@@ -161,6 +161,15 @@ public class CertificateWrapper {
return Collections.unmodifiableMap(fields);
}
@Override
public final boolean equals(Object other) {
return other instanceof CertificateWrapper && myCertificate.equals(((CertificateWrapper)other).getCertificate());
}
@Override
public final int hashCode() {
return myCertificate.hashCode();
}
/**
* Find out full list of names from specification.
@@ -384,6 +384,10 @@ public class ConfirmingTrustManager extends ClientOnlyTrustManager {
myDispatcher.addListener(listener);
}
public void removeListener(@NotNull CertificateListener listener) {
myDispatcher.removeListener(listener);
}
// Guarded by caller's lock
private boolean keyStoreIsEmpty() {
try {