package org.eclipse.buckminster.subversion; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; 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.Trivial; import org.eclipse.buckminster.runtime.URLUtils; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.Path; import org.eclipse.osgi.util.NLS; public abstract class GenericSession<REPO_LOCATION_TYPE, SVN_ENTRY_TYPE, SVN_REVISION_TYPE> implements ISubversionSession<SVN_ENTRY_TYPE, SVN_REVISION_TYPE> { private static Collection<RepositoryAccess> getCommonRootsStep(Collection<RepositoryAccess> source) throws CoreException { Collection<RepositoryAccess> commonRoots = null; for (RepositoryAccess repoAccess : source) { URI url = repoAccess.getSvnURL(); String[] urlSegs = Path.fromPortableString(url.getPath()).segments(); for (RepositoryAccess repoAccessCmp : source) { if (repoAccess == repoAccessCmp) continue; URI cmp = repoAccessCmp.getSvnURL(); if (!(Trivial.equalsAllowNull(url.getHost(), cmp.getHost()) && Trivial.equalsAllowNull(url.getScheme(), cmp.getScheme()) && url .getPort() == cmp.getPort())) continue; String[] cmpSegs = Path.fromPortableString(cmp.getPath()).segments(); int maxSegs = urlSegs.length; if (maxSegs > cmpSegs.length) maxSegs = cmpSegs.length; int idx; for (idx = 0; idx < maxSegs; ++idx) if (!urlSegs[idx].equals(cmpSegs[idx])) break; if (idx < 1) continue; String user = repoAccess.getUser(); String cmpUser = repoAccessCmp.getUser(); if (user == null) user = cmpUser; else { if (!(cmpUser == null || user.equals(cmpUser))) continue; } String password = repoAccess.getPassword(); String cmpPassword = repoAccessCmp.getPassword(); if (password == null) password = cmpPassword; else { if (!(cmpPassword == null || password.equals(cmpPassword))) continue; } StringBuilder bld = new StringBuilder(); bld.append(url.getScheme()); bld.append("://"); //$NON-NLS-1$ if (url.getHost() != null) { bld.append(url.getHost()); if (url.getPort() != -1) { bld.append(":"); //$NON-NLS-1$ bld.append(url.getPort()); } } for (int pdx = 0; pdx < idx; ++pdx) { String seg = urlSegs[pdx]; bld.append('/'); if (idx > 0 && seg.equals("trunk") || seg.equals("tags") || seg.equals("branches")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ // // Assume that common root is above this folder // break; bld.append(seg); } try { if (commonRoots == null) commonRoots = new HashSet<RepositoryAccess>(); commonRoots.add(new RepositoryAccess(new URI(bld.toString()), user, password)); } catch (URISyntaxException e) { throw BuckminsterException.wrap(e); } } } if (commonRoots == null) // // No common roots found // return source; // Add all SVNUrl's for which we don't have a common root // Set<RepositoryAccess> rogueRoots = null; for (RepositoryAccess repoAccess : source) { boolean found = false; URI url = repoAccess.getSvnURL(); String[] urlSegs = Path.fromPortableString(url.getPath()).segments(); for (RepositoryAccess repoAccessCmp : commonRoots) { URI cmp = repoAccessCmp.getSvnURL(); if (!(Trivial.equalsAllowNull(url.getHost(), cmp.getHost()) && Trivial.equalsAllowNull(url.getScheme(), cmp.getScheme()) && url .getPort() == cmp.getPort())) continue; String[] cmpSegs = Path.fromPortableString(cmp.getPath()).segments(); int maxSegs = cmpSegs.length; if (maxSegs > urlSegs.length) continue; int idx; for (idx = 0; idx < maxSegs; ++idx) if (!urlSegs[idx].equals(cmpSegs[idx])) break; if (idx < maxSegs) continue; String user = repoAccess.getUser(); String cmpUser = repoAccessCmp.getUser(); if (!(user == null || cmpUser == null || user.equals(cmpUser))) continue; String password = repoAccess.getPassword(); String cmpPassword = repoAccessCmp.getPassword(); if (!(password == null || cmpPassword == null || password.equals(cmpPassword))) continue; found = true; break; } if (found) continue; if (rogueRoots == null) rogueRoots = new HashSet<RepositoryAccess>(); rogueRoots.add(repoAccess); } if (rogueRoots != null) commonRoots.addAll(rogueRoots); return commonRoots; } private static String getScheme(URI uri) { if (uri == null) throw new IllegalArgumentException(Messages.URI_can_not_be_null_at_this_point); String scheme = uri.getScheme(); if (scheme == null) { CorePlugin.getLogger().warning(NLS.bind(Messages.URI_0_has_no_scheme, uri)); return "file"; //$NON-NLS-1$ } return scheme.toLowerCase(Locale.ENGLISH); } protected final VersionSelector branchOrTag; protected REPO_LOCATION_TYPE repositoryLocation; protected final IPath module; protected final boolean moduleAfterBranch; protected final boolean moduleAfterTag; protected final boolean moduleBeforeBranch; protected final boolean moduleBeforeTag; protected final boolean trunkStructure; protected final String password; private final SVN_REVISION_TYPE revision; protected final IPath subModule; protected final String urlLeadIn; protected final String username; protected final ISubversionCache<SVN_ENTRY_TYPE> cache; public GenericSession(String repositoryURI, VersionSelector branchOrTag, long revision, Date timestamp, RMContext context) throws CoreException { this.revision = getSVNRevision(revision, timestamp); this.branchOrTag = branchOrTag; Map<UUID, Object> userCache = context.getUserCache(); this.cache = getCache(userCache); try { URI uri = new URI(repositoryURI); // Find the repository root, i.e. the point just above 'trunk'. // IPath fullPath = new Path(uri.getPath()); String[] pathSegments = fullPath.segments(); int idx = pathSegments.length; while (--idx >= 0) { String segment = pathSegments[idx]; if ("trunk".equals(segment) || "tags".equals(segment) || "branches".equals(segment)) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ break; } if (idx >= 0) this.trunkStructure = true; else { this.trunkStructure = false; idx = pathSegments.length - 1; // Last element is considered the // module name } if (branchOrTag != null && !trunkStructure) // // No use continuing with this session since there's no hope // finding // the desired branch or tag. // throw BuckminsterException.fromMessage(NLS.bind(Messages.branch_or_tag_0_not_found, branchOrTag)); int relPathLen = pathSegments.length - idx; StringBuilder bld = new StringBuilder(); String scheme = uri.getScheme(); if (scheme != null) { bld.append(scheme); bld.append("://"); //$NON-NLS-1$ } String _username = null; String _password = null; String authority = uri.getAuthority(); if (authority != null) { int atIdx = authority.indexOf('@'); if (atIdx > 0) { String authentication = authority.substring(0, atIdx); authority = authority.substring(atIdx + 1); int upSplit = authentication.indexOf(':'); if (upSplit > 0) { _username = authentication.substring(0, upSplit); if ("null".equals(_username)) //$NON-NLS-1$ _username = null; _password = authentication.substring(upSplit + 1); if ("null".equals(_password)) //$NON-NLS-1$ _password = null; } } bld.append(authority); } this.username = _username; this.password = _password; if (fullPath.getDevice() != null) bld.append('/'); bld.append(fullPath.removeLastSegments(relPathLen)); String _urlLeadIn = bld.toString(); // Anything after 'trunk' is considered a module path. The module // path // will be used when finding branches and tags. // IPath modulePath = null; if (trunkStructure) { if (relPathLen > 1) { modulePath = fullPath.removeFirstSegments(idx + 1); modulePath = modulePath.setDevice(null); } } else modulePath = Path.fromPortableString(fullPath.lastSegment()); this.module = modulePath; String tmp = uri.getFragment(); IPath _subModule = null; if (tmp != null) _subModule = new Path(tmp).makeRelative(); this.subModule = _subModule; boolean _moduleBeforeTag = false; boolean _moduleAfterTag = false; boolean _moduleBeforeBranch = false; boolean _moduleAfterBranch = false; if (trunkStructure) { for (String entry : URLUtils.decodeToQueryPairs(uri.getQuery())) { if (entry.equalsIgnoreCase("moduleBeforeTag")) //$NON-NLS-1$ _moduleBeforeTag = true; else if (entry.equalsIgnoreCase("moduleAfterTag")) //$NON-NLS-1$ _moduleAfterTag = true; else if (entry.equalsIgnoreCase("moduleBeforeBranch")) //$NON-NLS-1$ _moduleBeforeBranch = true; else if (entry.equalsIgnoreCase("moduleAfterBranch")) //$NON-NLS-1$ _moduleAfterBranch = true; } } this.moduleBeforeTag = _moduleBeforeTag; this.moduleAfterTag = _moduleAfterTag; this.moduleBeforeBranch = _moduleBeforeBranch; this.moduleAfterBranch = _moduleAfterBranch; // Let's see if our SVNRootUrl matches any of the known // repositories. // int rank = 0; final URI ourRoot = new URI(_urlLeadIn); final String ourProto = getScheme(ourRoot); REPO_LOCATION_TYPE bestMatch = null; for (REPO_LOCATION_TYPE location : getKnownRepositories()) { URI repoRoot = new URI(getRootUrl(location)); if (!Trivial.equalsAllowNull(repoRoot.getHost(), ourRoot.getHost())) continue; String repoProto = getScheme(repoRoot); // We let the protocol svn or http match a repo that uses // svn+ssh or https // boolean repoIsSSH = repoProto.equals("svn+ssh") || repoProto.equals("https"); //$NON-NLS-1$ //$NON-NLS-2$ if (rank > 200 && !repoIsSSH) continue; if (!(repoProto.equals(ourProto) || (repoProto.equals("svn") && ourProto.equals("http")) //$NON-NLS-1$ //$NON-NLS-2$ || (repoProto.equals("http") && ourProto.equals("svn")) //$NON-NLS-1$ //$NON-NLS-2$ || ((ourProto.equals("svn") || ourProto.equals("http")) && repoIsSSH))) //$NON-NLS-1$ //$NON-NLS-2$ continue; String[] ourPath = Path.fromPortableString(ourRoot.getPath()).segments(); String[] repoPath = Path.fromPortableString(repoRoot.getPath()).segments(); idx = repoPath.length; final int top = ourPath.length; if (idx > top) // // repoPath is too qualified for our needs // continue; while (--idx >= 0) if (!ourPath[idx].equals(repoPath[idx])) break; if (idx >= 0) // // repoPath is not a prefix of ourPath // continue; _urlLeadIn = repoRoot.toString(); int diff = top - repoPath.length; if (diff > 0) { int myRank = (repoIsSSH ? 400 : 200) - diff; if (rank > myRank) continue; // Append the rest of our path // bld.setLength(0); bld.append(_urlLeadIn); for (idx = repoPath.length; idx < top; ++idx) { bld.append('/'); bld.append(ourPath[idx]); } _urlLeadIn = bld.toString(); } rank = (repoIsSSH ? 400 : 200) - diff; bestMatch = location; if (rank == 400) break; } this.urlLeadIn = _urlLeadIn; repositoryLocation = bestMatch; synchronized (userCache) { initializeSvn(context, ourRoot, bestMatch); } } catch (URISyntaxException e) { throw BuckminsterException.wrap(e); } } @Override final public void createCommonRoots(RMContext context) throws CoreException { final List<RepositoryAccess> unknownRoots = getUnknownRoots(context.getBindingProperties()); if (unknownRoots.size() == 0) return; Collection<RepositoryAccess> sourceRoots = unknownRoots; if (unknownRoots.size() > 1) { // Get all common roots with a segment count of at least 1 // for (;;) { Collection<RepositoryAccess> commonRoots = getCommonRootsStep(sourceRoots); if (commonRoots == sourceRoots) break; // Common roots were found. Iterate again to find commons // amongst the commons // sourceRoots = commonRoots; } } // Create the needed repositories so that Subclipse doesn't create every // single // root for us. // createRoots(sourceRoots); clearUnknownRoots(context.getBindingProperties()); } final public REPO_LOCATION_TYPE getRepositoryLocation() { return repositoryLocation; } @Override final public SVN_REVISION_TYPE getRevision() { return revision; } public abstract SVN_REVISION_TYPE getSVNRevision(long rev, Date timestamp); /** * Returns the directory where it's expected to find a list of branches or * tags. * * @param branches * true if branches, false if tags. * @return The SVNUrl appointing the branches or tags directory. * @throws MalformedURLException */ @Override final public URI getSVNRootUrl(boolean branches) throws CoreException { StringBuilder bld = new StringBuilder(); bld.append(urlLeadIn); if (branches) { bld.append("/branches"); //$NON-NLS-1$ if (moduleBeforeBranch && module != null) { bld.append('/'); bld.append(module); } } else { bld.append("/tags"); //$NON-NLS-1$ if (moduleBeforeTag && module != null) { bld.append('/'); bld.append(module); } } try { return new URI(bld.toString()); } catch (URISyntaxException e) { throw BuckminsterException.wrap(e); } } @Override final public URI getSVNUrl() throws CoreException { return getSVNUrl(null); } @Override final public URI getSVNUrl(String fileName) throws CoreException { StringBuilder bld = new StringBuilder(); bld.append(urlLeadIn); if (branchOrTag == null) { if (trunkStructure) bld.append("/trunk"); //$NON-NLS-1$ if (module != null) { bld.append('/'); bld.append(module); } } else if (branchOrTag.getType() == VersionSelector.BRANCH) { bld.append("/branches"); //$NON-NLS-1$ if (moduleBeforeBranch && module != null) { bld.append('/'); bld.append(module); } bld.append('/'); bld.append(branchOrTag.getName()); if (moduleAfterBranch && module != null) { bld.append('/'); bld.append(module); } } else { bld.append("/tags"); //$NON-NLS-1$ if (moduleBeforeTag && module != null) { bld.append('/'); bld.append(module); } bld.append('/'); bld.append(branchOrTag.getName()); if (moduleAfterTag && module != null) { bld.append('/'); bld.append(module); } } if (subModule != null) { bld.append('/'); bld.append(subModule); } if (fileName != null) { bld.append('/'); bld.append(fileName); } try { return new URI(bld.toString()); } catch (URISyntaxException e) { throw BuckminsterException.wrap(e); } } @Override final public boolean hasTrunkStructure() { return trunkStructure; } @Override final public SVN_ENTRY_TYPE[] listFolder(URI url, 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 // String key = GenericCache.cacheKey(url, getRevision()).intern(); synchronized (key) { SVN_ENTRY_TYPE[] list = cache.get(key); if (list != null) return list; Logger logger = CorePlugin.getLogger(); try { logger.debug("Listing remote folder %s", key); //$NON-NLS-1$ list = innerListFolder(url, monitor); if (list == null || list.length == 0) { logger.debug("Remote folder had no entries %s", key); //$NON-NLS-1$ list = getEmptyEntryList(); } cache.put(key, list); return list; } catch (Exception e) { if (SvnExceptionHandler.hasSvnException(e)) { logger.debug(Messages.remote_folder_does_not_exist_0, key); return getEmptyEntryList(); } throw BuckminsterException.wrap(e); } finally { monitor.done(); } } } final protected void addUnknownRoot(Map<String, String> properties, RepositoryAccess ra) { synchronized (properties) { final String unknownRootPrefix = getUnknownRootPrefix(); int maxNum = -1; final String raStr = ra.toString(); for (Map.Entry<String, String> entries : properties.entrySet()) { String key = entries.getKey(); if (key.startsWith(unknownRootPrefix)) { int lastDot = key.lastIndexOf('.'); if (lastDot < 0) continue; try { int keyNum = Integer.parseInt(key.substring(lastDot + 1)); if (maxNum < keyNum) maxNum = keyNum; } catch (NumberFormatException e) { continue; } if (entries.getValue().equals(raStr)) // // Entry is already present. Don't recreate // return; } } properties.put(unknownRootPrefix + (maxNum + 1), raStr); } } final protected void clearUnknownRoots(Map<String, String> properties) { synchronized (properties) { final Iterator<String> keys = properties.keySet().iterator(); while (keys.hasNext()) { String key = keys.next(); if (key.startsWith(getUnknownRootPrefix())) keys.remove(); } } } abstract protected void createRoots(Collection<RepositoryAccess> sourceRoots) throws CoreException; abstract protected ISubversionCache<SVN_ENTRY_TYPE> getCache(Map<UUID, Object> userCache); abstract protected SVN_ENTRY_TYPE[] getEmptyEntryList(); abstract protected REPO_LOCATION_TYPE[] getKnownRepositories() throws CoreException; abstract protected String getRootUrl(REPO_LOCATION_TYPE location); abstract protected String getUnknownRootPrefix(); final protected List<RepositoryAccess> getUnknownRoots(Map<String, String> properties) { synchronized (properties) { List<RepositoryAccess> unknownRoots = null; for (Map.Entry<String, String> entries : properties.entrySet()) { String key = entries.getKey(); if (key.startsWith(getUnknownRootPrefix())) { RepositoryAccess ra; try { ra = new RepositoryAccess(entries.getValue()); } catch (URISyntaxException e) { // Bogus entry continue; } if (unknownRoots == null) unknownRoots = new ArrayList<RepositoryAccess>(); unknownRoots.add(ra); } } if (unknownRoots == null) unknownRoots = Collections.emptyList(); return unknownRoots; } } abstract protected void initializeSvn(RMContext context, URI ourRoot, REPO_LOCATION_TYPE bestMatch) throws CoreException; abstract protected SVN_ENTRY_TYPE[] innerListFolder(URI url, IProgressMonitor monitor) throws Exception; }