/*******************************************************************************
*
* Copyright (c) 2004-2011 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi
*
*******************************************************************************/
package hudson.scm;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.XmlFile;
import hudson.matrix.MatrixConfiguration;
import hudson.model.AbstractProject;
import hudson.model.Hudson;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Saveable;
import hudson.model.listeners.SaveableListener;
import hudson.remoting.Channel;
import java.io.File;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Map;
import java.util.logging.Logger;
import org.tmatesoft.svn.core.SVNURL;
import hudson.scm.subversion.Messages;
import static java.util.logging.Level.INFO;
import static hudson.scm.SubversionSCM.DescriptorImpl.Credential;
/**
* Persists the credential per job. This object is remotable.
*
* @author Kohsuke Kawaguchi
*/
public final class PerJobCredentialStore implements Saveable, SubversionSCM.DescriptorImpl.RemotableSVNAuthenticationProvider {
private static final Logger LOGGER = Logger.getLogger(PerJobCredentialStore.class.getName());
/**
* Used to remember the context. If we are persisting, we don't want to persist a proxy,
* even if that happens in the context of a remote call.
*/
private static final ThreadLocal<Boolean> IS_SAVING = new ThreadLocal<Boolean>();
private final transient AbstractProject<?, ?> project;
private final transient String url;
private static final String credentialsFileName = "subversion.credentials";
private transient CredentialsSaveableListener saveableListener;
/**
* SVN authentication realm to its associated credentials, scoped to this project.
*/
private final Map<String, Credential> credentials = new Hashtable<String, Credential>();
public PerJobCredentialStore(AbstractProject<?, ?> project, String url) {
this.project = project;
this.url = url;
// read existing credential
XmlFile xml = getXmlFile(project);
try {
if (xml.exists()) {
xml.unmarshal(this);
}
} catch (IOException e) {
// ignore the failure to unmarshal, or else we'll never get through beyond this point.
LOGGER.log(INFO, Messages.PerJobCredentialStore_readCredentials_error(xml), e);
}
}
private synchronized Credential get(String key) {
return credentials.get(key);
}
public Credential getCredential(SVNURL url, String realm) {
return get(getCredentialsKey(url.toDecodedString(), realm));
}
public void acknowledgeAuthentication(String realm, Credential cred) {
try {
acknowledge(getCredentialsKey(url, realm), cred);
} catch (IOException e) {
LOGGER.log(INFO, Messages.PerJobCredentialStore_acknowledgeAuthentication_error(), e);
}
}
/**
* Method retuns credentials key based on realm and url. If url is not null and if it will be processed
* without revision number, realm is used if url is null,
*
* @param url svn url
* @param realm realm
* @return credentials key.
* @see SubversionSCM#getUrlWithoutRevision(String)
*/
private String getCredentialsKey(String url, String realm) {
return null == url ? realm : url.lastIndexOf("@") > 0 ? SubversionSCM.getUrlWithoutRevision(url) : url;
}
private synchronized void acknowledge(String key, Credential cred) throws IOException {
Credential old = cred == null ? credentials.remove(key) : credentials.put(key, cred);
// save only if there was a change
if (old == null && cred == null) {
return;
}
if (old == null || cred == null || !old.equals(cred)) {
save();
}
}
public synchronized void save() throws IOException {
IS_SAVING.set(Boolean.TRUE);
try {
if (!credentials.isEmpty()) {
XmlFile xmlFile = getXmlFile(project);
xmlFile.write(this);
SaveableListener.fireOnChange(this, xmlFile);
}
} finally {
IS_SAVING.remove();
}
}
public XmlFile getXmlFile(Job prj) {
//default behaviour
File rootDir = prj.getRootDir();
File credentialFile = new File(rootDir, credentialsFileName);
if (credentialFile.exists()) {
return new XmlFile(credentialFile);
}
//matrix configuration project
if (prj instanceof MatrixConfiguration && prj.getParent() != null) {
ItemGroup parent = prj.getParent();
if (parent instanceof Job){
return getXmlFile((Job)parent);
}
}
if (prj.hasCascadingProject()) {
return getXmlFile(prj.getCascadingProject());
}
return new XmlFile(new File(rootDir, credentialsFileName));
}
/*package*/
public synchronized boolean isEmpty() {
return credentials.isEmpty();
}
/**
* When sent to the remote node, send a proxy.
*/
private Object writeReplace() {
if (IS_SAVING.get() != null) {
return this;
}
Channel c = Channel.current();
return c == null ? this : c.export(SubversionSCM.DescriptorImpl.RemotableSVNAuthenticationProvider.class, this);
}
public CredentialsSaveableListener getSaveableListener() {
if (null == saveableListener) {
ExtensionList<SaveableListener> extensionList = Hudson.getInstance().getExtensionList(
SaveableListener.class);
if (null != extensionList && !extensionList.isEmpty()) {
for (SaveableListener listener : extensionList) {
if (listener instanceof CredentialsSaveableListener) {
saveableListener = (CredentialsSaveableListener) listener;
break;
}
}
}
}
return saveableListener;
}
@Extension
public static class CredentialsSaveableListener extends SaveableListener {
private boolean fileChanged = false;
@Override
public void onChange(Saveable o, XmlFile file) {
if (o instanceof PerJobCredentialStore) {
fileChanged = true;
}
}
public boolean isFileChanged() {
return fileChanged;
}
public void resetChangedStatus() {
fileChanged = false;
}
}
}