Initial EGit contribution to eclipse.org
[egit.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / components / RepositorySelectionPage.java
blobaaeb6aabba57a0b25a9843fbce926414b5605b52
1 /*******************************************************************************
2 * Copyright (C) 2007, 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
3 * Copyright (C) 2008, Roger C. Soares <rogersoares@intelinet.com.br>
4 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
5 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
7 * All rights reserved. This program and the accompanying materials
8 * are made available under the terms of the Eclipse Public License v1.0
9 * which accompanies this distribution, and is available at
10 * http://www.eclipse.org/legal/epl-v10.html
11 *******************************************************************************/
12 package org.eclipse.egit.ui.internal.components;
14 import java.io.File;
15 import java.net.URISyntaxException;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.regex.Pattern;
20 import org.eclipse.egit.ui.Activator;
21 import org.eclipse.egit.ui.UIText;
22 import org.eclipse.osgi.util.NLS;
23 import org.eclipse.swt.SWT;
24 import org.eclipse.swt.events.ModifyEvent;
25 import org.eclipse.swt.events.ModifyListener;
26 import org.eclipse.swt.events.SelectionAdapter;
27 import org.eclipse.swt.events.SelectionEvent;
28 import org.eclipse.swt.events.VerifyEvent;
29 import org.eclipse.swt.events.VerifyListener;
30 import org.eclipse.swt.layout.GridData;
31 import org.eclipse.swt.layout.GridLayout;
32 import org.eclipse.swt.widgets.Button;
33 import org.eclipse.swt.widgets.Combo;
34 import org.eclipse.swt.widgets.Composite;
35 import org.eclipse.swt.widgets.Control;
36 import org.eclipse.swt.widgets.Group;
37 import org.eclipse.swt.widgets.Label;
38 import org.eclipse.swt.widgets.Text;
39 import org.eclipse.jgit.transport.RemoteConfig;
40 import org.eclipse.jgit.transport.URIish;
41 import org.eclipse.jgit.util.FS;
43 /**
44 * Wizard page that allows the user entering the location of a remote repository
45 * by specifying URL manually or selecting a preconfigured remote repository.
47 public class RepositorySelectionPage extends BaseWizardPage {
48 private static final int REMOTE_CONFIG_TEXT_MAX_LENGTH = 80;
50 private static final String DEFAULT_REMOTE_NAME = "origin";
52 private static final int S_GIT = 0;
54 private static final int S_SSH = 1;
56 private static final int S_SFTP = 2;
58 private static final int S_HTTP = 3;
60 private static final int S_HTTPS = 4;
62 private static final int S_FTP = 5;
64 private static final int S_FILE = 6;
66 private static final String[] DEFAULT_SCHEMES;
67 static {
68 DEFAULT_SCHEMES = new String[7];
69 DEFAULT_SCHEMES[S_GIT] = "git";
70 DEFAULT_SCHEMES[S_SSH] = "git+ssh";
71 DEFAULT_SCHEMES[S_SFTP] = "sftp";
72 DEFAULT_SCHEMES[S_HTTP] = "http";
73 DEFAULT_SCHEMES[S_HTTPS] = "https";
74 DEFAULT_SCHEMES[S_FTP] = "ftp";
75 DEFAULT_SCHEMES[S_FILE] = "file";
78 private static void setEnabledRecursively(final Control control,
79 final boolean enable) {
80 control.setEnabled(enable);
81 if (control instanceof Composite)
82 for (final Control child : ((Composite) control).getChildren())
83 setEnabledRecursively(child, enable);
86 private final List<RemoteConfig> configuredRemotes;
88 private Group authGroup;
90 private Text uriText;
92 private Text hostText;
94 private Text pathText;
96 private Text userText;
98 private Text passText;
100 private Combo scheme;
102 private Text portText;
104 private int eventDepth;
106 private URIish uri;
108 private RemoteConfig remoteConfig;
110 private RepositorySelection selection;
112 private Composite remotePanel;
114 private Button remoteButton;
116 private Combo remoteCombo;
118 private Composite uriPanel;
120 private Button uriButton;
123 * Create repository selection page, allowing user specifying URI or
124 * (optionally) choosing from preconfigured remotes list.
125 * <p>
126 * Wizard page is created without image, just with text description.
128 * @param sourceSelection
129 * true if dialog is used for source selection; false otherwise
130 * (destination selection). This indicates appropriate text
131 * messages.
132 * @param configuredRemotes
133 * list of configured remotes that user may select as an
134 * alternative to manual URI specification. Remotes appear in
135 * given order in GUI, with {@value #DEFAULT_REMOTE_NAME} as the
136 * default choice. List may be null or empty - no remotes
137 * configurations appear in this case. Note that the provided
138 * list may be changed by this constructor.
140 public RepositorySelectionPage(final boolean sourceSelection,
141 final List<RemoteConfig> configuredRemotes) {
142 super(RepositorySelectionPage.class.getName());
143 this.uri = new URIish();
145 if (configuredRemotes != null)
146 removeUnusableRemoteConfigs(configuredRemotes);
147 if (configuredRemotes == null || configuredRemotes.isEmpty())
148 this.configuredRemotes = null;
149 else {
150 this.configuredRemotes = configuredRemotes;
151 this.remoteConfig = selectDefaultRemoteConfig();
153 selection = RepositorySelection.INVALID_SELECTION;
155 if (sourceSelection) {
156 setTitle(UIText.RepositorySelectionPage_sourceSelectionTitle);
157 setDescription(UIText.RepositorySelectionPage_sourceSelectionDescription);
158 } else {
159 setTitle(UIText.RepositorySelectionPage_destinationSelectionTitle);
160 setDescription(UIText.RepositorySelectionPage_destinationSelectionDescription);
165 * Create repository selection page, allowing user specifying URI, with no
166 * preconfigured remotes selection.
168 * @param sourceSelection
169 * true if dialog is used for source selection; false otherwise
170 * (destination selection). This indicates appropriate text
171 * messages.
173 public RepositorySelectionPage(final boolean sourceSelection) {
174 this(sourceSelection, null);
178 * @return repository selection representing current page state.
180 public RepositorySelection getSelection() {
181 return selection;
185 * Compare current repository selection set by user to provided one.
187 * @param s
188 * repository selection to compare.
189 * @return true if provided selection is equal to current page selection,
190 * false otherwise.
192 public boolean selectionEquals(final RepositorySelection s) {
193 return selection.equals(s);
196 public void createControl(final Composite parent) {
197 final Composite panel = new Composite(parent, SWT.NULL);
198 panel.setLayout(new GridLayout());
200 if (configuredRemotes != null)
201 createRemotePanel(panel);
202 createUriPanel(panel);
204 updateRemoteAndURIPanels();
205 setControl(panel);
206 checkPage();
209 private void createRemotePanel(final Composite parent) {
210 remoteButton = new Button(parent, SWT.RADIO);
211 remoteButton
212 .setText(UIText.RepositorySelectionPage_configuredRemoteChoice
213 + ":");
214 remoteButton.setSelection(true);
216 remotePanel = new Composite(parent, SWT.NULL);
217 remotePanel.setLayout(new GridLayout());
218 final GridData gd = new GridData();
219 gd.grabExcessHorizontalSpace = true;
220 gd.horizontalAlignment = SWT.FILL;
221 remotePanel.setLayoutData(gd);
223 remoteCombo = new Combo(remotePanel, SWT.READ_ONLY | SWT.DROP_DOWN);
224 final String items[] = new String[configuredRemotes.size()];
225 int i = 0;
226 for (final RemoteConfig rc : configuredRemotes)
227 items[i++] = getTextForRemoteConfig(rc);
228 final int defaultIndex = configuredRemotes.indexOf(remoteConfig);
229 remoteCombo.setItems(items);
230 remoteCombo.select(defaultIndex);
231 remoteCombo.addSelectionListener(new SelectionAdapter() {
232 @Override
233 public void widgetSelected(SelectionEvent e) {
234 final int idx = remoteCombo.getSelectionIndex();
235 remoteConfig = configuredRemotes.get(idx);
236 checkPage();
241 private void createUriPanel(final Composite parent) {
242 if (configuredRemotes != null) {
243 uriButton = new Button(parent, SWT.RADIO);
244 uriButton.setText(UIText.RepositorySelectionPage_uriChoice + ":");
245 uriButton.addSelectionListener(new SelectionAdapter() {
246 public void widgetSelected(SelectionEvent e) {
247 // occurs either on selection or unselection event
248 updateRemoteAndURIPanels();
249 checkPage();
254 uriPanel = new Composite(parent, SWT.NULL);
255 uriPanel.setLayout(new GridLayout());
256 final GridData gd = new GridData();
257 gd.grabExcessHorizontalSpace = true;
258 gd.horizontalAlignment = SWT.FILL;
259 uriPanel.setLayoutData(gd);
261 createLocationGroup(uriPanel);
262 createConnectionGroup(uriPanel);
263 authGroup = createAuthenticationGroup(uriPanel);
266 private void createLocationGroup(final Composite parent) {
267 final Group g = createGroup(parent,
268 UIText.RepositorySelectionPage_groupLocation);
270 newLabel(g, UIText.RepositorySelectionPage_promptURI + ":");
271 uriText = new Text(g, SWT.BORDER);
272 uriText.setLayoutData(createFieldGridData());
273 uriText.addModifyListener(new ModifyListener() {
274 public void modifyText(final ModifyEvent e) {
275 try {
276 eventDepth++;
277 if (eventDepth != 1)
278 return;
280 final URIish u = new URIish(uriText.getText());
281 safeSet(hostText, u.getHost());
282 safeSet(pathText, u.getPath());
283 safeSet(userText, u.getUser());
284 safeSet(passText, u.getPass());
286 if (u.getPort() > 0)
287 portText.setText(Integer.toString(u.getPort()));
288 else
289 portText.setText("");
291 if (isFile(u))
292 scheme.select(S_FILE);
293 else if (isSSH(u))
294 scheme.select(S_SSH);
295 else {
296 for (int i = 0; i < DEFAULT_SCHEMES.length; i++) {
297 if (DEFAULT_SCHEMES[i].equals(u.getScheme())) {
298 scheme.select(i);
299 break;
304 updateAuthGroup();
305 uri = u;
306 } catch (URISyntaxException err) {
307 // leave uriText as it is, but clean up underlying uri and
308 // decomposed fields
309 uri = new URIish();
310 hostText.setText("");
311 pathText.setText("");
312 userText.setText("");
313 passText.setText("");
314 portText.setText("");
315 scheme.select(0);
316 } finally {
317 eventDepth--;
319 checkPage();
323 newLabel(g, UIText.RepositorySelectionPage_promptHost + ":");
324 hostText = new Text(g, SWT.BORDER);
325 hostText.setLayoutData(createFieldGridData());
326 hostText.addModifyListener(new ModifyListener() {
327 public void modifyText(final ModifyEvent e) {
328 setURI(uri.setHost(nullString(hostText.getText())));
332 newLabel(g, UIText.RepositorySelectionPage_promptPath + ":");
333 pathText = new Text(g, SWT.BORDER);
334 pathText.setLayoutData(createFieldGridData());
335 pathText.addModifyListener(new ModifyListener() {
336 public void modifyText(final ModifyEvent e) {
337 setURI(uri.setPath(nullString(pathText.getText())));
342 private Group createAuthenticationGroup(final Composite parent) {
343 final Group g = createGroup(parent,
344 UIText.RepositorySelectionPage_groupAuthentication);
346 newLabel(g, UIText.RepositorySelectionPage_promptUser + ":");
347 userText = new Text(g, SWT.BORDER);
348 userText.setLayoutData(createFieldGridData());
349 userText.addModifyListener(new ModifyListener() {
350 public void modifyText(final ModifyEvent e) {
351 setURI(uri.setUser(nullString(userText.getText())));
355 newLabel(g, UIText.RepositorySelectionPage_promptPassword + ":");
356 passText = new Text(g, SWT.BORDER | SWT.PASSWORD);
357 passText.setLayoutData(createFieldGridData());
358 return g;
361 private void createConnectionGroup(final Composite parent) {
362 final Group g = createGroup(parent,
363 UIText.RepositorySelectionPage_groupConnection);
365 newLabel(g, UIText.RepositorySelectionPage_promptScheme + ":");
366 scheme = new Combo(g, SWT.DROP_DOWN | SWT.READ_ONLY);
367 scheme.setItems(DEFAULT_SCHEMES);
368 scheme.addSelectionListener(new SelectionAdapter() {
369 public void widgetSelected(final SelectionEvent e) {
370 final int idx = scheme.getSelectionIndex();
371 if (idx < 0)
372 setURI(uri.setScheme(null));
373 else
374 setURI(uri.setScheme(nullString(scheme.getItem(idx))));
375 updateAuthGroup();
379 newLabel(g, UIText.RepositorySelectionPage_promptPort + ":");
380 portText = new Text(g, SWT.BORDER);
381 portText.addVerifyListener(new VerifyListener() {
382 final Pattern p = Pattern.compile("^(?:[1-9][0-9]*)?$");
384 public void verifyText(final VerifyEvent e) {
385 final String v = portText.getText();
386 e.doit = p.matcher(
387 v.substring(0, e.start) + e.text + v.substring(e.end))
388 .matches();
391 portText.addModifyListener(new ModifyListener() {
392 public void modifyText(final ModifyEvent e) {
393 final String val = nullString(portText.getText());
394 if (val == null)
395 setURI(uri.setPort(-1));
396 else {
397 try {
398 setURI(uri.setPort(Integer.parseInt(val)));
399 } catch (NumberFormatException err) {
400 // Ignore it for now.
407 private static Group createGroup(final Composite parent, final String text) {
408 final Group g = new Group(parent, SWT.NONE);
409 final GridLayout layout = new GridLayout();
410 layout.numColumns = 2;
411 g.setLayout(layout);
412 g.setText(text);
413 final GridData gd = new GridData();
414 gd.grabExcessHorizontalSpace = true;
415 gd.horizontalAlignment = SWT.FILL;
416 g.setLayoutData(gd);
417 return g;
420 private static void newLabel(final Group g, final String text) {
421 new Label(g, SWT.NULL).setText(text);
424 private static GridData createFieldGridData() {
425 return new GridData(SWT.FILL, SWT.DEFAULT, true, false);
428 private static boolean isGIT(final URIish uri) {
429 return "git".equals(uri.getScheme());
432 private static boolean isFile(final URIish uri) {
433 if ("file".equals(uri.getScheme()) || uri.getScheme() == null)
434 return true;
435 if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null
436 || uri.getPass() != null || uri.getPath() == null)
437 return false;
438 if (uri.getScheme() == null)
439 return FS.resolve(new File("."), uri.getPath()).isDirectory();
440 return false;
443 private static boolean isSSH(final URIish uri) {
444 if (!uri.isRemote())
445 return false;
446 final String scheme = uri.getScheme();
447 if ("ssh".equals(scheme))
448 return true;
449 if ("ssh+git".equals(scheme))
450 return true;
451 if ("git+ssh".equals(scheme))
452 return true;
453 if (scheme == null && uri.getHost() != null && uri.getPath() != null)
454 return true;
455 return false;
458 private static String nullString(final String value) {
459 if (value == null)
460 return null;
461 final String v = value.trim();
462 return v.length() == 0 ? null : v;
465 private static void safeSet(final Text text, final String value) {
466 text.setText(value != null ? value : "");
469 private boolean isURISelected() {
470 return configuredRemotes == null || uriButton.getSelection();
473 private void setURI(final URIish u) {
474 try {
475 eventDepth++;
476 if (eventDepth == 1) {
477 uri = u;
478 uriText.setText(uri.toString());
479 checkPage();
481 } finally {
482 eventDepth--;
486 private static void removeUnusableRemoteConfigs(
487 final List<RemoteConfig> remotes) {
488 final Iterator<RemoteConfig> iter = remotes.iterator();
489 while (iter.hasNext()) {
490 final RemoteConfig rc = iter.next();
491 if (rc.getURIs().isEmpty())
492 iter.remove();
496 private RemoteConfig selectDefaultRemoteConfig() {
497 for (final RemoteConfig rc : configuredRemotes)
498 if (getTextForRemoteConfig(rc) == DEFAULT_REMOTE_NAME)
499 return rc;
500 return configuredRemotes.get(0);
503 private static String getTextForRemoteConfig(final RemoteConfig rc) {
504 final StringBuilder sb = new StringBuilder(rc.getName());
505 sb.append(": ");
506 boolean first = true;
507 for (final URIish u : rc.getURIs()) {
508 final String uString = u.toString();
509 if (first)
510 first = false;
511 else {
512 sb.append(", ");
513 if (sb.length() + uString.length() > REMOTE_CONFIG_TEXT_MAX_LENGTH) {
514 sb.append("...");
515 break;
518 sb.append(uString);
520 return sb.toString();
523 private void checkPage() {
524 if (isURISelected()) {
525 assert uri != null;
526 if (uriText.getText().length() == 0) {
527 selectionIncomplete(null);
528 return;
531 try {
532 final URIish finalURI = new URIish(uriText.getText());
533 String proto = finalURI.getScheme();
534 if (proto == null && scheme.getSelectionIndex() >= 0)
535 proto = scheme.getItem(scheme.getSelectionIndex());
537 if (uri.getPath() == null) {
538 selectionIncomplete(NLS.bind(
539 UIText.RepositorySelectionPage_fieldRequired,
540 unamp(UIText.RepositorySelectionPage_promptPath), proto));
541 return;
544 if (isFile(finalURI)) {
545 String badField = null;
546 if (uri.getHost() != null)
547 badField = UIText.RepositorySelectionPage_promptHost;
548 else if (uri.getUser() != null)
549 badField = UIText.RepositorySelectionPage_promptUser;
550 else if (uri.getPass() != null)
551 badField = UIText.RepositorySelectionPage_promptPassword;
552 if (badField != null) {
553 selectionIncomplete(NLS
554 .bind(
555 UIText.RepositorySelectionPage_fieldNotSupported,
556 unamp(badField), proto));
557 return;
560 final File d = FS.resolve(new File("."), uri.getPath());
561 if (!d.exists()) {
562 selectionIncomplete(NLS.bind(
563 UIText.RepositorySelectionPage_fileNotFound, d
564 .getAbsolutePath()));
565 return;
568 selectionComplete(finalURI, null);
569 return;
572 if (uri.getHost() == null) {
573 selectionIncomplete(NLS.bind(
574 UIText.RepositorySelectionPage_fieldRequired,
575 unamp(UIText.RepositorySelectionPage_promptHost), proto));
576 return;
579 if (isGIT(finalURI)) {
580 String badField = null;
581 if (uri.getUser() != null)
582 badField = UIText.RepositorySelectionPage_promptUser;
583 else if (uri.getPass() != null)
584 badField = UIText.RepositorySelectionPage_promptPassword;
585 if (badField != null) {
586 selectionIncomplete(NLS
587 .bind(
588 UIText.RepositorySelectionPage_fieldNotSupported,
589 unamp(badField), proto));
590 return;
594 selectionComplete(finalURI, null);
595 return;
596 } catch (URISyntaxException e) {
597 selectionIncomplete(e.getReason());
598 return;
599 } catch (Exception e) {
600 Activator.logError("Error validating " + getClass().getName(),
602 selectionIncomplete(UIText.RepositorySelectionPage_internalError);
603 return;
605 } else {
606 assert remoteButton.getSelection();
607 selectionComplete(null, remoteConfig);
608 return;
612 private String unamp(String s) {
613 return s.replace("&","");
616 private void selectionIncomplete(final String errorMessage) {
617 setExposedSelection(null, null);
618 setErrorMessage(errorMessage);
619 setPageComplete(false);
622 private void selectionComplete(final URIish u, final RemoteConfig rc) {
623 setExposedSelection(u, rc);
624 setErrorMessage(null);
625 setPageComplete(true);
628 private void setExposedSelection(final URIish u, final RemoteConfig rc) {
629 final RepositorySelection newSelection = new RepositorySelection(u, rc);
630 if (newSelection.equals(selection))
631 return;
633 selection = newSelection;
634 notifySelectionChanged();
637 private void updateRemoteAndURIPanels() {
638 setEnabledRecursively(uriPanel, isURISelected());
639 if (uriPanel.getEnabled())
640 updateAuthGroup();
641 if (configuredRemotes != null)
642 setEnabledRecursively(remotePanel, !isURISelected());
645 private void updateAuthGroup() {
646 switch (scheme.getSelectionIndex()) {
647 case S_GIT:
648 hostText.setEnabled(true);
649 portText.setEnabled(true);
650 setEnabledRecursively(authGroup, false);
651 break;
652 case S_SSH:
653 case S_SFTP:
654 case S_HTTP:
655 case S_HTTPS:
656 case S_FTP:
657 hostText.setEnabled(true);
658 portText.setEnabled(true);
659 setEnabledRecursively(authGroup, true);
660 break;
661 case S_FILE:
662 hostText.setEnabled(false);
663 portText.setEnabled(false);
664 setEnabledRecursively(authGroup, false);
665 break;
669 @Override
670 public void setVisible(boolean visible) {
671 super.setVisible(visible);
672 if (visible)
673 uriText.setFocus();