package hudson.plugins.collabnet.documentuploader; import com.collabnet.ce.webservices.CTFDocument; import com.collabnet.ce.webservices.CTFDocumentFolder; import com.collabnet.ce.webservices.CTFFile; import com.collabnet.ce.webservices.CTFProject; import com.collabnet.ce.webservices.CollabNetApp; import hudson.Extension; import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.model.*; import hudson.plugins.collabnet.AbstractTeamForgeNotifier; import hudson.plugins.collabnet.ConnectionFactory; import hudson.plugins.collabnet.util.CNFormFieldValidator; import hudson.plugins.collabnet.util.CNHudsonUtil; import hudson.plugins.collabnet.util.CommonUtil; import hudson.remoting.VirtualChannel; import hudson.tasks.BuildStepMonitor; 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; import java.util.logging.Logger; /** * Hudson plugin to upload the Hudson build log * to the CollabNet Documents. */ public class CNDocumentUploader extends AbstractTeamForgeNotifier { private static Logger logger = Logger.getLogger("CNDocumentUploader"); private static final String IMAGE_URL = "/plugin/collabnet/images/48x48/"; // listener is used for logging and will only be // set at the beginning of perform. private transient BuildListener listener = null; // collabNet object private transient CollabNetApp cna = null; // Variables from the form private String uploadPath; private String description; private FilePattern[] file_patterns; private boolean includeBuildLog; /** * Creates a new CNDocumentUploader object. * * @param project where the build log will be uploaded. * @param uploadPath on the CollabNet server, where the build log should * be uploaded. * @param description * @param filePatterns * @param includeBuildLog */ @DataBoundConstructor public CNDocumentUploader(ConnectionFactory connectionFactory, String project, String uploadPath, String description, FilePattern[] filePatterns, boolean includeBuildLog) { super(connectionFactory,project); this.uploadPath = uploadPath; this.description = description; this.file_patterns = filePatterns; this.includeBuildLog = includeBuildLog; } /** * 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 Document Uploader: " + message; this.listener.getLogger().println(message); } } /** * Log a message to the console, including stack trace. Logging will only work once the * listener is set. Otherwise, it will fail (silently). * * @param message A string to print to the console. * @param exception the exception containing the stack trace to log */ private void logConsole(String message, Exception exception) { if (this.listener != null) { message = "CollabNet Document Uploader: " + message; this.listener.getLogger().println(message); // now print the stack trace exception.printStackTrace(this.listener.error("error")); } } /** * 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) { CommonUtil.logRE(logger, methodName, re); } /** * @return the path where the build log is uploaded. */ public String getUploadPath() { return this.uploadPath; } /** * @return the description of the uploaded files. */ public String getDescription() { return this.description; } /** * @return the ant-style file patterns. */ public FilePattern[] getFilePatterns() { if (this.file_patterns != null) { return this.file_patterns; } else { return new FilePattern[0]; } } /** * @return true if the build log should be uploaded. */ public boolean getIncludeBuildLog() { return this.includeBuildLog; } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } /** * The function does the work of uploading the build log. * * @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)."); build.setResult(Result.UNSTABLE); build.addAction(this.createAction(0, null)); return false; } CTFProject p = cna.getProjectByTitle(this.getProject()); if (p == null) { this.logConsole("Critical Error: Unable to find project '" + this.getProject() + "'. " + "Setting build status to UNSTABLE (or worse)."); build.setResult(Result.UNSTABLE); build.addAction(this.createAction(0, null)); this.logoff(); return false; } String path = this.getInterpreted(build, this.getUploadPath()); CTFDocumentFolder folder; try { folder = p.getOrCreateDocumentFolder(path); } catch (RemoteException re) { this.log("findOrCreatePath", re); // if this fails, cannot continue this.logConsole("Critical Error: Unable to create a path for '" + path + "'. Setting build status to " + "UNSTABLE (or worse)."); build.setResult(Result.UNSTABLE); build.addAction(this.createAction(0, null)); this.logoff(); return false; } int numUploaded = this.uploadFiles(folder, build, listener); build.addAction(this.createAction(numUploaded, folder)); try { this.cna.logoff(); } catch (RemoteException re) { this.log("logoff", re); } return true; } private Action createAction(int numUploaded, CTFDocumentFolder folder) { String displaymsg = "Download from CollabNet Documents"; return new CnduResultAction(displaymsg, IMAGE_URL + "cn-icon.gif", "console", folder.getURL(), numUploaded); } /** * Upload files matching the file patterns to the Document Service. * * @param folder folder where the files should be uploaded. * @param build the current Hudson build. * @return the number of files successfully uploaded. */ public int uploadFiles(CTFDocumentFolder folder, AbstractBuild<?, ?> build, BuildListener listener) throws IOException, InterruptedException { int numUploaded = 0; String path = this.getInterpreted(build, this.getUploadPath()); this.logConsole("Uploading files to project '" + this.getProject() + "', folder '" + path + "' on host '" + this.getCollabNetUrl() + "' as user '" + this.getUsername() + "'."); 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) { CTFFile file = this.uploadFile(uploadFilePath); if (file == null) { this.logConsole("Failed to upload " + uploadFilePath.getName() + "."); continue; } try { CTFDocument doc = this.updateOrCreateDoc(folder, file, uploadFilePath.getName(), CNDocumentUploader. getMimeType(uploadFilePath), build); this.logConsole("Uploaded " + uploadFilePath.getName() + " -> " + doc.getURL()); numUploaded++; } catch (RemoteException re) { logConsole("Upload file failed: " + re.getMessage()); this.log("updateOrCreateDoc", re); } } } if (this.getIncludeBuildLog()) { CTFFile file = this.uploadBuildLog(build); if (file == null) { this.logConsole("Failed to upload " + build.getLogFile().getName() + "."); } else { try { CTFDocument docId = this.updateOrCreateDoc(folder, file, build.getLogFile().getName(), CNDocumentUploader. getMimeType(build. getLogFile()), build); this.logConsole("Uploaded " + build.getLogFile().getName() + " -> " + docId.getURL()); numUploaded++; } catch (RemoteException re) { logConsole("Upload log failed: " + re.getMessage(), re); this.log("updateOrCreateDoc", re); } } } return numUploaded; } /** * 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; } /** * If a document exists under this folder with the fileName, * increment it's version and update with the new fileId. * Otherwise, create a new document. * * @param folder of the folder where the document will be * created/updated. * @param file of the already upload build log. * @param fileName name of the uploaded file. * @param mimeType of the uploaded file. * @param build the current Hudson build. * @return the docId associated with the new/updated document. * @throws RemoteException */ private CTFDocument updateOrCreateDoc(CTFDocumentFolder folder, CTFFile file, String fileName, String mimeType, AbstractBuild<?, ?> build) throws IOException, InterruptedException { CTFDocument doc = folder.getDocuments().byTitle(fileName); if (doc != null) { doc.update(file); return doc; } else { return folder.createDocument(fileName, this.getInterpreted(build, this.getDescription()), null, "final", false, fileName, mimeType, file, null, null); } } /** * Get the mimetype for the file. * * @param filePath The filePath to return the mimetype for. * @return the string representing the mimetype of the file. */ public static String getMimeType(FilePath filePath) { String mimeType = "text/plain"; try { mimeType = filePath.act(new FileCallable<String>() { @Override public String invoke(File f, VirtualChannel channel) throws IOException { if (f.getName().endsWith("log")) { return "text/plain"; } return new MimetypesFileTypeMap().getContentType(f); }}); } catch (IOException ioe) { // ignore exceptions } catch (InterruptedException ie) {} return mimeType; } /** * 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(File f) { if (f.getName().endsWith("log")) { return "text/plain"; } return new MimetypesFileTypeMap().getContentType(f); } /** * Upload the build log to the collabnet server. * * @param build the current Hudson build. * @return the id associated with the file upload. */ private CTFFile uploadBuildLog(AbstractBuild <?, ?> build) { if (this.cna == null) { this.logConsole("Cannot call updateSucceedingBuild, not logged in!"); return null; } try { return cna.upload(build.getLogFile()); } catch (RemoteException re) { this.log("uploadBuildLog", re); return null; } } /** * Upload the build log to the collabnet server. * * @param filePath the path of file to upload * @return the id associated with the file upload. */ private CTFFile uploadFile(FilePath filePath) { if (this.cna == null) { this.logConsole("Cannot call uploadFile, not logged in!"); return null; } try { // must upload to same session so temp file will be available later for creation of document String id = filePath.act(new RemoteFileUploader(getCollabNetUrl(), getUsername(), cna.getSessionId())); return new CTFFile(cna,id); } catch (RemoteException re) { this.logConsole("upload file failed", re); } catch (IOException ioe) { this.logConsole("Could not upload file due to IOException: " + ioe.getMessage(), ioe); } catch (InterruptedException ie) { this.logConsole("Could not upload file due to InterruptedException: " + ie.getMessage()); } return null; } /** * Private class that can perform upload function. */ private static class RemoteFileUploader implements FileCallable<String> { private String mUrl; private String mUsername; private String mSessionId; /** * Constructor. Needs to have old sessionId, since the uploaded file is only available to the same session. * @param url collabnet url * @param username collabnet username * @param sessionId collabnet sessionId */ public RemoteFileUploader(String url, String username, String sessionId) { mUrl = url; mUsername = username; mSessionId = sessionId; } /** * @see FileCallable#invoke(File, VirtualChannel) */ public String invoke(File f, VirtualChannel channel) throws IOException { CollabNetApp cnApp = CNHudsonUtil.recreateCollabNetApp(mUrl, mUsername, mSessionId); return cnApp.upload(f).getId(); } } /** * Log out of the collabnet server. Invalidates the cna object. */ public void logoff() { CNHudsonUtil.logoff(this.cna); this.cna = null; } /** * Translates a string that may contain build vars like ${BUILD_VAR} to * a string with those vars interpreted. * * @param build the Hudson build. * @param str the string to be interpreted. * @return the interpreted string. * @throws IllegalArgumentException if the env var is not found. */ private String getInterpreted(AbstractBuild<?, ?> build, String str) throws IOException, InterruptedException { try { return CommonUtil.getInterpreted(build.getEnvironment(TaskListener.NULL), str); } catch (IllegalArgumentException iae) { this.logConsole(iae.getMessage()); throw iae; } } /** * The CNDocumentUploader Descriptor class. */ @Extension public static final class DescriptorImpl extends AbstractTeamForgeNotifier.DescriptorImpl { /** * @return string to display for configuration screen. */ @Override public String getDisplayName() { return "CollabNet Document Uploader"; } /** * Form validation for upload path. */ public FormValidation doCheckUploadPath(CollabNetApp app, @QueryParameter String project, @QueryParameter String value) throws IOException { return CNFormFieldValidator.documentPathCheck(app,project,value); } public FormValidation doCheckDescription(@QueryParameter String value) { return CNFormFieldValidator.requiredCheck(value,"description"); } } }