package hudson.plugins.collabnet.pblupload; import com.collabnet.cubit.api.CubitConnector; import hudson.Extension; import hudson.FilePath; import hudson.FilePath.FileCallable; import hudson.Launcher; import hudson.model.*; import hudson.plugins.collabnet.documentuploader.FilePattern; import hudson.plugins.collabnet.util.CNFormFieldValidator; import hudson.plugins.collabnet.util.CommonUtil; import hudson.plugins.promoted_builds.Promotion; import hudson.remoting.VirtualChannel; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import hudson.util.FormValidation; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * The PblUploader is used to upload files to the Project Build Library (Pbl) * of a Lab Management manager node. */ public class PblUploader extends Notifier implements java.io.Serializable { private static final String LEFT_NAV_DISPLAY_MESSAGE = "Download from CollabNet Project Build Library"; private static final String IMAGE_URL = "/plugin/collabnet/images/48x48/"; private static final long serialVersionUID = 1L; private static String API_URL = "cubit_api"; private static String API_VERSION = "1"; private static String PBL_UPLOAD_URL = "pbl_upload"; private String hostUrl; private String user; private String key; private String project; private String pubOrPriv = "priv"; // "pub" or "priv" for compatibility reasons private FilePattern[] file_patterns; private String removePrefixRegex; private String path; private boolean preserveLocal = false; private boolean force = false; private String comment; private String description; // listener is used for logging and will only be // set at the beginning of perform. private transient BuildListener listener = null; /** * Constructs a new PblUploader instance. * * @param hostUrl for the Cubit host. * @param user to login as. * @param key to login with. * @param project to upload files to * @param pubOrPriv whether these files should be in the pub (public) * or priv (private) directory. * @param filePatterns matching local files that should be uploaded. * @param path one the Cubit host where the files will be uploaded. * @param preserveLocal if true, local directory structure will be copied * to the server. * @param force if true, the files will be uploaded, even if they will * overwrite existing files. * @param comment about the upload. * @param description of the files. */ @DataBoundConstructor public PblUploader(String hostUrl, String user, String key, String project, boolean pubOrPriv, FilePattern[] filePatterns, String path, boolean preserveLocal, boolean force, String comment, String description, String removePrefixRegex) { this.hostUrl = hostUrl; this.user = user; // our sig generator depends on the letters in our hex // being lower case this.key = key.trim().toLowerCase(); this.project = project; this.pubOrPriv = pubOrPriv?"pub":"priv"; this.file_patterns = filePatterns; this.path = path; this.preserveLocal = preserveLocal; this.force = force; this.comment = comment; this.description = description; this.removePrefixRegex = removePrefixRegex; } /** * @return the Cubit host's URL. */ public String getHostUrl() { if (this.hostUrl != null) { return this.hostUrl; } else { return ""; } } /** * @return the user to login as. */ public String getUser() { if (this.user != null) { return this.user; } else { return ""; } } private String returnValueOrEmptyString(String input){ if (input != null) { return input; } else { return ""; } } /** * @return the key to login with. */ public String getKey() { return returnValueOrEmptyString(this.key); } /** * @return the project to upload files to. */ public String getProject() { if (this.project != null) { return this.project; } else { return ""; } } /** * @return "pub" if the files should be uploaded as public, * "priv" if the files should be uploaded as private. */ public boolean getPubOrPriv() { return "pub".equals(pubOrPriv); } /** * @return the ant-style file patterns to match against the local * workspace. */ public FilePattern[] getFilePatterns() { if (this.file_patterns != null) { return this.file_patterns; } else { return new FilePattern[0]; } } /** * @return the path to upload files to on the Cubit host. */ public String getPath() { if (this.path != null) { return this.path; } else { return ""; } } /** * @return whether or not local directory structure should be preserved. */ public boolean getPreserveLocal() { return this.preserveLocal; } /** * @return whether or not the upload should continue if matching files * are present. */ public boolean getForce() { return this.force; } /** * @return the comment. */ public String getComment() { if (this.comment != null) { return this.comment; } else { return ""; } } /** * @return the description of the files. */ public String getDescription() { if (this.description != null) { return this.description; } else { return ""; } } /** * @return the description of the files. */ public String getRemovePrefixRegex() { if (this.removePrefixRegex != null) { return this.removePrefixRegex; } else { return ""; } } /** * setting the listener allows logging to work * * @param listener to use for logging events. */ private void setupLogging(BuildListener listener) { this.listener = listener; } /** * Logging will only work once the listener is set. * Otherwise, it will fail (silently). * * @param message to log. */ private void log(String message) { if (this.listener != null) { this.listener.getLogger().println(message); } } @Override public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } /** * Upload the files to the PBL. This is the hudson builds entry point into this plugin * * @param build the current Hudson build. * @param launcher unused. * @param listener for events. * @return true if uploading files succeeded. * @throws InterruptedException * @throws IOException */ @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws java.lang.InterruptedException, IOException { this.setupLogging(listener); this.log(""); if (build.getResult() == Result.FAILURE) { this.log("Not attempting to upload files to Project Build " + "Library because the build has failed."); return true; } this.log("Uploading files to Project Build Library"); this.log(this.getRemoteURL(build)); boolean success = this.uploadFiles(build); // add result to build page build. addAction(new PblUploadResultAction(LEFT_NAV_DISPLAY_MESSAGE, IMAGE_URL + "cubit-icon.gif", "console", addTrailSlash(this. getRemoteURL(build)), success)); if (!success) { // set the build result to the worse of the current result // and UNSTABLE build.setResult(build.getResult().combine(Result.UNSTABLE)); } return success; } /** * Processes the configured list of file patterns to upload to the pbl. * All patterns are processed by the method getFilePatterns, and any empty * strings (which can result after) processing are ignored. * * @param build the current hudson build * @return List of file patterns, after being interpreted with empty strings * removed. */ private List<String> getProcessedFilePatterns(AbstractBuild<?,?> build) throws IOException, InterruptedException { List<String> output = new ArrayList<String>(); for (FilePattern uninterp_fp : this.getFilePatterns()) { String file_pattern; try { file_pattern = uninterp_fp.interpret(build, listener); if (!file_pattern.equals("")){ output.add(file_pattern); } } catch (IllegalArgumentException e) { this.log("File pattern " + uninterp_fp + " contained a bad " + "env var. Skipping."); } } return output; } /** * This methods is calculates the success or fail of the pbl upload plugin * and logs the state to Hudson build log. Success occurs if any files * are successfully uploaded. * * @param num_files Total number of files processed during upload * @param failures Total number of files that failed to upload * @return true if some files uploaded successfully */ private boolean determineAndLogFinalState(int num_files, int failures){ num_files = num_files > 0 ? num_files: 0; failures = failures > 0 ? failures: 0; if (num_files == 0) { this.log("Could not find any matching files to upload. " + "Please check your file patterns."); return false; } else if (num_files == failures) { this.log("No files successfully uploaded. " + "You may want to check your " + "configuration or the status of " + "the Lab Management Manager."); return false; } else if (failures == 0) { this.log(num_files + " files successfully uploaded!"); return true; } else { this.log("Attempted to upload " + num_files + " files."); this.log(failures + " file uploads failed."); this.log((num_files - failures) + " file uploads succeeded."); return true; } } /** * Upload the files. * * @param build the current Hudson build. * @return true, if successful, false otherwise. * @throws IOException * @throws InterruptedException */ private boolean uploadFiles(AbstractBuild<?, ?> build) { try { FilePath workspace = build.getWorkspace(); int num_files = 0; int failures = 0; this.log(""); for (String file_pattern : getProcessedFilePatterns(build)) { this.log("Upload files matching " + file_pattern + ":"); for (FilePath pathOfFileToUpload : workspace.list(file_pattern)) { num_files++; Map<String, String> args = this.setupArgs(build, pathOfFileToUpload, workspace); if (!this.pblUpload(args, pathOfFileToUpload, workspace)) { failures++; } } this.log(""); } return determineAndLogFinalState(num_files, failures); } catch (Exception e) { this.log("CRITICAL ERROR: Upload of files failed due to: " + e.getMessage()); return false; } } /** * @return the local path to the uploaded file. */ private String getLocalFilePath(FilePath workspace, FilePath uploadFilePath) { String path = this.getRelativePath(workspace, uploadFilePath) + uploadFilePath.getName(); if (path.startsWith("/")) { path = path.replaceFirst("/", ""); } return path; } /** * @return the URL for where our upload will end up. */ private String getRemoteURL(AbstractBuild<?, ?> build) throws IOException, InterruptedException { return addTrailSlash(getHostUrl())+"pbl/" + addTrailSlash(this.getProject()) + pubOrPriv + "/" + this.getInterpreted(build, this.getPath()); } /** * @param str with or without slash * @return string with trailing slash. */ private static String addTrailSlash(String str) { if (str.endsWith("/")) { return str; } else { return str + "/"; } } /** * Figure out what the full path of the uploaded file should be. * * @param build the current Hudson build. * @param workspace * @param uploadFilePath * @return a string with the interpreted path plus possibly the local * directory structure. */ private String createUploadPath(AbstractBuild<?, ?> build, FilePath workspace, FilePath uploadFilePath) throws IOException, InterruptedException { String fileDestinationPath = this.getInterpreted(build, this.getPath()); if (this.getPreserveLocal()){ String localPath = this.getRelativePath(workspace, uploadFilePath); if (this.removePrefixRegex != null && !"".equals(this.removePrefixRegex)) { if (localPath.split(removePrefixRegex).length > 0 && // makes sure the regex is a prefix localPath.split(removePrefixRegex)[0].equals("")){ localPath = localPath.replaceFirst(removePrefixRegex, ""); } if (localPath.matches(removePrefixRegex)) { // if the entire localPath matches, it's removed entirely localPath = ""; } } fileDestinationPath = addTrailSlash(fileDestinationPath) + localPath; } return fileDestinationPath; } /** * 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 { Map<String, String> envVars = null; if (Hudson.getInstance().getPlugin("promoted-builds") != null && build.getClass().equals(Promotion.class)) { // if this is a promoted build, get the env vars from // the original build Promotion promotion = Promotion.class.cast(build); envVars = promotion.getTarget().getEnvironment(TaskListener.NULL); } else { envVars = build.getEnvironment(TaskListener.NULL); } try { //XXX should this use envVars instead of build.getEnv.... ? return CommonUtil.getInterpreted(build.getEnvironment(TaskListener.NULL), str); } catch (IllegalArgumentException iae) { this.log(iae.getMessage()); throw iae; } } /** * Returns the relative directory path between a child and it's ancestor * FilePath. For example, if ancestor = /a/b/c/d/ and child = * /a/b/c/d/e/f/g.txt this function will return /e/f. If ancestor is not * really an ancestor, "" will be returned. * * @param ancestor * @param child * @return the child's directory, relative to the ancestor's. */ private String getRelativePath(FilePath ancestor, FilePath child) { try { String ancestor_str = ancestor.toURI().toString(); String child_str = child.getParent().toURI().toString(); if (child_str.startsWith(ancestor_str)) { return child_str.replaceFirst(ancestor_str, ""); } } catch (InterruptedException e){ //TODO: perhaps log this. } catch (IOException e){ //TODO: perhaps log this. } return ""; } /** * Setup of the args needed for uploading files. * * @param build the current Hudson build. * @param uploadFilePath * @param workspace * @return a key, value map of the args. * @throws IOException * @throws InterruptedException */ private Map<String, String> setupArgs(AbstractBuild<?, ?> build, FilePath uploadFilePath, FilePath workspace) throws IOException, InterruptedException { Map<String, String> args = new HashMap<String, String>(); String md5sum = uploadFilePath.digest(); String path; String description; String comment; try { path = this.createUploadPath(build, workspace, uploadFilePath); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Setting up args failed due to" + " bad path: " + e.getMessage()); } try { description = this.getInterpreted(build, this.getDescription()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Setting up args failed due to" + " bad description: " + e.getMessage()); } try { comment = this.getInterpreted(build, this.getComment()); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Setting up args failed due to" + " bad comment: " + e.getMessage()); } args.put("md5sum", md5sum); args.put("path", path); args.put("proj", this.getProject()); args.put("type", pubOrPriv); args.put("userid", this.getUser()); if (this.getForce()){ /* The value is irrelevant, the presence of the parameter is * all the server needs to force the upload */ args.put("force","TRUE"); } args.put("comment", comment); args.put("desc", description); return args; } private void logPblCallResults(CubitConnector.ResponseCodeAndBody response, Map <String, String> args, FilePath uploadFilePath, FilePath workspace){ String localFilePath = this.getLocalFilePath(workspace, uploadFilePath); String remoteURL = addTrailSlash(addTrailSlash(getHostUrl()) + "pbl/" + addTrailSlash(args.get("proj")) + args.get("type") + "/" + args.get("path") ) + uploadFilePath.getName(); String resultStr = "Upload for file " + localFilePath + " -> " + remoteURL; if (response.getStatus() == 200) { this.log(resultStr + ": OK"); } else { String[] lines = response.getBody().split("\\n"); this.log("Upload for file " + uploadFilePath.getName() + " failed: "); for (String line : lines) { this.log(line); } this.log(resultStr + ": FAILED"); } } /** * Do the upload. * * @param args to send to the Lab Management server. * @param uploadFilePath to the uploading file. * @return true, if successful. * @throws IOException * @throws InterruptedException */ private boolean pblUpload(final Map<String, String> args, FilePath uploadFilePath, FilePath workspace) throws IOException, InterruptedException { CubitConnector.ResponseCodeAndBody result = uploadFilePath. act(new FileCallable<CubitConnector.ResponseCodeAndBody>() { private static final long serialVersionUID = 1L; @Override public CubitConnector.ResponseCodeAndBody invoke(File file, VirtualChannel channel) throws IOException { final CubitConnector cubitConnector = new CubitConnector(getHostUrl(), getUser(), getKey()); return cubitConnector.callCubit(PBL_UPLOAD_URL, args, file, true); } }); logPblCallResults(result, args, uploadFilePath, workspace); return(result.getStatus() == 200); } /** * PBLUploader does not need to wait til the build is finalized. */ @Override public boolean needsToRunAfterFinalized() { return false; } /** * Descriptor for {@link PblUploader}. Used as a singleton. The class is * marked as public so that it can be accessed from views. * */ @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { /** * @return human-readable name used in the configuration screen. */ @Override public String getDisplayName() { return "Lab Management Project Build Library (PBL) Uploader"; } /** * The PblUploader can be used as a post-promotion task. * * @param jobType * @return true */ @Override @SuppressWarnings("unchecked") public boolean isApplicable(java.lang.Class<? extends AbstractProject> jobType) { return true; } /** * Form validation for the host_url * * @param value url */ public FormValidation doCheckHostUrl(@QueryParameter String value) { return CNFormFieldValidator.hostUrlCheck(value); } public FormValidation doCheckUser(@QueryParameter String value) { return CNFormFieldValidator.requiredCheck(value, "user name"); } public FormValidation doCheckProject(@QueryParameter String value) { return CNFormFieldValidator.requiredCheck(value, "project"); } /** * Form validation for the API key. */ public FormValidation doCheckKey(@QueryParameter String hostUrl, @QueryParameter String user, @QueryParameter String key) { return CNFormFieldValidator.cubitKeyCheck(hostUrl,user,key); } public FormValidation doCheckPath(@QueryParameter String value) throws FormValidation { return CNFormFieldValidator.requiredInterpretedCheck(value, "path"); } /** * Form validation for the path. * * @param value */ public FormValidation doCheckRemovePrefixRegex(@QueryParameter String value) { return CNFormFieldValidator.regexCheck(value); } public FormValidation doCheckDescription(@QueryParameter String value) throws FormValidation { return CNFormFieldValidator.unrequiredInterpretedCheck(value, "description"); } public FormValidation doCheckComment(@QueryParameter String value) throws FormValidation { return CNFormFieldValidator.unrequiredInterpretedCheck(value, "description"); } } }