package hudson.plugins.cmvc; import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; import hudson.Launcher; import hudson.Proc; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.ModelObject; import hudson.model.TaskListener; import hudson.plugins.cmvc.CmvcChangeLogSet.CmvcChangeLog; import hudson.plugins.cmvc.util.CmvcRawParser; import hudson.plugins.cmvc.util.CommandLineUtil; import hudson.plugins.cmvc.util.DateUtil; import hudson.scm.ChangeLogParser; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.util.ArgumentListBuilder; import hudson.util.ForkOutputStream; import hudson.util.FormValidation; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Serializable; import java.text.ParseException; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import net.sf.json.JSONObject; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; /** * This class implements the {@link SCM} methods for a CMVC repository. The call * to CMVC is assumed to work without any setup. This implies that either the * environment variable <code>BECOME_USER</code> is set or the become user is * provided in the project configuration page. Besides that this user must have * permissions to access the specified family from within the hudson host. * * <p> * Checks for changes in a CMVC family (repository). Triggers a build if any * integrated track within the monitored releases is detected. * </p> * * <p> * Utilizes CMVCs <code>Report -raw</code> command to query the family for * changes. First it looks for all integrated tracks - within the specified * releases - between the last build time and the current time (<code>-view TrackView</code>). * Then it performs another query to find all files included in these tracks(<code>-view ChangeView</code>). * </p> * * <p> * Changes are detected by running commands similar to the following: * </p> * * <pre> * Report -family family@localhost@6666 * -raw * -view TrackView * -where "lastUpdate between <lastBuildTime> * and <now> * and state = 'integrate' * and releaseName in ('RC_123') * order by defectName" * </pre> * * <pre> * Report -family family@localhost@6666 * -raw * -view ChangeView * -where "defectName in ('1', '2') and releaseName in ('RC_123') order by defectName" * </pre> * * @see <a href="http://www.redbooks.ibm.com/abstracts/gg244178.html">Did You * Say CMVC?</a> * * @author <a href="mailto:fuechi@ciandt.com">Fábio Franco Uechi</a> * */ public class CmvcSCM extends SCM implements Serializable { private static final long serialVersionUID = -6712277029373852186L; /** Configuration parameters */ /** * CMVC family. Syntax: family@host@port */ private String family; /** * Release names separated by comma */ private String releases; /** * User login used to connect to CMVC */ private String become; /** * Absolute fullpath + script name to be used to perform the checkout */ private String checkoutScript; /** * TrackView Report where clause. If defined will be used for polling * changes, otherwise the default condition will be used. */ private String trackViewReportWhereClause; /** * Utility class */ private CommandLineUtil commandLineUtil = null; /** * @param family * @param releases * @param project * @param cleanCopy */ @DataBoundConstructor public CmvcSCM(String family, String become, String releases, String checkoutScript, String trackViewReportWhereClause) { super(); this.checkoutScript = checkoutScript; this.family = family; this.releases = releases; this.become = become; this.trackViewReportWhereClause = trackViewReportWhereClause; } private CommandLineUtil getCmvcCommandLineUtil() { return commandLineUtil != null ? commandLineUtil : new CommandLineUtil( this); } @SuppressWarnings("unchecked") @Override public boolean checkout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException { boolean checkoutResult = false; CmvcChangeLogSet cmvcChangeLogSet = null; try { cmvcChangeLogSet = getCmvcChangeLogSet(build, launcher, workspace, listener, changelogFile); if (cmvcChangeLogSet != null) { if (cmvcChangeLogSet.getTrackNames() != null) { checkoutResult = doCheckout(build, launcher, workspace, listener, changelogFile, cmvcChangeLogSet); } else { checkoutResult = true; } listener.getLogger().print("Writing changelog file."); writeChangeLogFile(changelogFile, cmvcChangeLogSet); } } catch (Throwable e) { listener.fatalError("Error performing checkout: " + e.getMessage(), e); checkoutResult = false; } return checkoutResult; } private void writeChangeLogFile(File changelogFile, CmvcChangeLogSet cmvcChangeLogSet) throws IOException { FileWriter fileWriter = new FileWriter(changelogFile); try { CmvcRawParser.writeChangeLogFile(cmvcChangeLogSet, fileWriter); } finally { IOUtils.closeQuietly(fileWriter); } } @SuppressWarnings("unchecked") private boolean doCheckout(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile, CmvcChangeLogSet cmvcChangeLogSet) throws IOException, InterruptedException { listener.getLogger().println("Wiping out workspace."); workspace.deleteContents(); ArgumentListBuilder cmd = null; String[] releases = getReleases().split(","); for (String release : releases) { release = release.trim(); String[] tracksToCheckout = cmvcChangeLogSet. getTracksPerRelease(release).toArray(new String[0]); String tracksParameter = getCmvcCommandLineUtil(). convertToUnixQuotedParameter(tracksToCheckout); if ( "".equals(tracksParameter) ) { listener.getLogger().println("No tracks found to release " + release); } else { cmd = createCheckoutCommand(launcher, release, tracksParameter); listener.getLogger().println("Invoking checkout script. Release: " + release); if ( !run(launcher, cmd, listener, workspace, build) ) { return false; } } } return true; } private ArgumentListBuilder createCheckoutCommand(Launcher launcher, String release, String tracksParameter) { ArgumentListBuilder cmd; cmd = new ArgumentListBuilder(); if (isGroovyCheckoutScript() && !launcher.isUnix()) { cmd.add("groovy"); } cmd.add(this.checkoutScript); cmd.addQuoted(tracksParameter); cmd.add(release); return cmd; } private boolean isGroovyCheckoutScript() { return FilenameUtils.isExtension(this.checkoutScript, ".groovy"); } @SuppressWarnings("unchecked") private CmvcChangeLogSet getCmvcChangeLogSet(AbstractBuild build, Launcher launcher, FilePath workspace, BuildListener listener, File changelogFile) throws IOException, InterruptedException, ParseException { CmvcChangeLogSet changeLogSet = null; ArgumentListBuilder cmd = generateChangesDetectionCommand(build.getProject(), listener); ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (run(launcher, cmd, listener, workspace, new ForkOutputStream(baos, listener.getLogger()), build)) { BufferedReader in = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(baos.toByteArray()))); changeLogSet = new CmvcChangeLogSet(build); List<CmvcChangeLog> logs = CmvcRawParser.parseTrackViewReport(in, changeLogSet); changeLogSet.setLogs(logs); } else { throw new IOException("Error while checking for tracks"); } cmd = getCmvcCommandLineUtil().buildReportChangeViewCommand( changeLogSet); baos.reset(); if (cmd != null) { if (run(launcher, cmd, listener, workspace, new ForkOutputStream( baos, listener.getLogger()), build)) { BufferedReader in = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(baos.toByteArray()))); CmvcRawParser.parseChangeViewReportAndPopulateChangeLogs(in, changeLogSet); } else { throw new IOException("Error while checking for changes"); } } return changeLogSet; } @Override public ChangeLogParser createChangeLogParser() { return new CmvcChangeLogParser(); } @Override public SCMDescriptor<CmvcSCM> getDescriptor() { return DESCRIPTOR; } /** * Polls cmvc repository for integrated tracks within the current family and * releases * * <p> * By default it checks for changes in a CMVC family (repository). Triggers a build if any * integrated track within the monitored releases is detected. * </p> * * * @see hudson.scm.SCM#pollChanges(hudson.model.AbstractProject, * hudson.Launcher, hudson.FilePath, hudson.model.TaskListener) */ @SuppressWarnings("unchecked") @Override public boolean pollChanges(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException { if (project.getLastBuild() == null ) { listener.getLogger().println("No existing build. Starting a new one"); return true; } ArgumentListBuilder cmd = generateChangesDetectionCommand(project, listener); ByteArrayOutputStream baos = new ByteArrayOutputStream(); if (!run(launcher, cmd, listener, workspace, new ForkOutputStream(baos, listener.getLogger()), null)) return false; BufferedReader in = new BufferedReader(new InputStreamReader( new ByteArrayInputStream(baos.toByteArray()))); return CmvcRawParser.parseTrackViewReport(in); } @SuppressWarnings("unchecked") private ArgumentListBuilder generateChangesDetectionCommand( AbstractProject project, TaskListener listener) { Date lastBuild = null; if (project.getLastSuccessfulBuild() != null) { lastBuild = project.getLastSuccessfulBuild().getTimestamp().getTime(); } else { listener.getLogger().println("No existing successful build."); lastBuild = DateUtil.MIN_DATE; } ArgumentListBuilder cmd = getCmvcCommandLineUtil() .buildReportTrackViewCommand( DateUtil.convertToCmvcDate(new Date()), DateUtil.convertToCmvcDate(lastBuild)); return cmd; } /** * Invokes the command with the specified command line option and wait for * its completion. * @param dir * if launching locally this is a local path, otherwise a remote * path. * @param out * Receives output from the executed program. * @param build TODO */ @SuppressWarnings("unchecked") protected final boolean run(Launcher launcher, ArgumentListBuilder cmd, TaskListener listener, FilePath dir, OutputStream out, AbstractBuild build) throws IOException, InterruptedException { Map<String, String> env = createEnvVarMap(true, build); int r = launcher.launch().cmds(cmd).envs(env).stdout(out).pwd(dir).join(); if (r != 0) listener.fatalError(getDescriptor().getDisplayName() + " failed. exit code=" + r); return r == 0; } @SuppressWarnings("unchecked") protected final boolean run(Launcher launcher, ArgumentListBuilder cmd, TaskListener listener, FilePath dir, AbstractBuild build) throws IOException, InterruptedException { return run(launcher, cmd, listener, dir, listener.getLogger(), build); } /** * * @param overrideOnly * true to indicate that the returned map shall only contain * properties that need to be overridden. This is for use with * {@link Launcher}. false to indicate that the map should * contain complete map. This is to invoke {@link Proc} directly. * @param build TODO */ @SuppressWarnings("unchecked") protected final Map<String, String> createEnvVarMap(boolean overrideOnly, AbstractBuild build) { Map<String, String> env = new HashMap<String, String>(); try { if (build != null){ env = build.getEnvironment(TaskListener.NULL); } } catch (IOException e) { } catch (InterruptedException e) { } if (!overrideOnly) env.putAll(EnvVars.masterEnvVars); return env; } @SuppressWarnings("unchecked") @Override public void buildEnvVars(AbstractBuild build, Map<String, String> env) { super.buildEnvVars(build, env); env.put("CMVC_FAMILY", this.family); env.put("CMVC_RELEASES", this.releases); if (StringUtils.isNotEmpty(this.become)){ env.put("CMVC_BECOME", this.become); } } /** * Descriptor should be singleton. */ @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); public String getReleases() { return this.releases; } public String getFamily() { return this.family; } public String getBecome() { return this.become; } public String getCheckoutScript() { return checkoutScript; } public void setCheckoutScript(String checkoutScript) { this.checkoutScript = checkoutScript; } public String getTrackViewReportWhereClause() { return trackViewReportWhereClause; } public void setTrackViewReportWhereClause(String trackViewReportWhereClause) { this.trackViewReportWhereClause = trackViewReportWhereClause; } public static class DescriptorImpl extends SCMDescriptor<CmvcSCM> implements ModelObject { /** * CMVC binaries working dir */ private String cmvcPath; /** * CMVC version */ private String cmvcVersion; protected DescriptorImpl() { super(CmvcSCM.class, null); load(); } @Override public String getDisplayName() { return "CMVC"; } @Override public SCM newInstance(StaplerRequest req, JSONObject formData) throws FormException { CmvcSCM scm = req.bindJSON(CmvcSCM.class, formData); return scm; } @Override public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { this.cmvcPath = Util.fixEmpty(req.getParameter("cmvc.cmvcPath") .trim()); // this.cmvcVersion = Util.fixEmpty(req.getParameter( // "cmvc.cmvcVersion").trim()); save(); return true; } // // web methods // /** * Checks the correctness of family. */ public FormValidation doCheckFamily(@QueryParameter String value) { if (StringUtils.isEmpty(value)) { return FormValidation.error(Messages.cmvc_family_mandatory()); } return FormValidation.ok(); } /** * Checks the correctness of releases. */ public FormValidation doCheckReleases(@QueryParameter String value) { if (StringUtils.isEmpty(value)) { return FormValidation.error(Messages.cmvc_releases_mandatory()); } return FormValidation.ok(); } /** * Checks the correctness of CheckoutScript. */ public FormValidation doCheckCheckoutScript(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { File script = new File(value); if (!script.exists()) { return FormValidation.error(Messages.cmvc_checkoutScript_filenotexist()); } } return FormValidation.ok(); } /** * Checks the correctness of CheckoutScript. */ public FormValidation doCheckTrackViewReportWhereClause(@QueryParameter String value) { if (StringUtils.isNotEmpty(value)) { //TODO test where clause } return FormValidation.ok(); } public String getCmvcPath() { if (cmvcPath == null) { return "c:/cmvc/exe"; } return cmvcPath; } public String getCmvcVersion() { if (cmvcVersion == null) { return "2.0"; } return cmvcVersion; } public void setCmvcVersion(String cmvcVersion) { this.cmvcVersion = cmvcVersion; } } }