Java tutorial
/* * The MIT License * * Copyright (c) 2013, CloudBees, Inc., Stephen Connolly. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package jenkins.scm.impl.subversion; import com.cloudbees.jenkins.plugins.sshcredentials.SSHUserPrivateKey; import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsMatchers; import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import com.cloudbees.plugins.credentials.domains.DomainRequirement; import com.cloudbees.plugins.credentials.domains.URIRequirementBuilder; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; import hudson.Functions; import hudson.Util; import hudson.model.Item; import hudson.model.TaskListener; import hudson.scm.CredentialsSVNAuthenticationProviderImpl; import hudson.scm.FilterSVNAuthenticationManager; import hudson.scm.SubversionRepositoryStatus; import hudson.scm.SubversionSCM; import hudson.scm.subversion.SvnHelper; import hudson.security.ACL; import hudson.util.EditDistance; import hudson.util.FormValidation; import hudson.util.ListBoxModel; import jenkins.scm.api.SCMHead; import jenkins.scm.api.SCMHeadObserver; import jenkins.scm.api.SCMRevision; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceCriteria; import jenkins.scm.api.SCMSourceDescriptor; import jenkins.scm.api.SCMSourceOwner; import jenkins.scm.api.SCMSourceOwners; import net.jcip.annotations.GuardedBy; import org.acegisecurity.Authentication; import org.acegisecurity.context.SecurityContextHolder; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOCase; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.AncestorInPath; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.tmatesoft.svn.core.SVNDirEntry; import org.tmatesoft.svn.core.SVNException; import org.tmatesoft.svn.core.SVNNodeKind; import org.tmatesoft.svn.core.SVNURL; import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager; import org.tmatesoft.svn.core.internal.util.SVNPathUtil; import org.tmatesoft.svn.core.io.ISVNSession; import org.tmatesoft.svn.core.io.SVNRepository; import org.tmatesoft.svn.core.io.SVNRepositoryFactory; import org.tmatesoft.svn.core.wc.SVNRevision; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.TreeSet; import java.util.UUID; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import java.util.regex.Pattern; /** * A {@link SCMSource} for Subversion. * * @author Stephen Connolly */ public class SubversionSCMSource extends SCMSource { private static final String DEFAULT_INCLUDES = "trunk,branches/*,tags/*,sandbox/*"; private static final String DEFAULT_EXCLUDES = ""; public static final StringListComparator COMPARATOR = new StringListComparator(); public static final Logger LOGGER = Logger.getLogger(SubversionSCMSource.class.getName()); private final String remoteBase; private final String credentialsId; private final String includes; private final String excludes; @GuardedBy("this") private transient String uuid; @DataBoundConstructor @SuppressWarnings("unused") // by stapler public SubversionSCMSource(String id, String remoteBase, String credentialsId, String includes, String excludes) { super(id); this.remoteBase = StringUtils.removeEnd(remoteBase, "/") + "/"; this.credentialsId = credentialsId; this.includes = StringUtils.defaultIfEmpty(includes, DEFAULT_INCLUDES); this.excludes = StringUtils.defaultIfEmpty(excludes, DEFAULT_EXCLUDES); } /** * Gets the credentials id. * * @return the credentials id. */ @SuppressWarnings("unused") // by stapler public String getCredentialsId() { return credentialsId; } /** * Gets the comma separated list of exclusions. * * @return the comma separated list of exclusions. */ @SuppressWarnings("unused") // by stapler public String getExcludes() { return excludes; } /** * Gets the comma separated list of inclusions. * * @return the comma separated list of inclusions. */ @SuppressWarnings("unused") // by stapler public String getIncludes() { return includes; } /** * Gets the base SVN URL of the project. * * @return the base SVN URL of the project. */ @SuppressWarnings("unused") // by stapler public String getRemoteBase() { return remoteBase; } @CheckForNull public synchronized String getUuid() { if (uuid == null) { SVNRepositoryView repository = null; try { SVNURL repoURL = SVNURL.parseURIEncoded(remoteBase); repository = openSession(repoURL); uuid = repository.getUuid(); } catch (SVNException e) { LOGGER.log(Level.WARNING, "Could not connect to remote repository " + remoteBase + " to determine UUID", e); } catch (IOException e) { LOGGER.log(Level.WARNING, "Could not connect to remote repository " + remoteBase + " to determine UUID", e); } finally { closeSession(repository); } } return uuid; } /** * {@inheritDoc} */ @NonNull @Override protected void retrieve(@NonNull final SCMHeadObserver observer, @NonNull TaskListener listener) throws IOException { SVNRepositoryView repository = null; try { listener.getLogger().println("Opening conection to " + remoteBase); SVNURL repoURL = SVNURL.parseURIEncoded(remoteBase); repository = openSession(repoURL); String repoPath = SubversionSCM.DescriptorImpl.getRelativePath(repoURL, repository.getRepository()); List<String> prefix = Collections.emptyList(); fetch(listener, repository, -1, repoPath, toPaths(splitCludes(includes)), prefix, prefix, toPaths(splitCludes(excludes)), getCriteria(), observer); } catch (SVNException e) { e.printStackTrace(listener.error("Could not communicate with Subversion server")); throw new IOException(e); } finally { closeSession(repository); } } /** * {@inheritDoc} */ @Override protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener listener) throws IOException { SVNRepositoryView repository = null; try { listener.getLogger().println("Opening connection to " + remoteBase); SVNURL repoURL = SVNURL.parseURIEncoded(remoteBase); repository = openSession(repoURL); String repoPath = SubversionSCM.DescriptorImpl.getRelativePath(repoURL, repository.getRepository()); String path = SVNPathUtil.append(repoPath, head.getName()); SVNRepositoryView.NodeEntry svnEntry = repository.getNode(path, -1); return new SCMRevisionImpl(head, svnEntry.getRevision()); } catch (SVNException e) { throw new IOException(e); } finally { closeSession(repository); } } private static void closeSession(@CheckForNull SVNRepositoryView repository) { if (repository != null) { repository.close(); } } private SVNRepositoryView openSession(SVNURL repoURL) throws SVNException, IOException { return new SVNRepositoryView(repoURL, credentialsId == null ? null : CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials(StandardCredentials.class, getOwner(), ACL.SYSTEM, URIRequirementBuilder.fromUri(repoURL.toString()).build()), CredentialsMatchers.allOf(CredentialsMatchers.withId(credentialsId), CredentialsMatchers.anyOf( CredentialsMatchers.instanceOf(StandardCredentials.class), CredentialsMatchers.instanceOf(SSHUserPrivateKey.class))))); } void fetch(@NonNull TaskListener listener, @NonNull final SVNRepositoryView repository, long rev, @NonNull final String repoPath, @NonNull SortedSet<List<String>> paths, @NonNull List<String> prefix, @NonNull List<String> realPath, @NonNull SortedSet<List<String>> excludedPaths, @CheckForNull SCMSourceCriteria branchCriteria, @NonNull SCMHeadObserver observer) throws IOException, SVNException { String svnPath = SVNPathUtil.append(repoPath, StringUtils.join(realPath, '/')); assert prefix.size() == realPath.size(); assert wildcardStartsWith(realPath, prefix); SortedMap<List<String>, SortedSet<List<String>>> includePaths = groupPaths(paths, prefix); listener.getLogger().println("Checking directory " + svnPath + (rev > -1 ? "@" + rev : "@HEAD")); SVNRepositoryView.NodeEntry node = repository.getNode(svnPath, rev); if (!SVNNodeKind.DIR.equals(node.getType()) || node.getChildren() == null) { return; } for (Map.Entry<List<String>, SortedSet<List<String>>> entry : includePaths.entrySet()) { for (List<String> path : entry.getValue()) { String name = path.get(prefix.size()); SVNRepositoryView.ChildEntry[] children = node.getChildren().clone(); Arrays.sort(children, new Comparator<SVNRepositoryView.ChildEntry>() { public int compare(SVNRepositoryView.ChildEntry o1, SVNRepositoryView.ChildEntry o2) { long diff = o2.getRevision() - o1.getRevision(); return diff < 0 ? -1 : diff > 0 ? 1 : 0; } }); for (final SVNRepositoryView.ChildEntry svnEntry : children) { if (svnEntry.getType() == SVNNodeKind.DIR && isMatch(svnEntry.getName(), name)) { List<String> childPrefix = copyAndAppend(prefix, name); List<String> childRealPath = copyAndAppend(realPath, svnEntry.getName()); if (wildcardStartsWith(childRealPath, excludedPaths)) { continue; } if (path.equals(childPrefix)) { final String childPath = StringUtils.join(childRealPath, '/'); final String candidateRootPath = SVNPathUtil.append(repoPath, childPath); final long candidateRevision = svnEntry.getRevision(); final long lastModified = svnEntry.getLastModified(); listener.getLogger().println( "Checking candidate branch " + candidateRootPath + "@" + candidateRevision); if (branchCriteria == null || branchCriteria.isHead(new SCMSourceCriteria.Probe() { @Override public String name() { return childPath; } @Override public long lastModified() { return lastModified; } @Override public boolean exists(@NonNull String path) throws IOException { try { return repository.checkPath(SVNPathUtil.append(candidateRootPath, path), candidateRevision) != SVNNodeKind.NONE; } catch (SVNException e) { throw new IOException(e); } } }, listener)) { listener.getLogger().println("Met criteria"); SCMHead head = new SCMHead(childPath); observer.observe(head, new SCMRevisionImpl(head, svnEntry.getRevision())); if (!observer.isObserving()) { return; } } else { listener.getLogger().println("Does not meet criteria"); } } else { fetch(listener, repository, svnEntry.getRevision(), repoPath, paths, childPrefix, childRealPath, excludedPaths, branchCriteria, observer); } } } } } } /** * Copies a list and appends some more values. * * @param list the list. * @param values the values to append. * @param <T> the type of values to append. * @return the list. */ @NonNull private static <T> List<T> copyAndAppend(@NonNull List<T> list, T... values) { List<T> childPrefix = new ArrayList<T>(list.size() + values.length); childPrefix.addAll(list); childPrefix.addAll(Arrays.asList(values)); return childPrefix; } /** * Groups a set of path segments based on a supplied prefix. * * @param pathSegments the input path segments. * @param prefix the prefix to group on. * @return a map, all keys will {@link #startsWith(java.util.List, java.util.List)} the input prefix and be longer * than the input prefix, all values will {@link #startsWith(java.util.List, * java.util.List)} their corresponding key. */ @NonNull static SortedMap<List<String>, SortedSet<List<String>>> groupPaths( @NonNull SortedSet<List<String>> pathSegments, @NonNull List<String> prefix) { // ensure pre-condition is valid and ensure we are using a copy pathSegments = filterPaths(pathSegments, prefix); SortedMap<List<String>, SortedSet<List<String>>> result = new TreeMap<List<String>, SortedSet<List<String>>>( COMPARATOR); while (!pathSegments.isEmpty()) { List<String> longestPrefix = null; int longestIndex = -1; for (List<String> pathSegment : pathSegments) { if (longestPrefix == null) { longestPrefix = pathSegment; longestIndex = indexOfNextWildcard(pathSegment, prefix.size()); } else { int index = indexOfNextWildcard(pathSegment, prefix.size()); if (index > longestIndex) { longestPrefix = pathSegment; longestIndex = index; } } } assert longestPrefix != null; longestPrefix = new ArrayList<String>(longestPrefix.subList(0, longestIndex)); SortedSet<List<String>> group = filterPaths(pathSegments, longestPrefix); result.put(longestPrefix, group); pathSegments.removeAll(group); } String optimization; while (null != (optimization = getOptimizationPoint(result.keySet(), prefix.size()))) { List<String> optimizedPrefix = copyAndAppend(prefix, optimization); SortedSet<List<String>> optimizedGroup = new TreeSet<List<String>>(COMPARATOR); for (Iterator<Map.Entry<List<String>, SortedSet<List<String>>>> iterator = result.entrySet() .iterator(); iterator.hasNext();) { Map.Entry<List<String>, SortedSet<List<String>>> entry = iterator.next(); if (startsWith(entry.getKey(), optimizedPrefix)) { iterator.remove(); optimizedGroup.addAll(entry.getValue()); } } result.put(optimizedPrefix, optimizedGroup); } return result; } /** * Returns the string that has multiple matches and can therefore be used to optimize the set of prefixes. * * @param newPrefixes the proposed set of prefixes. * @param oldPrefixSize the length of the old prefix. * @return either a string that when appended to the old prefix will give a prefix with multiple matches in the * supplied set of new prefixes or {@code null} if there is no such string. */ @CheckForNull static String getOptimizationPoint(@NonNull Set<List<String>> newPrefixes, int oldPrefixSize) { Set<String> set = new HashSet<String>(); for (List<String> p : newPrefixes) { if (p.size() <= oldPrefixSize) { continue; } String value = p.get(oldPrefixSize); if (set.contains(value)) { return value; } set.add(value); } return null; } /** * Returns the index of the next wildcard segment on or after the specified start index. * * @param pathSegment the path segments. * @param startIndex the first index to check. * @return the index with a wildcard or {@code -1} if none exist. */ static int indexOfNextWildcard(@NonNull List<String> pathSegment, int startIndex) { int index = startIndex; ListIterator<String> i = pathSegment.listIterator(index); while (i.hasNext()) { String segment = i.next(); if (segment.indexOf('*') != -1 || segment.indexOf('?') != -1) { break; } index++; } return index; } /** * Returns {@code true} if and only if the value starts with the supplied prefix. * * @param value the value. * @param prefix the candidate prefix. * @return {@code true} if and only if the value starts with the supplied prefix. */ static boolean startsWith(@NonNull List<String> value, @NonNull List<String> prefix) { if (value.size() < prefix.size()) { return false; } ListIterator<String> i1 = value.listIterator(); ListIterator<String> i2 = prefix.listIterator(); while (i1.hasNext() && i2.hasNext()) { if (!i1.next().equals(i2.next())) { return false; } } return true; } /** * Returns {@code true} if and only if the value starts with the supplied prefix. * * @param value the value. * @param wildcardPrefix the candidate prefix. * @return {@code true} if and only if the value starts with the supplied prefix. */ static boolean wildcardStartsWith(@NonNull List<String> value, @NonNull List<String> wildcardPrefix) { if (value.size() < wildcardPrefix.size()) { return false; } ListIterator<String> i1 = value.listIterator(); ListIterator<String> i2 = wildcardPrefix.listIterator(); while (i1.hasNext() && i2.hasNext()) { if (!isMatch(i1.next(), i2.next())) { return false; } } return true; } /** * Returns {@code true} if and only if the value starts with any of the supplied prefixes. * * @param value the value. * @param wildcardPrefixes the candidate prefixes. * @return {@code true} if and only if the value starts with any of the supplied prefixes. */ static boolean wildcardStartsWith(@NonNull List<String> value, @NonNull Collection<List<String>> wildcardPrefixes) { for (List<String> wildcardPrefix : wildcardPrefixes) { if (wildcardStartsWith(value, wildcardPrefix)) { return true; } } return false; } /** * Filters the set of path segments, retaining only those that start with the supplied prefix. * * @param pathSegments the set of path segments. * @param prefix the prefix. * @return a new set of path segments. */ @NonNull static SortedSet<List<String>> filterPaths(@NonNull SortedSet<List<String>> pathSegments, @NonNull List<String> prefix) { SortedSet<List<String>> result = new TreeSet<List<String>>(COMPARATOR); for (List<String> pathSegment : pathSegments) { if (startsWith(pathSegment, prefix)) { result.add(pathSegment); } } return result; } /** * Splits a set of path strings into a set of lists of path segments. * * @param pathStrings the set of path strings. * @return the set of lists of path segments. */ @NonNull static SortedSet<List<String>> toPaths(@NonNull SortedSet<String> pathStrings) { SortedSet<List<String>> result = new TreeSet<List<String>>(COMPARATOR); for (String clude : pathStrings) { result.add(Arrays.asList(clude.split("/"))); } return result; } /** * Split a comma separated set of includes/excludes into a set of strings. * * @param cludes a comma separated set of includes/excludes. * @return a set of strings. */ @NonNull static SortedSet<String> splitCludes(@CheckForNull String cludes) { TreeSet<String> result = new TreeSet<String>(); StringTokenizer tokenizer = new StringTokenizer(StringUtils.defaultString(cludes), ","); while (tokenizer.hasMoreTokens()) { String clude = tokenizer.nextToken().trim(); if (StringUtils.isNotEmpty(clude)) { result.add(clude.trim()); } } return result; } /** * Checks if we have a match against a wildcard matcher. * * @param value the value * @param wildcareMatcher the wildcard matcher * @return {@code true} if and only if the value matches the wildcard matcher. */ static boolean isMatch(@NonNull String value, @NonNull String wildcareMatcher) { return FilenameUtils.wildcardMatch(value, wildcareMatcher, IOCase.SENSITIVE); } /** * {@inheritDoc} */ @NonNull @Override public SubversionSCM build(@NonNull SCMHead head, @CheckForNull SCMRevision revision) { if (revision != null && !head.equals(revision.getHead())) { revision = null; } if (revision != null && !(revision instanceof SCMRevisionImpl)) { revision = null; } StringBuilder remote = new StringBuilder(remoteBase); if (!remoteBase.endsWith("/")) { remote.append('/'); } remote.append(head.getName()); if (revision != null) { remote.append('@').append(((SCMRevisionImpl) revision).getRevision()); } else if (remote.indexOf("@") != -1) { // name contains an @ so need to ensure there is an @ at the end of the name remote.append('@'); } return new SubversionSCM(remote.toString(), credentialsId, "."); } /** * Our implementation. */ public static class SCMRevisionImpl extends SCMRevision { /** * The subversion revision. */ private long revision; public SCMRevisionImpl(SCMHead head, long revision) { super(head); this.revision = revision; } public long getRevision() { return revision; } /** * {@inheritDoc} */ @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } SCMRevisionImpl that = (SCMRevisionImpl) o; return revision == that.revision && getHead().equals(that.getHead()); } /** * {@inheritDoc} */ @Override public int hashCode() { return (int) (revision ^ (revision >>> 32)); } } static class StringListComparator implements Comparator<List<String>> { public int compare(List<String> o1, List<String> o2) { ListIterator<String> e1 = o1.listIterator(); ListIterator<String> e2 = o2.listIterator(); while (e1.hasNext() && e2.hasNext()) { String s1 = e1.next(); String s2 = e2.next(); int rv = s1.compareTo(s2); if (rv != 0) { return rv; } } if (e1.hasNext()) { return -1; } if (e2.hasNext()) { return 1; } return 0; } } @Extension @SuppressWarnings("unused") // by jenkins public static class DescriptorImpl extends SCMSourceDescriptor { static final Pattern URL_PATTERN = Pattern.compile("(https?|svn(\\+[a-z0-9]+)?|file)://.+"); /** * {@inheritDoc} */ @Override public String getDisplayName() { return Messages.SubversionSCMSource_DisplayName(); } /** * Stapler helper method. * * @param context the context. * @param remoteBase the remote base. * @return list box model. */ @SuppressWarnings("unused") // by stapler public ListBoxModel doFillCredentialsIdItems(@AncestorInPath SCMSourceOwner context, @QueryParameter String remoteBase) { if (context == null || !context.hasPermission(Item.EXTENDED_READ)) { return new StandardListBoxModel(); } List<DomainRequirement> domainRequirements; domainRequirements = URIRequirementBuilder.fromUri(remoteBase.trim()).build(); return new StandardListBoxModel().withEmptySelection().withMatching( CredentialsMatchers.anyOf( CredentialsMatchers.instanceOf(StandardUsernamePasswordCredentials.class), CredentialsMatchers.instanceOf(StandardCertificateCredentials.class), CredentialsMatchers.instanceOf(SSHUserPrivateKey.class)), CredentialsProvider.lookupCredentials(StandardCredentials.class, context, ACL.SYSTEM, domainRequirements)); } /** * validate the value for a remote (repository) location. */ public FormValidation doCheckCredentialsId(StaplerRequest req, @AncestorInPath SCMSourceOwner context, @QueryParameter String remoteBase, @QueryParameter String value) { // TODO suspiciously similar to SubversionSCM.ModuleLocation.DescriptorImpl.checkCredentialsId; refactor into shared method? // Test the connection only if we may use the credentials if (context == null || !context.hasPermission(CredentialsProvider.USE_ITEM)) { return FormValidation.ok(); } // if check remote is reporting an issue then we don't need to String url = Util.fixEmptyAndTrim(remoteBase); if (url == null) return FormValidation.ok(); if (!URL_PATTERN.matcher(url).matches()) return FormValidation.ok(); try { String urlWithoutRevision = SvnHelper.getUrlWithoutRevision(url); SVNURL repoURL = SVNURL.parseURIDecoded(urlWithoutRevision); StandardCredentials credentials = value == null ? null : CredentialsMatchers.firstOrNull( CredentialsProvider.lookupCredentials(StandardCredentials.class, context, ACL.SYSTEM, URIRequirementBuilder.fromUri(repoURL.toString()).build()), CredentialsMatchers.withId(value)); if (checkRepositoryPath(context, repoURL, credentials) != SVNNodeKind.NONE) { // something exists; now check revision if any SVNRevision revision = getRevisionFromRemoteUrl(url); if (revision != null && !revision.isValid()) { return FormValidation.errorWithMarkup( hudson.scm.subversion.Messages.SubversionSCM_doCheckRemote_invalidRevision()); } return FormValidation.ok(); } SVNRepository repository = null; try { repository = getRepository(context, repoURL, credentials, Collections.<String, Credentials>emptyMap(), null); long rev = repository.getLatestRevision(); // now go back the tree and find if there's anything that exists String repoPath = getRelativePath(repoURL, repository); String p = repoPath; while (p.length() > 0) { p = SVNPathUtil.removeTail(p); if (repository.checkPath(p, rev) == SVNNodeKind.DIR) { // found a matching path List<SVNDirEntry> entries = new ArrayList<SVNDirEntry>(); repository.getDir(p, rev, false, entries); // build up the name list List<String> paths = new ArrayList<String>(); for (SVNDirEntry e : entries) if (e.getKind() == SVNNodeKind.DIR) paths.add(e.getName()); String head = SVNPathUtil.head(repoPath.substring(p.length() + 1)); String candidate = EditDistance.findNearest(head, paths); return FormValidation.error( hudson.scm.subversion.Messages.SubversionSCM_doCheckRemote_badPathSuggest(p, head, candidate != null ? "/" + candidate : "")); } } return FormValidation .error(hudson.scm.subversion.Messages.SubversionSCM_doCheckRemote_badPath(repoPath)); } finally { if (repository != null) repository.closeSession(); } } catch (SVNException e) { LOGGER.log(Level.INFO, "Failed to access subversion repository " + url, e); String message = hudson.scm.subversion.Messages.SubversionSCM_doCheckRemote_exceptionMsg1( Util.escape(url), Util.escape(e.getErrorMessage().getFullMessage()), "javascript:document.getElementById('svnerror').style.display='block';" + "document.getElementById('svnerrorlink').style.display='none';" + "return false;") + "<br/><pre id=\"svnerror\" style=\"display:none\">" + Functions.printThrowable(e) + "</pre>"; return FormValidation.errorWithMarkup(message); } } public SVNNodeKind checkRepositoryPath(SCMSourceOwner context, SVNURL repoURL, StandardCredentials credentials) throws SVNException { SVNRepository repository = null; try { repository = getRepository(context, repoURL, credentials, Collections.<String, Credentials>emptyMap(), null); repository.testConnection(); long rev = repository.getLatestRevision(); String repoPath = getRelativePath(repoURL, repository); return repository.checkPath(repoPath, rev); } catch (SVNException e) { if (LOGGER.isLoggable(Level.FINE)) { LogRecord lr = new LogRecord(Level.FINE, "Could not check repository path {0} using credentials {1} ({2})"); lr.setThrown(e); lr.setParameters(new Object[] { repoURL, credentials == null ? null : CredentialsNameProvider.name(credentials), credentials }); LOGGER.log(lr); } throw e; } finally { if (repository != null) repository.closeSession(); } } protected SVNRepository getRepository(SCMSourceOwner context, SVNURL repoURL, StandardCredentials credentials, Map<String, Credentials> additionalCredentials, ISVNSession session) throws SVNException { SVNRepository repository = SVNRepositoryFactory.create(repoURL, session); ISVNAuthenticationManager sam = SubversionSCM.createSvnAuthenticationManager( new CredentialsSVNAuthenticationProviderImpl(credentials, additionalCredentials)); sam = new FilterSVNAuthenticationManager(sam) { // If there's no time out, the blocking read operation may hang forever, because TCP itself // has no timeout. So always use some time out. If the underlying implementation gives us some // value (which may come from ~/.subversion), honor that, as long as it sets some timeout value. @Override public int getReadTimeout(SVNRepository repository) { int r = super.getReadTimeout(repository); if (r <= 0) r = SubversionSCM.DEFAULT_TIMEOUT; return r; } }; repository.setTunnelProvider(SubversionSCM.createDefaultSVNOptions()); repository.setAuthenticationManager(sam); return repository; } public static String getRelativePath(SVNURL repoURL, SVNRepository repository) throws SVNException { String repoPath = repoURL.getPath().substring(repository.getRepositoryRoot(true).getPath().length()); if (!repoPath.startsWith("/")) repoPath = "/" + repoPath; return repoPath; } /** * Gets the revision from a remote URL - i.e. the part after '@' if any * * @return the revision or null */ private static SVNRevision getRevisionFromRemoteUrl(String remoteUrlPossiblyWithRevision) { int idx = remoteUrlPossiblyWithRevision.lastIndexOf('@'); int slashIdx = remoteUrlPossiblyWithRevision.lastIndexOf('/'); if (idx > 0 && idx > slashIdx) { String n = remoteUrlPossiblyWithRevision.substring(idx + 1); return SVNRevision.parse(n); } return null; } } /** * We need to listen out for post-commit hooks */ @Extension @SuppressWarnings("unused") // instantiated by Jenkins public static class ListenerImpl extends SubversionRepositoryStatus.Listener { /** * Maximum number of repositories to retain... since we should only ever have 1-2 relevant, this size * shouldn't matter much, but keep it finite to prevent memory stealing. */ public static final int RECENT_SIZE = 64; /** * Guard against repeated calls by poorly configured hook scripts. */ @GuardedBy("itself") private final Map<String, Long> recentUpdates = new LinkedHashMap<String, Long>(RECENT_SIZE) { @Override protected boolean removeEldestEntry(Map.Entry<String, Long> eldest) { return size() >= RECENT_SIZE; } }; /** * {@inheritDoc} */ @Override public boolean onNotify(UUID uuid, long revision, Set<String> paths) { final String id = uuid.toString(); synchronized (recentUpdates) { Long recentUpdate = recentUpdates.get(id); if (recentUpdate != null && revision == recentUpdate) { LOGGER.log(Level.FINE, "Received duplicate post-commit hook from {0} for revision {1} on paths {2}", new Object[] { uuid, revision, paths }); return false; } recentUpdates.put(id, revision); } LOGGER.log(Level.INFO, "Received post-commit hook from {0} for revision {1} on paths {2}", new Object[] { uuid, revision, paths }); boolean notified = false; // run in high privilege to see all the projects anonymous users don't see. // this is safe because when we actually schedule a build, it's a build that can // happen at some random time anyway. Authentication old = SecurityContextHolder.getContext().getAuthentication(); SecurityContextHolder.getContext().setAuthentication(ACL.SYSTEM); try { for (SCMSourceOwner owner : SCMSourceOwners.all()) { for (SCMSource source : owner.getSCMSources()) { if (source instanceof SubversionSCMSource) { if (id.equals(((SubversionSCMSource) source).getUuid())) { LOGGER.log(Level.INFO, "SCM changes detected relevant to {0}. Notifying update", owner.getFullDisplayName()); owner.onSCMSourceUpdated(source); notified = true; } } } } } finally { SecurityContextHolder.getContext().setAuthentication(old); } if (!notified) { LOGGER.log(Level.INFO, "No subversion consumers for UUID {0}", uuid); } return notified; } } }