/* * The MIT License * * Copyright (c) 2010, Manufacture Française des Pneumatiques Michelin, Romain Seguy, * Amadeus SAS, Vincent Latombe * Copyright (c) 2007-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Erik Ramfelt, * Henrik Lynggaard, Peter Liljenberg, Andrew Bayer * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.michelin.cio.hudson.plugins.clearcaseucmbaseline; import hudson.FilePath; import hudson.Launcher; import hudson.Util; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Descriptor; import hudson.model.Hudson; import hudson.model.ParameterValue; import hudson.model.ParametersAction; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.clearcase.AbstractClearCaseScm; import hudson.plugins.clearcase.ClearToolLauncher; import hudson.plugins.clearcase.HudsonClearToolLauncher; import hudson.plugins.clearcase.PluginImpl; import hudson.plugins.clearcase.ucm.UcmMakeBaseline; import hudson.plugins.clearcase.ucm.UcmMakeBaselineComposite; import hudson.plugins.clearcase.util.PathUtil; import hudson.tasks.BuildWrapper; import hudson.tasks.Publisher; import hudson.util.DescribableList; import hudson.util.VariableResolver; import java.io.IOException; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.lang.ArrayUtils; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.export.Exported; /** * This class represents the actual {@link ParameterValue} for the * {@link ClearCaseUcmBaselineParameterDefinition} parameter. * * <p>This value holds the following information: * <li>The ClearCase UCM PVOB name (which is actually defined at config-time — * cf. {@link ClearCaseUcmBaselineParameterDefinition});</li> * <li>The ClearCase UCM component name (which is actually defined at config-time * — cf. {@link ClearCaseUcmBaselineParameterDefinition});</li> * <li>The ClearCase UCM promotion level (which is actually defined at config-time * — cf. {@link ClearCaseUcmBaselineParameterDefinition});</li> * <li>The ClearCase UCM view to create name (which is actually defined at config-time * — cf. {@link ClearCaseUcmBaselineParameterDefinition});</li> * <li>The ClearCase UCM baseline (this is the only one information which is * asked at build-time)</li> * </ul></p> * * @author Romain Seguy (http://openromain.blogspot.com) */ public class ClearCaseUcmBaselineParameterValue extends ParameterValue { // TODO move the attributes of this class and of ClearCaseUcmBaselineParameterDefinition // to a single dedicated class to avoid having too much duplicated code @Exported(visibility=3) private String baseline; // this att is set by the user once the build takes place @Exported(visibility=3) private String component; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private boolean excludeElementCheckedout; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private boolean forceRmview; // this att can be overriden by the user but default value // comes from ClearCaseUcmBaselineParameterDefinition private String mkviewOptionalParam; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private String promotionLevel; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private String pvob; // this att comes from ClearCaseUcmBaselineParameterDefinition private List<String> restrictions; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private boolean snapshotView; // this att comes from ClearCaseUcmBaselineParameterDefinition private String stream; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private boolean useUpdate; // this att comes from ClearCaseUcmBaselineParameterDefinition @Exported(visibility=3) private String viewName; // this att comes from ClearCaseUcmBaselineParameterDefinition private StringBuffer fatalErrorMessage = new StringBuffer(); // I have to have two constructors: If I use only one (the most complete one), // I get an exception in ClearCaseUcmBaselineParameterDefinition.createValue(StaplerRequest, JSONObject) // while invoking req.bindJSON() // Is it because of the two booleans? No time to investigate, sorry. @DataBoundConstructor public ClearCaseUcmBaselineParameterValue(String name, String baseline, boolean forceRmview) { this(name, null, null, null, null, null, null, baseline, false, forceRmview, false, false); } public ClearCaseUcmBaselineParameterValue( String name, String pvob, String component, String promotionLevel, String stream, String viewName, String mkviewOptionalParam, String baseline, boolean useUpdate, boolean forceRmview, boolean snapshotView, boolean excludeElementCheckedout) { super(name); this.pvob = ClearCaseUcmBaselineUtils.prefixWithSeparator(pvob); this.component = component; this.promotionLevel = promotionLevel; this.stream = stream; this.viewName = viewName; this.mkviewOptionalParam = mkviewOptionalParam; this.baseline = baseline; this.useUpdate = useUpdate; this.forceRmview = forceRmview; this.snapshotView = snapshotView; if(this.snapshotView) { // the "element * CHECKEDOUT" rule is mandatory for snapshot views this.excludeElementCheckedout = true; } else { this.excludeElementCheckedout = excludeElementCheckedout; } } /** * Returns the {@link BuildWrapper} (defined as an inner class) which does * the "checkout" from the ClearCase UCM baseline selected by the user. * * <p>If a {@link ClearCaseUcmBaselineParameterDefinition} is added for the * build but the SCM is not {@link ClearCaseUcmBaselineSCM}, then the * {@link BuildWrapper} which is returned will make the build fail.</p> */ @Override public BuildWrapper createBuildWrapper(AbstractBuild<?, ?> build) { // let's ensure that a baseline has been really provided if(baseline == null || baseline.length() == 0) { fatalErrorMessage.append("The value '" + baseline + "' is not a valid ClearCase UCM baseline."); } // HUDSON-5877: let's ensure the job has no publishers/notifiers coming // from the ClearCase plugin DescribableList<Publisher, Descriptor<Publisher>> publishersList = build.getProject().getPublishersList(); for(Publisher publisher : publishersList) { if(publisher instanceof UcmMakeBaseline || publisher instanceof UcmMakeBaselineComposite) { if(fatalErrorMessage.length() > 0) { fatalErrorMessage.append('\n'); } fatalErrorMessage.append("This job is set up to use a '").append(publisher.getDescriptor().getDisplayName()).append( "' publisher which is not compatible with the ClearCase UCM baseline SCM mode. Please remove this publisher."); } } if(fatalErrorMessage.length() > 0) { return new BuildWrapper() { /** * This method just makes the build fail for various reasons. */ @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { listener.fatalError(fatalErrorMessage.toString()); return null; } }; } if(build.getProject().getScm() instanceof ClearCaseUcmBaselineSCM) { // the job is apparently set-up in a clean way, the build can take place return new BuildWrapper() { private ClearToolLauncher createClearToolLauncher(TaskListener listener, FilePath workspace, Launcher launcher) { return new HudsonClearToolLauncher( PluginImpl.getDescriptor().getCleartoolExe(), Hudson.getInstance().getDescriptor(ClearCaseUcmBaselineSCM.class).getDisplayName(), listener, workspace, launcher); } /** * This method is a copy of {@link AbstractClearCaseScm#generateNormalizedViewName} * which is unfortunately not static. * * @see AbstractClearCaseScm#generateNormalizedViewName */ private String generateNormalizedViewName(VariableResolver variableResolver, String viewName) { String normalizedViewName = Util.replaceMacro(viewName, variableResolver); normalizedViewName = normalizedViewName.replaceAll("[\\s\\\\\\/:\\?\\*\\|]+", "_"); return normalizedViewName; } /** * This method is the one which actually does the ClearCase stuff * (creating the view, setting the config spec, downloading the * view, etc.). * * <p>This method is invoked when the user clicks on the 'Build' * button appearing on the parameters page.</p> */ @Override public Environment setUp(AbstractBuild build, final Launcher launcher, BuildListener listener) throws IOException, InterruptedException { // we use our own variable resolver to have the support for // the CLEARCASE_BASELINE env variable (cf. HUDSON-6410) VariableResolver variableResolver = new BuildVariableResolver(build, launcher, listener, baseline); ClearToolLauncher clearToolLauncher = createClearToolLauncher(listener, build.getProject().getWorkspace(), launcher); ClearToolUcmBaseline cleartool = new ClearToolUcmBaseline(variableResolver, clearToolLauncher); viewName = generateNormalizedViewName(variableResolver, viewName); FilePath workspace = build.getProject().getWorkspace(); FilePath viewPath = workspace.child(viewName); StringBuilder configSpec = new StringBuilder(); // --- 0. Has the same baseline been retrieved during last execution? --- boolean lastBuildUsedSameBaseline = false; if(forceRmview == false) { // we care only if we don't want to remove the existing view ClearCaseUcmBaselineParameterValue lastCcParamValue = null; // tons of loops and ifs to find the ClearCaseUcmBaselineParameterValue of the last build, if any List<Run> builds = build.getProject().getBuilds(); if(builds.size() > 1) { Run latestBuild = builds.get(1); // builds.get(0) is the currently running build List<ParametersAction> actions = latestBuild.getActions(ParametersAction.class); if(actions != null) { for(ParametersAction action : actions) { List<ParameterValue> parameters = action.getParameters(); if(parameters != null) { for(ParameterValue parameter : parameters) { if(parameter instanceof ClearCaseUcmBaselineParameterValue) { lastCcParamValue = (ClearCaseUcmBaselineParameterValue) parameter; // there can be only one time this kind of parameter, so let's break break; } } } if(lastCcParamValue != null) { // there can be only one time this kind of parameter, so let's break here too break; } } } } if(lastCcParamValue != null) { if(pvob.equals(lastCcParamValue.pvob) && component.equals(lastCcParamValue.component) && baseline.equals(lastCcParamValue.baseline)) { // the baseline used in the latest build is the same as the newly requested one lastBuildUsedSameBaseline = true; } } } final String newlineForOS = launcher.isUnix() ? "\n" : "\r\n"; // we assume that the slave OS file separator is the same as the server // since we have no way of determining the OS of the Clearcase server final String fileSepForOS = PathUtil.fileSepForOS(launcher.isUnix()); if(forceRmview || !lastBuildUsedSameBaseline || !viewPath.exists()) { // --- 1. We remove the view if it already exists --- if(viewPath.exists()) { if(!useUpdate || forceRmview) { cleartool.rmview(viewName); // --- 2. We create the view to be loaded --- cleartool.mkview(viewName, mkviewOptionalParam, snapshotView, null); } } else { cleartool.mkview(viewName, mkviewOptionalParam, snapshotView, null); } // --- 3. We create the configspec --- if(!excludeElementCheckedout) { configSpec.append("element * CHECKEDOUT").append(newlineForOS); } Set<String> loadRules = new HashSet<String>(); // we use a Set to avoid duplicate load rules (cf. HUDSON-6398) // cleartool lsbl -fmt "%[depends_on_closure]p" <baseline>@<pvob> String[] dependentBaselines = cleartool.getDependentBaselines(pvob, baseline); // we add the selected baseline at the beginning of the dependentBaselines // array so that the "element" and "load" sections of the config spec are // generated for this baseline dependentBaselines = (String[]) ArrayUtils.add(dependentBaselines, 0, baseline + '@' + pvob); for(String dependentBaselineSelector: dependentBaselines) { int indexOfSeparator = dependentBaselineSelector.indexOf('@'); if(indexOfSeparator == -1) { if(LOGGER.isLoggable(Level.INFO)) { LOGGER.info("Ignoring dependent baseline '" + dependentBaselineSelector + '\''); } continue; } String dependentBaseline = dependentBaselineSelector.substring(0, indexOfSeparator); String component = cleartool.getComponentFromBaseline(pvob, dependentBaseline); String componentRootDir = cleartool.getComponentRootDir(pvob, component); // some components may be rootless: they must simply be skipped (cf. HUDSON-6398) if(StringUtils.isBlank(componentRootDir)) { continue; } // example of generated config spec "element": // element /xxx/spd_comp/... spd_comp_v1.x_20100402100000 -nocheckout configSpec.append("element \"").append(componentRootDir).append(fileSepForOS).append("...\" ").append(dependentBaseline).append(" -nocheckout").append(newlineForOS); // is any download restriction defined? if(restrictions != null && restrictions.size() > 0) { for(String restriction: restrictions) { // the comparison must not take into account path separators, // so let's unify them to / for that purpose String restrictionForComparison = restriction.replace('\\', '/'); String componentRootDirForComparison = componentRootDir.replace('\\', '/'); if(restrictionForComparison.startsWith(componentRootDirForComparison)) { // example of generated config spec "load": // load /xxx/spd_comp/src loadRules.add("load " + restriction); } } } else { loadRules.add("load " + componentRootDir); } } configSpec.append("element * /main/0 -ucm -nocheckout").append(newlineForOS); for(String loadRule: loadRules) { configSpec.append(loadRule).append(newlineForOS); } listener.getLogger().println("The view will be created based on the following config spec:"); listener.getLogger().println("--- config spec start ---"); listener.getLogger().print(configSpec.toString()); listener.getLogger().println("--- config spec end ---"); // --- 4. We actually load the view based on the configspec --- // cleartool setcs <configspec> cleartool.setcs(viewName, configSpec.toString()); } else { listener.getLogger().println("The requested ClearCase UCM baseline is the same as previous build: Reusing previously loaded view"); } // --- 5. Create the environment variables --- return new Environment() { @Override public void buildEnvVars(Map<String, String> env) { env.put(ClearCaseUcmBaselineSCM.CLEARCASE_BASELINE_ENVSTR, baseline); env.put(AbstractClearCaseScm.CLEARCASE_VIEWNAME_ENVSTR, viewName); env.put(AbstractClearCaseScm.CLEARCASE_VIEWPATH_ENVSTR, env.get("WORKSPACE") + fileSepForOS + viewName); } }; } }; } else { return new BuildWrapper() { /** * This method makes the build fail when a {@link ClearCaseUcmBaselineParameterDefinition} * parameter is defined for the job, but the SCM is not an instance * of {@link ClearCaseUcmBaselineSCM}. */ @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) throws IOException, InterruptedException { String ccUcmBaselineSCMDisplayName = Hudson.getInstance().getDescriptor(ClearCaseUcmBaselineSCM.class).getDisplayName(); listener.fatalError( "This job is not set up to use a '" + ccUcmBaselineSCMDisplayName + "' SCM while it has a '" + Hudson.getInstance().getDescriptor(ClearCaseUcmBaselineParameterDefinition.class).getDisplayName() + "' parameter: Either remove the parameter or set the SCM to be '" + ccUcmBaselineSCMDisplayName + "'; In the meantime: Aborting!"); return null; } }; } } public String getBaseline() { return baseline; } public void setBaseline(String baseline) { this.baseline = baseline; } public String getComponent() { return component; } public void setComponent(String component) { this.component = component; } public boolean getExcludeElementCheckedout() { return excludeElementCheckedout; } public void setExcludeElementCheckedout(boolean excludeElementCheckedout) { this.excludeElementCheckedout = excludeElementCheckedout; } public boolean getForceRmview() { return forceRmview; } public void setForceRmview(boolean forceRmview) { this.forceRmview = forceRmview; } public String getMkviewOptionalParam() { return mkviewOptionalParam; } public void setMkviewOptionalParam(String mkviewOptionalParam) { this.mkviewOptionalParam = mkviewOptionalParam; } public String getPromotionLevel() { return promotionLevel; } public void setPromotionLevel(String promotionLevel) { this.promotionLevel = promotionLevel; } public String getPvob() { return pvob; } public void setPvob(String pvob) { this.pvob = pvob; } public void setRestrictions(List<String> restrictions) { this.restrictions = restrictions; } public boolean getSnapshotView() { return snapshotView; } public void setSnapshotView(boolean snapshotView) { this.snapshotView = snapshotView; } public String getStream() { return stream; } public void setStream(String stream) { this.stream = stream; } public boolean getUseUpdate() { return useUpdate; } public void setUseUpdate(boolean useUpdate) { this.useUpdate = useUpdate; } public String getViewName() { return viewName; } public void setViewName(String viewName) { this.viewName = viewName; } private final static Logger LOGGER = Logger.getLogger(ClearCaseUcmBaselineParameterValue.class.getName()); }