package hudson.plugins.collabnet.filerelease;
import com.collabnet.ce.webservices.CTFFile;
import com.collabnet.ce.webservices.CTFPackage;
import com.collabnet.ce.webservices.CTFProject;
import com.collabnet.ce.webservices.CTFRelease;
import com.collabnet.ce.webservices.CTFReleaseFile;
import com.collabnet.ce.webservices.CollabNetApp;
import hudson.Extension;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.FreeStyleProject;
import hudson.model.Result;
import hudson.plugins.collabnet.AbstractTeamForgeNotifier;
import hudson.plugins.collabnet.ConnectionFactory;
import hudson.plugins.collabnet.documentuploader.FilePattern;
import hudson.plugins.collabnet.util.CNFormFieldValidator;
import hudson.plugins.collabnet.util.CNHudsonUtil;
import hudson.plugins.collabnet.util.ComboBoxUpdater;
import hudson.remoting.VirtualChannel;
import hudson.tasks.BuildStepMonitor;
import hudson.util.ComboBoxModel;
import hudson.util.FormValidation;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.rmi.RemoteException;
/**
* Hudson plugin to update files from the Hudson workspace
* to the CollabNet File Release System.
*/
public class CNFileRelease extends AbstractTeamForgeNotifier {
// listener is used for logging and will only be
// set at the beginning of perform.
private transient BuildListener listener = null;
private static final String IMAGE_URL = "/plugin/collabnet/images/48x48/";
// collabNet object
private transient CollabNetApp cna = null;
// Variables from the form
private String rpackage;
private String release;
private boolean overwrite;
private FilePattern[] file_patterns;
/**
* Creates a new CNFileRelease object.
*
* @param project where the files will be uploaded. The project
* contains the package.
* @param pkg where the files will be uploaded. The package contains
* the release.
* @param release where the files will be uploaded.
* @param overwrite whether or not to overwrite existing files.
* @param filePatterns Any files in the Hudson workspace that match these
* ant-style patterns will be uploaded to the
* CollabNet server.
*/
@DataBoundConstructor
public CNFileRelease(ConnectionFactory connectionFactory,
String project, String pkg, String release,
boolean overwrite, FilePattern[] filePatterns) {
super(connectionFactory,project);
this.rpackage = pkg;
this.release = release;
this.overwrite = overwrite;
this.file_patterns = filePatterns;
}
/**
* Setting the listener allows logging to work.
*
* @param listener passed into the perform method.
*/
private void setupLogging(BuildListener listener) {
this.listener = listener;
}
/**
* Log a message to the console. Logging will only work once the
* listener is set. Otherwise, it will fail (silently).
*
* @param message A string to print to the console.
*/
private void logConsole(String message) {
if (this.listener != null) {
message = "CollabNet FileRelease: " + message;
this.listener.getLogger().println(message);
}
}
/**
* Convenience method to log RemoteExceptions.
*
* @param methodName in progress on when this exception occurred.
* @param re The RemoteException that was thrown.
*/
private void log(String methodName, RemoteException re) {
this.logConsole(methodName + " failed due to " + re.getClass().getName() +
": " + re.getMessage());
}
/**
* @return the package of the release where the files are uploaded.
*/
public String getPkg() {
return this.rpackage;
}
/**
* @return the release where the files are uploaded.
*/
public String getRelease() {
return this.release;
}
/**
* @return whether or not existing files should be overwritten.
*/
public boolean isOverwrite() {
return this.overwrite;
}
/**
* @return the ant-style file patterns.
*/
public FilePattern[] getFilePatterns() {
if (this.file_patterns != null) {
return this.file_patterns;
} else {
return new FilePattern[0];
}
}
@Override
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
/**
* The function does the work of uploading files for the release.
*
* @param build current Hudson build.
* @param launcher unused.
* @param listener receives events that happen during a build. We use it
* for logging.
* @return true if successful, false if a critical error occurred.
*/
@Override
public boolean perform(AbstractBuild<?, ?> build, Launcher launcher,
BuildListener listener) throws IOException, InterruptedException {
this.setupLogging(listener);
this.cna = connect();
if (this.cna == null) {
this.logConsole("Critical Error: login to " + this.getCollabNetUrl() +
" failed. Setting build status to UNSTABLE (or worse).");
Result previousBuildStatus = build.getResult();
build.setResult(previousBuildStatus.combine(Result.UNSTABLE));
build.addAction(this.createAction(0, null));
return false;
}
CTFRelease release = this.getReleaseObject();
if (release == null) {
Result previousBuildStatus = build.getResult();
build.setResult(previousBuildStatus.combine(Result.UNSTABLE));
this.logoff();
build.addAction(this.createAction(0, release));
return false;
}
// now that we have the releaseId, we can do the upload.
int numUploaded = this.uploadFiles(build, release);
build.addAction(this.createAction(numUploaded, release));
this.logoff();
return true;
}
/**
* Get the ResultAction for this build.
*
* @param numUploaded
* @return CnfrResultAction.
*/
public CnfrResultAction createAction(int numUploaded, CTFRelease release) {
String displaymsg = "Download from CollabNet File Release System";
return new CnfrResultAction(displaymsg,
IMAGE_URL + "cn-icon.gif",
"console",
release.getUrl(),
numUploaded);
}
/**
* Upload the files which match the file patterns to the given
* releaseId.
*
* @param build current Hudson build.
* @param release where the files will be uploaded.
* @return the number of files successfully uploaded.
* @throws IOException
* @throws InterruptedException
*/
public int uploadFiles(AbstractBuild<?, ?> build, CTFRelease release) throws IOException, InterruptedException {
int numUploaded = 0;
this.logConsole("Uploading file to project '" + this.getProject() +
"', package '" + this.getPkg() + "', release '" +
this.getRelease() + "' on host '" + this.getCollabNetUrl() +
"' as user '" + this.getUsername() + "'.");
// upload files
for (FilePattern uninterp_fp : this.getFilePatterns()) {
String file_pattern;
try {
file_pattern = uninterp_fp.interpret(build, listener);
} catch (IllegalArgumentException e) {
this.logConsole("File pattern " + uninterp_fp + " contained a bad "
+ "env var. Skipping.");
continue;
}
if (file_pattern.equals("")) {
// skip empty fields
continue;
}
FilePath[] filePaths = this.getFilePaths(build, file_pattern);
for (FilePath uploadFilePath : filePaths) {
// check if a file already exists
CTFReleaseFile file = null;
try {
file = release.getFileByTitle(uploadFilePath.getName());
} catch (RemoteException re) {
this.log("find file", re);
}
if (file != null) {
if (this.isOverwrite()) {
// delete existing file
try {
file.delete();
this.logConsole("Deleted previously uploaded file: " +
uploadFilePath.getName());
} catch (RemoteException re) {
this.log("delete file", re);
}
} else {
this.logConsole("File " + uploadFilePath.getName() +
" already exists in the file release " +
"system and overwrite is set to false. " +
"Skipping.");
continue;
}
}
try {
CTFFile f = new CTFFile(cna,uploadFilePath.act(
new RemoteFrsFileUploader(getCollabNetUrl(), getUsername(), cna.getSessionId())
));
CTFReleaseFile rf = release.addFile(uploadFilePath.getName(), getMimeType(uploadFilePath), f);
this.logConsole("Uploaded file " + uploadFilePath.getName() + " -> " + rf.getURL());
numUploaded++;
} catch (RemoteException re) {
this.log("upload file", re);
} catch (IOException ioe) {
this.logConsole("Could not upload file due to IOException: "
+ ioe.toString());
ioe.printStackTrace(this.listener.error("error"));
} catch (InterruptedException ie) {
this.logConsole("Could not upload file due to " +
"InterruptedException: " + ie.getMessage());
}
}
}
return numUploaded;
}
/**
* Private class that can perform upload function.
*/
private static class RemoteFrsFileUploader implements FileCallable<String> {
private String mServerUrl;
private String mUsername;
private String mSessionId;
/**
* Constructor. Needs to have old sessionId, since the uploaded file is only available to the same session.
* @param serverUrl collabnet serverUrl
* @param username collabnet username
* @param sessionId collabnet sessionId
*/
public RemoteFrsFileUploader(String serverUrl, String username, String sessionId) {
mServerUrl = serverUrl;
mUsername = username;
mSessionId = sessionId;
}
/**
* @see FileCallable#invoke(File, VirtualChannel)
*/
public String invoke(File f, VirtualChannel channel) throws IOException {
CollabNetApp cnApp = CNHudsonUtil.recreateCollabNetApp(mServerUrl, mUsername, mSessionId);
return cnApp.upload(f).getId();
}
}
/**
* Return the filepaths in the workspace which match the pattern.
*
* @param build The hudson build.
* @param pattern An ant-style pattern.
* @return an array of FilePaths which match this pattern in the
* hudson workspace.
*/
private FilePath[] getFilePaths(AbstractBuild<?, ?> build,
String pattern) {
FilePath workspace;
if (FreeStyleProject.class.isInstance(build.getProject())) { // generic instanceof causes compilation error
// our standard project
workspace = build.getWorkspace();
} else {
// promoted build - use the project's workspace, since the build doesn't always account for custom workspace
// may be a bug with promoted build?
workspace = build.getProject().getRootProject().getWorkspace();
}
String logEntry = "Searching ant pattern '" + pattern + "'";
FilePath[] uploadFilePaths = new FilePath[0];
try {
uploadFilePaths = workspace.list(pattern);
logEntry += " in " + workspace.absolutize().getRemote();
} catch (IOException ioe) {
this.logConsole("Could not list workspace due to IOException: "
+ ioe.getMessage());
} catch (InterruptedException ie) {
this.logConsole("Could not list workspace due to " +
"InterruptedException: " + ie.getMessage());
}
logEntry += " : found " + uploadFilePaths.length + " entry(ies)";
logConsole(logEntry);
return uploadFilePaths;
}
/**
* Get the mimetype for the file.
*
* @param f The file to return the mimetype for.
* @return the string representing the mimetype of the file.
*/
public static String getMimeType(FilePath f) {
return new MimetypesFileTypeMap().getContentType(f.getName());
}
/**
* Log out of the collabnet server. Invalidates the cna object.
*/
public void logoff() {
if (this.cna != null) {
CNHudsonUtil.logoff(cna);
this.cna = null;
} else {
this.logConsole("logoff failed. Not logged in!");
}
}
/**
* Get the releaseId from the project/package/release names.
* Returns null if somewhere along the way one of these IDs
* cannot be found.
*
* @return the id for the release.
*/
public CTFRelease getReleaseObject() throws RemoteException {
CTFProject projectId = this.getProjectObject();
if (projectId == null) {
this.logConsole("Critical Error: projectId cannot be found for " +
this.getProject() + ". This could mean that the project "
+ "does not exist OR that the user logging in does not "
+ "have access to that project. " +
"Setting build status to UNSTABLE (or worse).");
return null;
}
CTFPackage pkg = projectId.getPackages().byTitle(getPkg());
if (pkg == null) {
this.logConsole("Critical Error: packageId cannot be found for " +
this.getPkg() + ". " +
"Setting build status to UNSTABLE (or worse).");
return null;
}
CTFRelease release = pkg.getReleaseByTitle(getRelease());
if (release == null) {
this.logConsole("Critical Error: releaseId cannot be found for " +
this.getRelease() + ". " +
"Setting build status to UNSTABLE (or worse).");
return null;
}
return release;
}
/**
* Get the project id for the project name.
*
* @return the matching project id, or null if none is found.
*/
public CTFProject getProjectObject() throws RemoteException {
if (this.cna == null) {
this.logConsole("Cannot getProjectId, not logged in!");
return null;
}
return cna.getProjectByTitle(this.getProject());
}
/**
* The CNFileRelease Descriptor class.
*/
@Extension
public static final class DescriptorImpl extends AbstractTeamForgeNotifier.DescriptorImpl {
/**
* @return string to display for configuration screen.
*/
@Override
public String getDisplayName() {
return "CollabNet File Release";
}
/**
* Form validation for package.
*
* @return the form validation
*/
public FormValidation doCheckPkg(CollabNetApp cna, @QueryParameter String project, @QueryParameter String pkg) throws RemoteException {
return CNFormFieldValidator.packageCheck(cna,project,pkg);
}
/**
* Form validation for release.
*
* @return the form validation
*/
public FormValidation doCheckRelease(CollabNetApp cna, @QueryParameter String project,
@QueryParameter String pkg, @QueryParameter String release) throws RemoteException {
return CNFormFieldValidator.releaseCheck(cna,project,pkg,release,true);
}
/**
* Gets a list of packages to choose from and write them as a
* JSON string into the response data.
*/
public ComboBoxModel doFillPkgItems(CollabNetApp cna, @QueryParameter String project) throws RemoteException {
return ComboBoxUpdater.getPackages(cna,project);
}
/**
* Gets a list of releases to choose from and write them as a
* JSON string into the response data.
*/
public ComboBoxModel doFillReleaseItems(CollabNetApp cna,
@QueryParameter String project, @QueryParameter("package") String _package) throws RemoteException {
return ComboBoxUpdater.getReleases(cna,project,_package);
}
}
}