Refactor/rewrite CloneSourcePage to universal RepositorySelectionPage
[egit/zawir.git] / org.spearce.egit.ui / src / org / spearce / egit / ui / internal / components / RepositorySelectionPage.java
bloba487cb16d8876ee05d09fccc82f7a593f8d98888
1 /*******************************************************************************
2 * Copyright (C) 2007, 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 * See LICENSE for the full license text, also available.
10 *******************************************************************************/
11 package org.spearce.egit.ui.internal.components;
13 import java.io.File;
14 import java.net.URISyntaxException;
15 import java.util.Iterator;
16 import java.util.LinkedList;
17 import java.util.List;
18 import java.util.regex.Pattern;
20 import org.eclipse.jface.wizard.WizardPage;
21 import org.eclipse.osgi.util.NLS;
22 import org.eclipse.swt.SWT;
23 import org.eclipse.swt.events.ModifyEvent;
24 import org.eclipse.swt.events.ModifyListener;
25 import org.eclipse.swt.events.SelectionAdapter;
26 import org.eclipse.swt.events.SelectionEvent;
27 import org.eclipse.swt.events.VerifyEvent;
28 import org.eclipse.swt.events.VerifyListener;
29 import org.eclipse.swt.layout.GridData;
30 import org.eclipse.swt.layout.GridLayout;
31 import org.eclipse.swt.widgets.Button;
32 import org.eclipse.swt.widgets.Combo;
33 import org.eclipse.swt.widgets.Composite;
34 import org.eclipse.swt.widgets.Control;
35 import org.eclipse.swt.widgets.Group;
36 import org.eclipse.swt.widgets.Label;
37 import org.eclipse.swt.widgets.Text;
38 import org.spearce.egit.ui.Activator;
39 import org.spearce.egit.ui.UIText;
40 import org.spearce.jgit.transport.RemoteConfig;
41 import org.spearce.jgit.transport.URIish;
42 import org.spearce.jgit.util.FS;
44 /**
45 * Wizard page that allows the user entering the location of a remote repository
46 * by specifying URL manually or selecting a preconfigured remote repository.
48 public class RepositorySelectionPage extends WizardPage {
49 private static final int REMOTE_CONFIG_TEXT_MAX_LENGTH = 80;
51 private static final String DEFAULT_REMOTE_NAME = "origin";
53 private static final int S_GIT = 0;
55 private static final int S_SSH = 1;
57 private static final int S_SFTP = 2;
59 private static final int S_HTTP = 3;
61 private static final int S_HTTPS = 4;
63 private static final int S_FTP = 5;
65 private static final int S_FILE = 6;
67 private static final String[] DEFAULT_SCHEMES;
68 static {
69 DEFAULT_SCHEMES = new String[7];
70 DEFAULT_SCHEMES[S_GIT] = "git";
71 DEFAULT_SCHEMES[S_SSH] = "git+ssh";
72 DEFAULT_SCHEMES[S_SFTP] = "sftp";
73 DEFAULT_SCHEMES[S_HTTP] = "http";
74 DEFAULT_SCHEMES[S_HTTPS] = "https";
75 DEFAULT_SCHEMES[S_FTP] = "ftp";
76 DEFAULT_SCHEMES[S_FILE] = "file";
79 private static void setEnabledRecursively(final Control control,
80 final boolean enable) {
81 control.setEnabled(enable);
82 if (control instanceof Composite)
83 for (final Control child : ((Composite) control).getChildren())
84 setEnabledRecursively(child, enable);
87 private final List<RepositorySelectionListener> selectionListeners;
89 private final List<RemoteConfig> configuredRemotes;
91 private Group authGroup;
93 private Text uriText;
95 private Text hostText;
97 private Text pathText;
99 private Text userText;
101 private Text passText;
103 private Combo scheme;
105 private Text portText;
107 private int eventDepth;
109 private URIish uri;
111 private RemoteConfig remoteConfig;
113 private RemoteConfig exposedRemoteConfig;
115 private URIish exposedURI;
117 private Composite remotePanel;
119 private Button remoteButton;
121 private Combo remoteCombo;
123 private Composite uriPanel;
125 private Button uriButton;
128 * Create repository selection page, allowing user specifying URI or
129 * (optionally) choosing from preconfigured remotes list.
130 * <p>
131 * Wizard page is created without image, just with text description.
133 * @param sourceSelection
134 * true if dialog is used for source selection; false otherwise
135 * (destination selection). This indicates appropriate text
136 * messages.
137 * @param configuredRemotes
138 * list of configured remotes that user may select as an
139 * alternative to manual URI specification. Remotes appear in
140 * given order in GUI, with {@value #DEFAULT_REMOTE_NAME} as the
141 * default choice. List may be null or empty - no remotes
142 * configurations appear in this case. Note that the provided
143 * list may be changed by this constructor.
145 public RepositorySelectionPage(final boolean sourceSelection,
146 final List<RemoteConfig> configuredRemotes) {
147 super(RepositorySelectionPage.class.getName());
148 this.uri = new URIish();
150 if (configuredRemotes != null)
151 removeUnusableRemoteConfigs(configuredRemotes);
152 if (configuredRemotes == null || configuredRemotes.isEmpty())
153 this.configuredRemotes = null;
154 else {
155 this.configuredRemotes = configuredRemotes;
156 this.remoteConfig = selectDefaultRemoteConfig();
159 if (sourceSelection) {
160 setTitle(UIText.RepositorySelectionPage_sourceSelectionTitle);
161 setDescription(UIText.RepositorySelectionPage_sourceSelectionDescription);
162 } else {
163 setTitle(UIText.RepositorySelectionPage_destinationSelectionTitle);
164 setDescription(UIText.RepositorySelectionPage_destinationSelectionDescription);
166 selectionListeners = new LinkedList<RepositorySelectionListener>();
170 * Create repository selection page, allowing user specifying URI, with no
171 * preconfigured remotes selection.
173 * @param sourceSelection
174 * true if dialog is used for source selection; false otherwise
175 * (destination selection). This indicates appropriate text
176 * messages.
178 public RepositorySelectionPage(final boolean sourceSelection) {
179 this(sourceSelection, null);
183 * Add {@link RepositorySelectionListener} to list of listeners notified on
184 * repository selection change.
186 * @param l
187 * listener that will be notified about changes
189 public void addRepositorySelectionListener(
190 final RepositorySelectionListener l) {
191 selectionListeners.add(l);
194 public void createControl(final Composite parent) {
195 final Composite panel = new Composite(parent, SWT.NULL);
196 panel.setLayout(new GridLayout());
198 if (configuredRemotes != null)
199 createRemotePanel(panel);
200 createUriPanel(panel);
202 updateRemoteAndURIPanels();
203 setControl(panel);
204 checkPage();
207 private void createRemotePanel(final Composite parent) {
208 remoteButton = new Button(parent, SWT.RADIO);
209 remoteButton
210 .setText(UIText.RepositorySelectionPage_configuredRemoteChoice
211 + ":");
212 remoteButton.setSelection(true);
214 remotePanel = new Composite(parent, SWT.NULL);
215 remotePanel.setLayout(new GridLayout());
216 final GridData gd = new GridData();
217 gd.grabExcessHorizontalSpace = true;
218 gd.horizontalAlignment = SWT.FILL;
219 remotePanel.setLayoutData(gd);
221 remoteCombo = new Combo(remotePanel, SWT.READ_ONLY | SWT.DROP_DOWN);
222 final String items[] = new String[configuredRemotes.size()];
223 int i = 0;
224 for (final RemoteConfig rc : configuredRemotes)
225 items[i++] = getTextForRemoteConfig(rc);
226 final int defaultIndex = configuredRemotes.indexOf(remoteConfig);
227 remoteCombo.setItems(items);
228 remoteCombo.select(defaultIndex);
229 remoteCombo.addSelectionListener(new SelectionAdapter() {
230 @Override
231 public void widgetSelected(SelectionEvent e) {
232 final int idx = remoteCombo.getSelectionIndex();
233 remoteConfig = configuredRemotes.get(idx);
234 checkPage();
239 private void createUriPanel(final Composite parent) {
240 if (configuredRemotes != null) {
241 uriButton = new Button(parent, SWT.RADIO);
242 uriButton.setText(UIText.RepositorySelectionPage_uriChoice + ":");
243 uriButton.addSelectionListener(new SelectionAdapter() {
244 public void widgetSelected(SelectionEvent e) {
245 // occurs either on selection or unselection event
246 updateRemoteAndURIPanels();
247 checkPage();
252 uriPanel = new Composite(parent, SWT.NULL);
253 uriPanel.setLayout(new GridLayout());
254 final GridData gd = new GridData();
255 gd.grabExcessHorizontalSpace = true;
256 gd.horizontalAlignment = SWT.FILL;
257 uriPanel.setLayoutData(gd);
259 createLocationGroup(uriPanel);
260 createConnectionGroup(uriPanel);
261 authGroup = createAuthenticationGroup(uriPanel);
264 private void createLocationGroup(final Composite parent) {
265 final Group g = createGroup(parent,
266 UIText.RepositorySelectionPage_groupLocation);
268 newLabel(g, UIText.RepositorySelectionPage_promptURI + ":");
269 uriText = new Text(g, SWT.BORDER);
270 uriText.setLayoutData(createFieldGridData());
271 uriText.addModifyListener(new ModifyListener() {
272 public void modifyText(final ModifyEvent e) {
273 try {
274 eventDepth++;
275 if (eventDepth != 1)
276 return;
278 final URIish u = new URIish(uriText.getText());
279 safeSet(hostText, u.getHost());
280 safeSet(pathText, u.getPath());
281 safeSet(userText, u.getUser());
282 safeSet(passText, u.getPass());
284 if (u.getPort() > 0)
285 portText.setText(Integer.toString(u.getPort()));
286 else
287 portText.setText("");
289 if (isFile(u))
290 scheme.select(S_FILE);
291 else if (isSSH(u))
292 scheme.select(S_SSH);
293 else {
294 for (int i = 0; i < DEFAULT_SCHEMES.length; i++) {
295 if (DEFAULT_SCHEMES[i].equals(u.getScheme())) {
296 scheme.select(i);
297 break;
302 updateAuthGroup();
303 uri = u;
304 } catch (URISyntaxException err) {
305 // leave uriText as it is, but clean up underlying uri and
306 // decomposed fields
307 uri = new URIish();
308 hostText.setText("");
309 pathText.setText("");
310 userText.setText("");
311 passText.setText("");
312 portText.setText("");
313 scheme.select(0);
314 } finally {
315 eventDepth--;
317 checkPage();
321 newLabel(g, UIText.RepositorySelectionPage_promptHost + ":");
322 hostText = new Text(g, SWT.BORDER);
323 hostText.setLayoutData(createFieldGridData());
324 hostText.addModifyListener(new ModifyListener() {
325 public void modifyText(final ModifyEvent e) {
326 setURI(uri.setHost(nullString(hostText.getText())));
330 newLabel(g, UIText.RepositorySelectionPage_promptPath + ":");
331 pathText = new Text(g, SWT.BORDER);
332 pathText.setLayoutData(createFieldGridData());
333 pathText.addModifyListener(new ModifyListener() {
334 public void modifyText(final ModifyEvent e) {
335 setURI(uri.setPath(nullString(pathText.getText())));
340 private Group createAuthenticationGroup(final Composite parent) {
341 final Group g = createGroup(parent,
342 UIText.RepositorySelectionPage_groupAuthentication);
344 newLabel(g, UIText.RepositorySelectionPage_promptUser + ":");
345 userText = new Text(g, SWT.BORDER);
346 userText.setLayoutData(createFieldGridData());
347 userText.addModifyListener(new ModifyListener() {
348 public void modifyText(final ModifyEvent e) {
349 setURI(uri.setUser(nullString(userText.getText())));
353 newLabel(g, UIText.RepositorySelectionPage_promptPassword + ":");
354 passText = new Text(g, SWT.BORDER | SWT.PASSWORD);
355 passText.setLayoutData(createFieldGridData());
356 return g;
359 private void createConnectionGroup(final Composite parent) {
360 final Group g = createGroup(parent,
361 UIText.RepositorySelectionPage_groupConnection);
363 newLabel(g, UIText.RepositorySelectionPage_promptScheme + ":");
364 scheme = new Combo(g, SWT.DROP_DOWN | SWT.READ_ONLY);
365 scheme.setItems(DEFAULT_SCHEMES);
366 scheme.addSelectionListener(new SelectionAdapter() {
367 public void widgetSelected(final SelectionEvent e) {
368 final int idx = scheme.getSelectionIndex();
369 if (idx < 0)
370 setURI(uri.setScheme(null));
371 else
372 setURI(uri.setScheme(nullString(scheme.getItem(idx))));
373 updateAuthGroup();
377 newLabel(g, UIText.RepositorySelectionPage_promptPort + ":");
378 portText = new Text(g, SWT.BORDER);
379 portText.addVerifyListener(new VerifyListener() {
380 final Pattern p = Pattern.compile("^(?:[1-9][0-9]*)?$");
382 public void verifyText(final VerifyEvent e) {
383 final String v = portText.getText();
384 e.doit = p.matcher(
385 v.substring(0, e.start) + e.text + v.substring(e.end))
386 .matches();
389 portText.addModifyListener(new ModifyListener() {
390 public void modifyText(final ModifyEvent e) {
391 final String val = nullString(portText.getText());
392 if (val == null)
393 setURI(uri.setPort(-1));
394 else {
395 try {
396 setURI(uri.setPort(Integer.parseInt(val)));
397 } catch (NumberFormatException err) {
398 // Ignore it for now.
405 private static Group createGroup(final Composite parent, final String text) {
406 final Group g = new Group(parent, SWT.NONE);
407 final GridLayout layout = new GridLayout();
408 layout.numColumns = 2;
409 g.setLayout(layout);
410 g.setText(text);
411 final GridData gd = new GridData();
412 gd.grabExcessHorizontalSpace = true;
413 gd.horizontalAlignment = SWT.FILL;
414 g.setLayoutData(gd);
415 return g;
418 private static void newLabel(final Group g, final String text) {
419 new Label(g, SWT.NULL).setText(text);
422 private static GridData createFieldGridData() {
423 return new GridData(SWT.FILL, SWT.DEFAULT, true, false);
427 * @return the URI entered in the Wizard page. null if URI is invalid or
428 * user chosen to select remote config instead of providing direct
429 * URI.
431 public URIish getURI() {
432 return exposedURI;
436 * @return the selected remote configuration in the Wizard page. null if
437 * user chosen to select repository as URI.
439 public RemoteConfig getRemoteConfig() {
440 return exposedRemoteConfig;
443 private static boolean isGIT(final URIish uri) {
444 return "git".equals(uri.getScheme());
447 private static boolean isFile(final URIish uri) {
448 if ("file".equals(uri.getScheme()))
449 return true;
450 if (uri.getHost() != null || uri.getPort() > 0 || uri.getUser() != null
451 || uri.getPass() != null || uri.getPath() == null)
452 return false;
453 if (uri.getScheme() == null)
454 return FS.resolve(new File("."), uri.getPath()).isDirectory();
455 return false;
458 private static boolean isSSH(final URIish uri) {
459 if (!uri.isRemote())
460 return false;
461 final String scheme = uri.getScheme();
462 if ("ssh".equals(scheme))
463 return true;
464 if ("ssh+git".equals(scheme))
465 return true;
466 if ("git+ssh".equals(scheme))
467 return true;
468 if (scheme == null && uri.getHost() != null && uri.getPath() != null)
469 return true;
470 return false;
473 private static String nullString(final String value) {
474 if (value == null)
475 return null;
476 final String v = value.trim();
477 return v.length() == 0 ? null : v;
480 private static void safeSet(final Text text, final String value) {
481 text.setText(value != null ? value : "");
484 private boolean isURISelected() {
485 return configuredRemotes == null || uriButton.getSelection();
488 private void setURI(final URIish u) {
489 try {
490 eventDepth++;
491 if (eventDepth == 1) {
492 uri = u;
493 uriText.setText(uri.toString());
494 checkPage();
496 } finally {
497 eventDepth--;
501 private static void removeUnusableRemoteConfigs(
502 final List<RemoteConfig> remotes) {
503 final Iterator<RemoteConfig> iter = remotes.iterator();
504 while (iter.hasNext()) {
505 final RemoteConfig rc = iter.next();
506 if (rc.getURIs().isEmpty())
507 iter.remove();
511 private RemoteConfig selectDefaultRemoteConfig() {
512 for (final RemoteConfig rc : configuredRemotes)
513 if (getTextForRemoteConfig(rc) == DEFAULT_REMOTE_NAME)
514 return rc;
515 return configuredRemotes.get(0);
518 private static String getTextForRemoteConfig(final RemoteConfig rc) {
519 final StringBuilder sb = new StringBuilder(rc.getName());
520 sb.append(": ");
521 boolean first = true;
522 for (final URIish u : rc.getURIs()) {
523 final String uString = u.toString();
524 if (first)
525 first = false;
526 else {
527 sb.append(", ");
528 if (sb.length() + uString.length() > REMOTE_CONFIG_TEXT_MAX_LENGTH) {
529 sb.append("...");
530 break;
533 sb.append(uString);
535 return sb.toString();
538 private void checkPage() {
539 if (isURISelected()) {
540 assert uri != null;
541 if (uriText.getText().length() == 0) {
542 selectionIncomplete(null);
543 return;
546 try {
547 final URIish finalURI = new URIish(uriText.getText());
548 String proto = finalURI.getScheme();
549 if (proto == null && scheme.getSelectionIndex() >= 0)
550 proto = scheme.getItem(scheme.getSelectionIndex());
552 if (uri.getPath() == null) {
553 selectionIncomplete(NLS.bind(
554 UIText.RepositorySelectionPage_fieldRequired,
555 UIText.RepositorySelectionPage_promptPath, proto));
556 return;
559 if (isFile(finalURI)) {
560 String badField = null;
561 if (uri.getHost() != null)
562 badField = UIText.RepositorySelectionPage_promptHost;
563 else if (uri.getUser() != null)
564 badField = UIText.RepositorySelectionPage_promptUser;
565 else if (uri.getPass() != null)
566 badField = UIText.RepositorySelectionPage_promptPassword;
567 if (badField != null) {
568 selectionIncomplete(NLS
569 .bind(
570 UIText.RepositorySelectionPage_fieldNotSupported,
571 badField, proto));
572 return;
575 final File d = FS.resolve(new File("."), uri.getPath());
576 if (!d.exists()) {
577 selectionIncomplete(NLS.bind(
578 UIText.RepositorySelectionPage_fileNotFound, d
579 .getAbsolutePath()));
580 return;
583 selectionComplete(finalURI, null);
584 return;
587 if (uri.getHost() == null) {
588 selectionIncomplete(NLS.bind(
589 UIText.RepositorySelectionPage_fieldRequired,
590 UIText.RepositorySelectionPage_promptHost, proto));
591 return;
594 if (isGIT(finalURI)) {
595 String badField = null;
596 if (uri.getUser() != null)
597 badField = UIText.RepositorySelectionPage_promptUser;
598 else if (uri.getPass() != null)
599 badField = UIText.RepositorySelectionPage_promptPassword;
600 if (badField != null) {
601 selectionIncomplete(NLS
602 .bind(
603 UIText.RepositorySelectionPage_fieldNotSupported,
604 badField, proto));
605 return;
609 selectionComplete(finalURI, null);
610 return;
611 } catch (URISyntaxException e) {
612 selectionIncomplete(e.getReason());
613 return;
614 } catch (Exception e) {
615 Activator.logError("Error validating " + getClass().getName(),
617 selectionIncomplete(UIText.RepositorySelectionPage_internalError);
618 return;
620 } else {
621 assert remoteButton.getSelection();
622 selectionComplete(null, remoteConfig);
623 return;
627 private void selectionIncomplete(final String errorMessage) {
628 setExposedSelection(null, null);
629 setErrorMessage(errorMessage);
630 setPageComplete(false);
633 private void selectionComplete(final URIish u, final RemoteConfig rc) {
634 setExposedSelection(u, rc);
635 setErrorMessage(null);
636 setPageComplete(true);
639 private void setExposedSelection(final URIish u, final RemoteConfig rc) {
640 if (u == exposedURI && rc == exposedRemoteConfig) // nothing changed
641 return;
642 this.exposedURI = u;
643 this.exposedRemoteConfig = rc;
645 for (final RepositorySelectionListener l : selectionListeners)
646 l.selectionChanged(u, rc);
649 private void updateRemoteAndURIPanels() {
650 setEnabledRecursively(uriPanel, isURISelected());
651 if (uriPanel.getEnabled())
652 updateAuthGroup();
653 if (configuredRemotes != null)
654 setEnabledRecursively(remotePanel, !isURISelected());
657 private void updateAuthGroup() {
658 switch (scheme.getSelectionIndex()) {
659 case S_GIT:
660 hostText.setEnabled(true);
661 portText.setEnabled(true);
662 setEnabledRecursively(authGroup, false);
663 break;
664 case S_SSH:
665 case S_SFTP:
666 case S_HTTP:
667 case S_HTTPS:
668 case S_FTP:
669 hostText.setEnabled(true);
670 portText.setEnabled(true);
671 setEnabledRecursively(authGroup, true);
672 break;
673 case S_FILE:
674 hostText.setEnabled(false);
675 portText.setEnabled(false);
676 setEnabledRecursively(authGroup, false);
677 break;