diff --git a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.form b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.form index 46fab8e0e386..9d488f181bf2 100644 --- a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.form +++ b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.form @@ -72,7 +72,7 @@ - + diff --git a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.java b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.java index 8ac75b75c656..c18310ddd6fb 100644 --- a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.java +++ b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateConfigurable.java @@ -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 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()); - myCertificatesList.getEmptyText().setText("No certificates"); - myCertificatesList.setCellRenderer(new ListCellRendererWrapper() { - @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 model = getListModel(); - // In JDK 1.7 getSelectedValuesList can be used instead - List selected = new ArrayList(); - 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 getListModel() { - //noinspection unchecked - return (CollectionListModel)myCertificatesList.getModel(); - } - - private List 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(myTrustManager.getCertificates())); } @Override public void apply() throws ConfigurationException { List existing = myTrustManager.getCertificates(); - Set added = new HashSet(getCertificates()); + Set added = new HashSet(myCertificates); added.removeAll(existing); Set removed = new HashSet(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 old = new HashSet(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(myTrustManager.getCertificates())); + List original = myTrustManager.getCertificates(); + myTreeBuilder = new CertificateTreeBuilder(myTree, original); + myCertificates = new HashSet(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 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 model = getListModel(); - if (model.getElementIndex(certificate) >= 0) { - model.remove(certificate); + if (myCertificates.contains(certificate)) { + myCertificates.remove(certificate); + myTreeBuilder.removeCertificate(certificate); } } }); diff --git a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateTreeBuilder.java b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateTreeBuilder.java new file mode 100644 index 000000000000..bf952d7097e5 --- /dev/null +++ b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateTreeBuilder.java @@ -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 myCertificates = new MultiMap(); + + public CertificateTreeBuilder(@NotNull Tree tree, @NotNull Collection certificates) { + for (X509Certificate certificate : certificates) { + addCertificate(certificate); + } + init(tree, new DefaultTreeModel(new DefaultMutableTreeNode()), new MyTreeStructure(), new Comparator() { + @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 getCertificates() { + return ContainerUtil.map(myCertificates.values(), new Function() { + @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 getSelectedCertificates() { + Set selected = new HashSet(); + 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 certificates = getSelectedCertificates(); + return certificates.isEmpty() ? null : certificates.iterator().next(); + } + + @NotNull + public List getCertificatesByOrganization(@NotNull String organizationName) { + Collection wrappers = myCertificates.get(organizationName); + return extract(wrappers); + } + + private static List extract(Collection wrappers) { + return ContainerUtil.map(wrappers, new Function() { + @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 extends PresentableNodeDescriptor { + 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 { + public static final Object ROOT = new Object(); + + private RootDescriptor() { + super(null, ROOT); + } + + @Override + protected void update(PresentationData presentation) { + presentation.addText("", SimpleTextAttributes.REGULAR_ATTRIBUTES); + } + } + + static class OrganizationDescriptor extends MyNodeDescriptor { + 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 { + 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); + } + } +} diff --git a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateWrapper.java b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateWrapper.java index f6ae07f45bff..8c55767f930c 100644 --- a/platform/platform-api/src/com/intellij/util/net/ssl/CertificateWrapper.java +++ b/platform/platform-api/src/com/intellij/util/net/ssl/CertificateWrapper.java @@ -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. diff --git a/platform/platform-api/src/com/intellij/util/net/ssl/ConfirmingTrustManager.java b/platform/platform-api/src/com/intellij/util/net/ssl/ConfirmingTrustManager.java index 8d430ebe7c7b..05c1f1e78198 100644 --- a/platform/platform-api/src/com/intellij/util/net/ssl/ConfirmingTrustManager.java +++ b/platform/platform-api/src/com/intellij/util/net/ssl/ConfirmingTrustManager.java @@ -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 {