/******************************************************************************* * Copyright (c) 2004, 2006 * Thomas Hallgren, Kenneth Olwing, Mitch Sonies * Pontus Rydin, Nils Unden, Peer Torngren * The code, documentation and other materials contained herein have been * licensed under the Eclipse Public License - v 1.0 by the individual * copyright holders listed above, as Initial Contributors under such license. * The text of such license is available at www.eclipse.org. *******************************************************************************/ package org.eclipse.buckminster.subclipse.internal; import java.io.File; import java.io.FileNotFoundException; import java.net.URI; import java.util.Collection; import java.util.Date; import java.util.Map; import java.util.Properties; import java.util.UUID; import org.eclipse.buckminster.core.CorePlugin; import org.eclipse.buckminster.core.RMContext; import org.eclipse.buckminster.core.version.VersionSelector; import org.eclipse.buckminster.runtime.BuckminsterException; import org.eclipse.buckminster.runtime.Logger; import org.eclipse.buckminster.runtime.MonitorUtils; import org.eclipse.buckminster.subclipse.Messages; import org.eclipse.buckminster.subversion.GenericCache; import org.eclipse.buckminster.subversion.GenericSession; import org.eclipse.buckminster.subversion.ISubversionCache; import org.eclipse.buckminster.subversion.ISvnEntryHelper; import org.eclipse.buckminster.subversion.RepositoryAccess; import org.eclipse.buckminster.subversion.SvnExceptionHandler; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.NullProgressMonitor; import org.tigris.subversion.clientadapter.Activator; import org.tigris.subversion.subclipse.core.ISVNRepositoryLocation; import org.tigris.subversion.subclipse.core.SVNClientManager; import org.tigris.subversion.subclipse.core.SVNException; import org.tigris.subversion.subclipse.core.SVNProviderPlugin; import org.tigris.subversion.subclipse.core.repo.SVNRepositories; import org.tigris.subversion.svnclientadapter.ISVNClientAdapter; import org.tigris.subversion.svnclientadapter.ISVNDirEntry; import org.tigris.subversion.svnclientadapter.ISVNInfo; import org.tigris.subversion.svnclientadapter.ISVNPromptUserPassword; import org.tigris.subversion.svnclientadapter.SVNClientException; import org.tigris.subversion.svnclientadapter.SVNRevision; import org.tigris.subversion.svnclientadapter.SVNUrl; /** * <p> * The SVN repository will be able to use reader checks if a repository contains * the three recommended directories <code>trunk</code>, <code>tags</code>, and * <code>branches</code>. A missing <code>tags</code> directory is interpreted * as no <code>tags</code>. A missing <code>branches</code> directory is * interpreted as no branches. In order to use <code>trunk</code>, * <code>tags</code>, and <code>branches</code> repository identifier must * contain the path element <code>trunk</code>. Anything that follows the * <code>trunk</code> element in the path will be considered a * <code>module</code> path. If no <code>trunk</code> element is present in the * path, the last element will be considered the <code>module</code> * </p> * <p> * The repository URL may also contain a query part that in turn may have four * different flags: * <dl> * <dt>moduleBeforeTag</dt> * <dd>When resolving a tag, put the module name between the <code>tags</code> * directory and the actual tag</dd> * <dt>moduleAfterTag</dt> * <dd>When resolving a tag, append the module name after the actual tag</dd> * <dt>moduleBeforeBranch</dt> * <dd>When resolving a branch, put the module name between the * <code>branches</code> directory and the actual branch</dd> * <dt>moduleAfterBranch</dt> * <dd>When resolving a branch, append the module name after the actual branch</dd> * </dl> * </p> * A fragment in the repository URL will be treated as a sub-module. It will be * appended at the end of the resolved URL. * * @author Thomas Hallgren * @author Guillaume Chatelet */ public class SvnSession extends GenericSession<ISVNRepositoryLocation, ISVNDirEntry, SVNRevision> { private class UnattendedPromptUserPassword implements ISVNPromptUserPassword { private int promptPasswordLimit = 3; private int promptUserLimit = 3; @Override public String askQuestion(String realm, String question, boolean showAnswer, boolean maySave) { // We do not support questions // return null; } @Override public int askTrustSSLServer(String info, boolean allowPermanently) { return ISVNPromptUserPassword.AcceptTemporary; } @Override public boolean askYesNo(String realm, String question, boolean yesIsDefault) { return yesIsDefault; } @Override public String getPassword() { return password; } @Override public int getSSHPort() { // We do not support SSH // return -1; } @Override public String getSSHPrivateKeyPassphrase() { // We do not support SSH // return null; } @Override public String getSSHPrivateKeyPath() { // We do not support SSH // return null; } @Override public String getSSLClientCertPassword() { // We do not support SSL // return null; } @Override public String getSSLClientCertPath() { // We do not support SSL // return null; } @Override public String getUsername() { return username; } @Override public boolean prompt(String realm, String user, boolean maySave) { // We support the password prompt only if we actually know the // password // and only a limited number of times // return password != null && --promptPasswordLimit >= 0; } @Override public boolean promptSSH(String realm, String user, int sshPort, boolean maySave) { // We do not support SSH prompt // return false; } @Override public boolean promptSSL(String realm, boolean maySave) { // We do not support SSL prompt // return false; } @Override public boolean promptUser(String realm, String user, boolean maySave) { // We do support the user prompt but only a limited number of times // return --promptUserLimit >= 0; } @Override public boolean userAllowedSave() { // No need to save anything // return false; } } private static final SvnEntryHelper HELPER = new SvnEntryHelper(); private static final ISVNDirEntry[] emptyFolder = new ISVNDirEntry[0]; private static SVNProviderPlugin getPlugin() { return SVNProviderPlugin.getPlugin(); } private ISVNClientAdapter clientAdapter; private static final String UNKNOWN_ROOT_PREFIX = SvnSession.class.getPackage().getName() + ".root."; //$NON-NLS-1$ private static SVNRepositories getRepositories() { return getPlugin().getRepositories(); } /** * @param repositoryURI * The string representation of the URI that appoints the trunk * of repository module. No branch or tag information must be * included. * @param branch * The desired branch or <code>null</code> if not applicable. * @param tag * The desired tag or <code>null</code> if not applicable. * @param revision * The desired revision or <code>-1</code> of not applicable * @param timestamp * The desired timestamp or <code>null</code> if not applicable * @param context * The context used for the resolution/materialization operation * @throws CoreException */ public SvnSession(String repositoryURI, VersionSelector branchOrTag, long revision, Date timestamp, RMContext context) throws CoreException { super(repositoryURI, branchOrTag, revision, timestamp, context); } @Override public void close() { clientAdapter.dispose(); } @Override public ISVNRepositoryLocation[] getKnownRepositories() throws CoreException { return getRepositories().getKnownRepositories(new NullProgressMonitor()); } @Override public long getLastChangeNumber() throws CoreException { try { SVNUrl svnURL = TypeTranslator.from(getSVNUrl(null)); ISVNDirEntry root = clientAdapter.getDirEntry(svnURL, getRevision()); if (root == null) throw new FileNotFoundException(svnURL.toString()); return root.getLastChangedRevision().getNumber(); } catch (Exception e) { throw BuckminsterException.wrap(e); } } public long getLastChangeNumber(File workingCopy) throws CoreException { try { return clientAdapter.getInfoFromWorkingCopy(workingCopy).getLastChangedRevision().getNumber(); } catch (Exception e) { throw BuckminsterException.wrap(e); } } @Override public Date getLastTimestamp() throws CoreException { try { SVNUrl svnURL = TypeTranslator.from(getSVNUrl(null)); ISVNDirEntry root = clientAdapter.getDirEntry(svnURL, getRevision()); if (root == null) throw new FileNotFoundException(svnURL.toString()); return root.getLastChangedDate(); } catch (Exception e) { throw BuckminsterException.wrap(e); } } public SVNRevision.Number getRepositoryRevision(IProgressMonitor monitor) throws CoreException { SVNRevision.Number repoRev = null; if (getRevision() instanceof SVNRevision.Number) { repoRev = (SVNRevision.Number) getRevision(); MonitorUtils.complete(monitor); } else { monitor.beginTask(null, 1); try { for (int retries = 0;; ++retries) { try { SVNUrl svnURL = TypeTranslator.from(getSVNUrl(null)); ISVNInfo info = clientAdapter.getInfo(svnURL); if (info == null) return null; repoRev = info.getRevision(); break; } catch (SVNClientException e) { if (++retries < 3) { try { Thread.sleep(2000); } catch (InterruptedException e1) { } continue; } throw BuckminsterException.wrap(e); } } } finally { monitor.done(); } } return repoRev; } @Override public ISVNDirEntry getRootEntry(IProgressMonitor monitor) throws CoreException { // Synchronizing on an interned string should make it impossible for two // sessions to request the same entry from the remote server // SVNUrl url = TypeTranslator.from(getSVNUrl(null)); SVNUrl parent = url.getParent(); if (parent != null) { // List the parent instead of fetching the folder explicitly. This // will save us a lot of calls since the list is cached. // String lastEntry = url.getLastPathSegment(); ISVNDirEntry[] dirEntries; try { dirEntries = innerListFolder(TypeTranslator.from(url.getParent()), monitor); } catch (CoreException e) { dirEntries = emptyFolder; } for (ISVNDirEntry dirEntry : dirEntries) if (dirEntry.getPath().equals(lastEntry)) return dirEntry; // Parent was not accessible. Perhaps we have no permissions. } SVNRevision revision = getRevision(); String key = GenericCache.cacheKey(TypeTranslator.from(url), getRevision()).intern(); synchronized (key) { // Check the cache. We use containsKey since it might have // valid null entries // if (getCache().dirContainsKey(key)) return getCache().getDir(key); Logger logger = CorePlugin.getLogger(); monitor.beginTask(null, 1); try { logger.debug("Obtaining remote folder %s[%s]", url, revision); //$NON-NLS-1$ ISVNDirEntry entry = getClientAdapter().getDirEntry(url, revision); getCache().putDir(key, entry); return entry; } catch (SVNClientException e) { if (SvnExceptionHandler.hasSvnException(e)) { logger.debug("Remote folder does not exist %s[%s]", url, revision); //$NON-NLS-1$ getCache().putDir(key, null); return null; } throw BuckminsterException.wrap(e); } } } @Override public ISvnEntryHelper<ISVNDirEntry> getSvnEntryHelper() { return HELPER; } @Override public SVNRevision getSVNRevision(long revision, Date timestamp) { if (revision == -1) { if (timestamp == null) return SVNRevision.HEAD; return new SVNRevision.DateSpec(timestamp); } if (timestamp != null) throw new IllegalArgumentException(org.eclipse.buckminster.subversion.Messages.svn_session_cannot_use_both_timestamp_and_revision_number); return new SVNRevision.Number(revision); } @Override public String toString() { try { return getSVNUrl(null).toString(); } catch (CoreException e) { return super.toString(); } } @Override protected void createRoots(Collection<RepositoryAccess> sourceRoots) throws CoreException { SVNRepositories repos = getRepositories(); for (RepositoryAccess root : sourceRoots) { Properties configuration = new Properties(); configuration.setProperty("url", root.getSvnURL().toString()); //$NON-NLS-1$ String user = root.getUser(); if (user != null) configuration.setProperty("user", user); //$NON-NLS-1$ String pwd = root.getPassword(); if (pwd != null) configuration.setProperty("password", pwd); //$NON-NLS-1$ try { final ISVNRepositoryLocation repoLocation = repos.createRepository(configuration); repos.addOrUpdateRepository(repoLocation); } catch (SVNException e) { // Repository already exists } } } @Override protected ISubversionCache<ISVNDirEntry> getCache(Map<UUID, Object> userCache) { assert (cache == null); final SvnCache svnCache = new SvnCache(); svnCache.initialize(userCache); return svnCache; } @Override protected ISVNDirEntry[] getEmptyEntryList() { return emptyFolder; } @Override protected String getRootUrl(ISVNRepositoryLocation location) { return location.getRepositoryRoot().toString(); } @Override protected String getUnknownRootPrefix() { return UNKNOWN_ROOT_PREFIX; } @Override protected void initializeSvn(RMContext context, URI ourRoot, ISVNRepositoryLocation bestMatch) throws CoreException { final SVNProviderPlugin plugin = getPlugin(); final ISVNClientAdapter client = getClientAdapter(); // Add the UnattendedPromptUserPassword callback only in case // the authentication data (at least the username) is actually // specified in the URL // ISVNPromptUserPassword pwCb = (username == null) ? plugin.getSvnPromptUserPassword() : new UnattendedPromptUserPassword(); if (pwCb != null) client.addPasswordCallback(pwCb); clientAdapter = client; if (bestMatch == null) addUnknownRoot(context.getBindingProperties(), new RepositoryAccess(ourRoot, username, password)); } @Override protected ISVNDirEntry[] innerListFolder(URI url, IProgressMonitor monitor) throws CoreException { monitor.beginTask(null, 1); try { return clientAdapter.getList(TypeTranslator.from(url), getRevision(), getRevision(), false); } catch (SVNClientException e) { throw BuckminsterException.wrap(e); } finally { monitor.worked(1); } } ISVNClientAdapter getClientAdapter() throws CoreException { if (clientAdapter == null) { final SVNClientManager clientManager = getPlugin().getSVNClientManager(); clientAdapter = Activator.getDefault().getClientAdapter(clientManager.getSvnClientInterface()); if (clientAdapter == null) clientAdapter = Activator.getDefault().getAnyClientAdapter(); if (clientAdapter == null) throw BuckminsterException.fromMessage(Messages.unable_to_load_default_svn_client); } return clientAdapter; } private SvnCache getCache() { return ((SvnCache) cache); } }