PageRenderTime 131ms CodeModel.GetById 30ms app.highlight 56ms RepoModel.GetById 14ms app.codeStats 1ms

/src/org/bitbucket/connectors/jetbrains/BitbucketUtil.java

https://bitbucket.org/atlassian/jetbrains-bitbucket-connector/
Java | 426 lines | 366 code | 42 blank | 18 comment | 63 complexity | aa4bc54a01ad8929995c2f75f92fe5a7 MD5 | raw file
  1package org.bitbucket.connectors.jetbrains;
  2
  3
  4import com.intellij.openapi.fileChooser.FileChooser;
  5import com.intellij.openapi.fileChooser.FileChooserDescriptor;
  6import com.intellij.openapi.progress.ProgressIndicator;
  7import com.intellij.openapi.progress.ProgressManager;
  8import com.intellij.openapi.progress.Task;
  9import com.intellij.openapi.project.Project;
 10import com.intellij.openapi.ui.Messages;
 11import com.intellij.openapi.util.Computable;
 12import com.intellij.openapi.util.IconLoader;
 13import com.intellij.openapi.util.Ref;
 14import com.intellij.openapi.util.text.StringUtil;
 15import com.intellij.openapi.vfs.VfsUtil;
 16import com.intellij.openapi.vfs.VirtualFile;
 17import com.intellij.openapi.vfs.VirtualFileManager;
 18import com.intellij.util.SystemProperties;
 19import com.intellij.util.net.HttpConfigurable;
 20import org.apache.commons.httpclient.*;
 21import org.apache.commons.httpclient.auth.AuthScope;
 22import org.apache.commons.httpclient.methods.GetMethod;
 23import org.apache.commons.httpclient.methods.PostMethod;
 24import org.bitbucket.connectors.jetbrains.ui.BitbucketBundle;
 25import org.bitbucket.connectors.jetbrains.ui.BitbucketLoginDialog;
 26import org.bitbucket.connectors.jetbrains.ui.HtmlMessageDialog;
 27import org.bitbucket.connectors.jetbrains.vcs.VcsHandler;
 28import org.jdom.Element;
 29import org.jdom.JDOMException;
 30import org.jdom.input.SAXBuilder;
 31import org.jetbrains.annotations.NotNull;
 32import org.jetbrains.annotations.Nullable;
 33
 34import javax.swing.*;
 35import java.awt.*;
 36import java.io.IOException;
 37import java.io.StringReader;
 38import java.util.*;
 39import java.util.List;
 40
 41public class BitbucketUtil {
 42    public static final String BITBUCKET = "Bitbucket";
 43    public static final String BITBUCKET_DN = "bitbucket.org";
 44    public static final String API_URL_BASE = "https://api." + BITBUCKET_DN + "/1.0";
 45    public static final Icon ICON = IconLoader.getIcon("res/bitbucket.png", BitbucketUtil.class);
 46
 47    private static HttpClient getClient(String login, String password) {
 48        HttpClient client = new HttpClient();
 49        UsernamePasswordCredentials cred = new UsernamePasswordCredentials(login, password);
 50        client.getParams().setAuthenticationPreemptive(true);
 51        client.getState().setCredentials(AuthScope.ANY, cred);
 52
 53        HttpConfigurable proxyConfig = HttpConfigurable.getInstance();
 54        if (proxyConfig.USE_HTTP_PROXY) {
 55            client.getHostConfiguration().setProxy(proxyConfig.PROXY_HOST, proxyConfig.PROXY_PORT);
 56            if (proxyConfig.PROXY_AUTHENTICATION) {
 57                UsernamePasswordCredentials proxyCred = new UsernamePasswordCredentials(proxyConfig.PROXY_LOGIN, proxyConfig.getPlainProxyPassword());
 58                client.getState().setProxyCredentials(AuthScope.ANY, proxyCred);
 59            }
 60        }
 61
 62        return client;
 63    }
 64
 65    public static Element request(String username, String password, String url, boolean post, @Nullable Map<String, String> params) throws IOException, JDOMException {
 66        return request(username, password, url, post, params, null);
 67    }
 68
 69
 70    public static Element request(String username, String password, String url, boolean post, @Nullable Map<String, String> params, @Nullable String query) throws IOException, JDOMException {
 71        url = "https://api." + BITBUCKET_DN + "/1.0" + url + "?format=xml";
 72
 73        if (query != null)
 74            url = url + "&" + query;
 75
 76        HttpClient client = getClient(username, password);
 77        HttpMethod res;
 78        if (post) {
 79            res = new PostMethod(url);
 80            if (params != null) {
 81                List<NameValuePair> pairs = new ArrayList<NameValuePair>();
 82                for (Map.Entry<String, String> entry: params.entrySet()) {
 83                    pairs.add(new NameValuePair(entry.getKey(), entry.getValue()));
 84                }
 85                NameValuePair[] arr = new NameValuePair[pairs.size()];
 86                pairs.toArray(arr);
 87                ((PostMethod) res).setRequestBody(arr);
 88            }
 89        } else {
 90            res = new GetMethod(url);
 91        }
 92        client.executeMethod(res);
 93        String s = res.getResponseBodyAsString();
 94        return new SAXBuilder(false).build(new StringReader(s)).getRootElement();
 95    }
 96
 97    /**
 98     * Returns list of repositories for logged user, or null if the login is cancelled.
 99     *
100     * @param project project
101     * @param ownOnly return own repositories only if true
102     * @return list of repositories, or null on cancel
103     */
104    public static List<RepositoryInfo> getRepositories(Project project, final boolean ownOnly) {
105        final BitbucketSettings settings = BitbucketSettings.getInstance();
106        boolean logged;
107        try {
108            logged = executeWithProgressSynchronously(project, new Computable<Boolean>() {
109                public Boolean compute() {
110                    ProgressManager.getInstance().getProgressIndicator().setText(BitbucketBundle.message("logging-bitbucket"));
111                    return testConnection(settings.getLogin(), settings.getPassword());
112                }
113            });
114        } catch (CancelledException e) {
115            return null;
116        }
117
118        if (!logged) {
119            BitbucketLoginDialog dialog = new BitbucketLoginDialog(project);
120            dialog.show();
121            if (!dialog.isOK()) {
122                return null;
123            }
124        }
125
126        try {
127            return executeWithProgressSynchronously(project, new Computable<List<RepositoryInfo>>() {
128                public List<RepositoryInfo> compute() {
129                    ProgressManager.getInstance().getProgressIndicator().setText(BitbucketBundle.message("getting-repositories-list"));
130                    return getRepositories(settings.getLogin(), settings.getPassword(), ownOnly);
131                }
132            });
133        } catch (CancelledException e) {
134            return null;
135        }
136    }
137
138    private static List<RepositoryInfo> getRepositories(String username, String password, boolean ownOnly) {
139        List<RepositoryInfo> repositories = new ArrayList<RepositoryInfo>();
140        try {
141            Element element = request(username, password, "/user/repositories/", false, null);
142            for (Element res : (List<Element>) element.getChildren("resource")) {
143                RepositoryInfo info = new RepositoryInfo(res);
144                if (!ownOnly || info.getOwner().equals(username)) {
145                    repositories.add(info);
146                }
147            }
148            return repositories;
149        } catch (Exception e) {
150            // ignore
151        }
152        return repositories;
153    }
154
155    public static class CancelledException extends RuntimeException {
156    }
157
158    public static boolean checkCredentials(Project project, final String login, final String password) {
159        if (login == null && password == null && areCredentialsEmpty()) {
160            return false;
161        }
162        try {
163            return executeWithProgressSynchronously(project, new Computable<Boolean>() {
164                public Boolean compute() {
165                    ProgressManager.getInstance().getProgressIndicator().setText(BitbucketBundle.message("trying-login-bibucket"));
166                    if (login != null && password != null) {
167                        return testConnection(login, password);
168                    }
169                    BitbucketSettings settings = BitbucketSettings.getInstance();
170                    return testConnection(settings.getLogin(), settings.getPassword());
171                }
172            });
173        }
174        catch (CancelledException e) {
175            return false;
176        }
177
178    }
179
180    public static boolean areCredentialsEmpty() {
181        BitbucketSettings settings = BitbucketSettings.getInstance();
182        return StringUtil.isEmptyOrSpaces(settings.getLogin()) || StringUtil.isEmptyOrSpaces(settings.getPassword());
183    }
184
185    public static boolean testConnection(final String login, final String password) {
186        try {
187            Element element = request(login, password, "/emails/", false, null);
188            List<Element> children = element.getChildren();
189            return children.size() > 0 && children.get(0).getChildren().size() > 0;
190        }
191        catch (Exception e) {
192            // Ignore
193        }
194        return false;
195    }
196
197    public static boolean sshEnabled(final Project project, final String login, final String password) {
198        return executeWithProgressSynchronously(project, new Computable<Boolean>() {
199            public Boolean compute() {
200                return sshEnabled(login, password);
201            }
202        });
203    }
204
205    private static boolean sshEnabled(final String login, final String password) {
206        try {
207            Element element = request(login, password, "/ssh-keys/", false, null);
208            List<Element> children = element.getChildren();
209            return children.size() > 0 && children.get(0).getChild("key") != null;
210        } catch (Exception e) {
211            return false;
212        }
213    }
214
215    /**
216     * Shows SSH key selection dialog and uploads the selected file as Bitbucket key
217     *
218     * @param project
219     * @param login
220     * @param password
221     *
222     * @return true if uploaded successfully, false on upload error, null on cancel
223     */
224    public static Boolean addSshKey(final Project project, final Component parentComponent, final String login, final String password) {
225        FileChooserDescriptor descriptor = new FileChooserDescriptor(true, false, false, false, false, false) {
226            @Override
227            public boolean isFileVisible(VirtualFile file, boolean showHiddenFiles) {
228                return file.isDirectory() || super.isFileVisible(file, showHiddenFiles);
229            }
230        };
231        descriptor.setIsTreeRootVisible(true);
232        String home = SystemProperties.getUserHome();
233        VirtualFile root = null;
234        if (home != null) {
235            root = VirtualFileManager.getInstance().findFileByUrl("file://" + home);
236            if (root != null) {
237                VirtualFile ssh = root.findChild(".ssh"); // id_dsa.pub
238                if (ssh == null) {
239                    ssh = root.findChild("Application Data/SSH/UserKeys");
240                }
241                if (ssh != null) {
242                    root = ssh;
243                }
244            }
245        }
246        descriptor.setTitle(BitbucketBundle.message("ssh-key-dialog-title"));
247        descriptor.setDescription(BitbucketBundle.message("ssh-key-dialog-desc"));
248
249        VirtualFile[] files = FileChooser.chooseFiles(descriptor, parentComponent, null, root);
250        if (files.length == 0) {
251            return null;
252        }
253        VirtualFile f = files[0];
254
255        boolean result = false;
256        try {
257            final String key = VfsUtil.loadText(f);
258            result = executeWithProgressSynchronously(project, new Computable<Boolean>() {
259                public Boolean compute() {
260                    return addSshKey(login, password, key);
261                }
262            });
263        } catch (IOException e1) {
264            Messages.showErrorDialog("Can't read SSH key: " + f.getPath(), "SSH key");
265        }
266        return result;
267    }
268
269    private static boolean addSshKey(final String login, final String password, final String key) {
270        try {
271            Element element = request(login, password, "/ssh-keys/", true, Collections.singletonMap("key", key));
272            return element.getChildren("key").size() > 0;
273        } catch (Exception e) {
274            return false;
275        }
276    }
277
278    public static <T> T executeWithProgressSynchronously(final Project project, final Computable<T> computable) throws CancelledException {
279        final Ref<T> result = new Ref<T>();
280        ProgressManager.getInstance().run(new Task.Modal(project, BitbucketBundle.message("access-bitbucket"), true) {
281            public void run(@NotNull ProgressIndicator indicator) {
282                indicator.setIndeterminate(true);
283                result.set(computable.compute());
284            }
285
286            @Override
287            public void onCancel() {
288                throw new CancelledException();
289            }
290        });
291        return result.get();
292    }
293
294    public static void executeWithProgressSynchronously(final Project project, String title, final Runnable runnable) throws CancelledException {
295        ProgressManager.getInstance().run(new Task.Modal(project, title, true) {
296            public void run(@NotNull ProgressIndicator indicator) {
297                indicator.setIndeterminate(true);
298                runnable.run();
299            }
300
301            @Override
302            public void onCancel() {
303                throw new CancelledException();
304            }
305        });
306    }
307
308    public static void share(final Project project, final VirtualFile root, final String name, final String description, final boolean ssh, final boolean git, final VcsHandler vcsHandler) {
309        final RepositoryInfo[] repo = new RepositoryInfo[1];
310        final String title = BitbucketBundle.message("push-bitbucket", name);
311        executeWithProgressSynchronously(project, title, new Runnable() {
312            public void run() {
313                BitbucketSettings settings = BitbucketSettings.getInstance();
314                RepositoryInfo repository = createBitbucketRepository(settings.getLogin(), settings.getPassword(), name, description, true, git);
315                if (repository != null && repository.isCreating()) {
316                    if (!waitRepositoryAvailable(settings, repository.getSlug())) {
317                        return;
318                    }
319                }
320                repo[0] = repository;
321            }
322        });
323        final RepositoryInfo repository = repo[0];
324        if (repository != null) {
325            try {
326                final String repositoryUrl = repository.getCheckoutUrl(ssh, true);
327                vcsHandler.setRepositoryDefaultUrl(project, root, repository.getCheckoutUrl(ssh, false));
328
329                new Task.Backgroundable(project, title, true) {
330                    public void run(@NotNull ProgressIndicator indicator) {
331                        if (!vcsHandler.push(project, root, repositoryUrl)) {
332                            return;
333                        }
334
335                        SwingUtilities.invokeLater(new Runnable() {
336                            public void run() {
337                                showPushedDialog(project, name, repository);
338                            }
339                        });
340                    }
341                }.queue();
342            } catch (Exception e) {
343                throw new RuntimeException(e);
344            }
345        } else {
346            Messages.showErrorDialog(project, BitbucketBundle.message("push-err"), BitbucketBundle.message("share-project-on-bitbucket"));
347        }
348    }
349
350    private static void showPushedDialog(Project project, String name, RepositoryInfo repository) {
351        try {
352            HtmlMessageDialog dialog = new HtmlMessageDialog(project, BitbucketBundle.message("project-shared", name, repository.getCheckoutUrl()), BitbucketBundle.message("share-project-on-bitbucket"));
353            dialog.show();
354        } catch (URIException e) {
355            Messages.showErrorDialog(project, e.getMessage(), BitbucketBundle.message("url-encode-err"));
356        }
357    }
358
359    private static boolean waitRepositoryAvailable(BitbucketSettings settings, String name) {
360        RepositoryInfo r;
361        do {
362            try {
363                Thread.sleep(3000);
364                r = getRepository(settings.getLogin(), settings.getPassword(), name);
365                if (r == null) {
366                    return false;
367                }
368            } catch (InterruptedException e) {
369                return false;
370            }
371        } while (r.isCreating());
372        return true;
373    }
374
375    private static RepositoryInfo createBitbucketRepository(String login, String password, String name, String description, boolean isPrivate, boolean git) {
376        Map<String, String> params = new HashMap<String, String>();
377        params.put("name", name);
378        params.put("description", description);
379        if (isPrivate) {
380            params.put("is_private", "True");
381        }
382        params.put("scm", git ? "git" : "hg");
383
384        try {
385            Element element = request(login, password, "/repositories/", true, params);
386            return new RepositoryInfo(element);
387        } catch (Exception e) {
388            return null;
389        }
390    }
391
392    public static RepositoryInfo getRepository(String login, String password, String name) {
393        try {
394            Element element = request(login, password, "/repositories/" + login + "/" + name + "/", false, null);
395            return new RepositoryInfo(element);
396        } catch (Exception e) {
397            return null;
398        }
399    }
400
401    public static boolean isSshUrl(String url) {
402        return url != null && url.startsWith("ssh://");
403    }
404
405    public static boolean isHttpUrl(String url) {
406        return url != null && (url.startsWith("http://") || url.startsWith("https://"));
407    }
408
409    public static boolean enableSsh(Project project, Component parent) {
410        BitbucketSettings instance = BitbucketSettings.getInstance();
411        if (!BitbucketUtil.sshEnabled(project, instance.getLogin(), instance.getPassword())) {
412            if (Messages.showOkCancelDialog(project, BitbucketBundle.message("ssh-key-required"), BitbucketBundle.message("ssh-key-title"), null) == Messages.OK) {
413                Boolean res = BitbucketUtil.addSshKey(project, parent, instance.getLogin(), instance.getPassword());
414                if (Boolean.FALSE.equals(res)) {
415                    Messages.showErrorDialog(project, BitbucketBundle.message("ssh-key-invalid"), BitbucketBundle.message("ssh-key-title"));
416                    return false;
417                } else if (res == null) {
418                    return false;
419                }
420            } else {
421                return false;
422            }
423        }
424        return true;
425    }
426}