/* * Sonar is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * Sonar is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with Sonar; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 */ package hudson.plugins.sonar; import hudson.*; import hudson.maven.AbstractMavenProject; import hudson.maven.MavenModule; import hudson.maven.MavenModuleSet; import hudson.maven.ModuleName; import hudson.model.*; import hudson.plugins.sonar.model.LightProjectConfig; import hudson.plugins.sonar.model.TriggersConfig; import hudson.plugins.sonar.template.SonarPomGenerator; import hudson.plugins.sonar.utils.MagicNames; import hudson.plugins.sonar.utils.SonarMaven; import hudson.tasks.*; import hudson.tasks.Maven.MavenInstallation; import hudson.util.FormValidation; import net.sf.json.JSONObject; import org.apache.commons.lang.StringUtils; import org.apache.maven.model.Model; import org.apache.maven.model.io.xpp3.MavenXpp3Reader; import org.codehaus.plexus.util.xml.pull.XmlPullParserException; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; import java.io.InputStreamReader; import java.util.Arrays; import java.util.List; import java.util.logging.Logger; /** * Old fields should be left so that old config data can be read in, but * they should be deprecated and transient so that they won't show up in XML * when writing back */ public class SonarPublisher extends Notifier { private static final Logger LOG = Logger.getLogger(SonarPublisher.class.getName()); /** * Store a config version, so we're able to migrate config on various * functionality upgrades. */ private Integer configVersion; /** * Sonar installation name. */ private final String installationName; /** * Optional. * * @since 1.4 */ private String branch; /** * Optional. */ private final String mavenOpts; /** * Optional. */ private final String jobAdditionalProperties; /** * Triggers. If null, then we should use triggers from {@link SonarInstallation}. * * @since 1.2 */ private TriggersConfig triggers; // ================================================= // Next fields available only for free-style projects private String mavenInstallationName; /** * @since 1.2 */ private String rootPom; /** * If not null, then we should generate pom.xml. * * @since 1.2 */ private LightProjectConfig lightProject; public SonarPublisher(String installationName, String jobAdditionalProperties, String mavenOpts) { this(installationName, new TriggersConfig(), jobAdditionalProperties, mavenOpts, null, null, null); } public SonarPublisher( String installationName, TriggersConfig triggers, String jobAdditionalProperties, String mavenOpts ) { this(installationName, triggers, jobAdditionalProperties, mavenOpts, null, null, null); } public SonarPublisher(String installationName, TriggersConfig triggers, String jobAdditionalProperties, String mavenOpts, String mavenInstallationName, String rootPom, LightProjectConfig lightProject ) { this(installationName, null, triggers, jobAdditionalProperties, mavenOpts, mavenInstallationName, rootPom, lightProject); } @DataBoundConstructor public SonarPublisher(String installationName, String branch, TriggersConfig triggers, String jobAdditionalProperties, String mavenOpts, String mavenInstallationName, String rootPom, LightProjectConfig lightProject ) { super(); this.configVersion = 1; this.installationName = installationName; this.branch = branch; // Triggers this.triggers = triggers; // Maven this.mavenOpts = mavenOpts; this.jobAdditionalProperties = jobAdditionalProperties; // Non Maven Project this.mavenInstallationName = mavenInstallationName; this.rootPom = rootPom; // Sonar Light this.lightProject = lightProject; } /** * Migrate data. * * @return this */ @SuppressWarnings({"UnusedDeclaration"}) public Object readResolve() { // Default unspecified to v0 if (configVersion == null) { configVersion = 0; } if (configVersion < 1) { // No migration - see http://jira.codehaus.org/browse/SONARPLUGINS-402 configVersion = 1; } return this; } /** * @return config version */ public Integer getConfigVersion() { return configVersion; } /** * @return name of {@link hudson.plugins.sonar.SonarInstallation} */ public String getInstallationName() { return installationName; } /** * @return MAVEN_OPTS */ public String getMavenOpts() { return mavenOpts; } /** * @return additional Maven options like "-Pprofile" and "-Dname=value" */ public String getJobAdditionalProperties() { return StringUtils.trimToEmpty(jobAdditionalProperties); } /** * @return true, if we should use triggers from {@link SonarInstallation} */ public boolean isUseGlobalTriggers() { return triggers == null; } public boolean isUseLocalTriggers() { return !isUseGlobalTriggers(); } /** * See <a href="http://docs.codehaus.org/display/SONAR/Advanced+parameters#Advancedparameters-ManageSCMbranches">Sonar Branch option</a>. * * @return branch * @since 1.4 */ public String getBranch() { return branch; } /** * @return triggers configuration */ public TriggersConfig getTriggers() { return triggers; } /** * @return name of {@link hudson.tasks.Maven.MavenInstallation} */ public String getMavenInstallationName() { return mavenInstallationName; } /** * Root POM. Should be applied only for free-style projects. * * @return Root POM */ public String getRootPom() { return StringUtils.trimToEmpty(rootPom); } /** * @return true, if we should generate pom.xml */ public boolean isUseSonarLight() { return lightProject != null; } /** * @return configuration for Sonar Light */ public LightProjectConfig getLightProject() { return lightProject; } @SuppressWarnings({"UnusedDeclaration"}) public static boolean isMavenBuilder(AbstractProject currentProject) { return currentProject instanceof MavenModuleSet; } /** * Returns list of configured Maven installations. This method used in UI. * * @return list of configured Maven installations */ @SuppressWarnings({"UnusedDeclaration"}) public static List<MavenInstallation> getMavenInstallations() { return Arrays.asList(Hudson.getInstance().getDescriptorByType(Maven.DescriptorImpl.class).getInstallations()); } public SonarInstallation getInstallation() { DescriptorImpl sonarDescriptor = Hudson.getInstance().getDescriptorByType(DescriptorImpl.class); if (StringUtils.isEmpty(getInstallationName()) && sonarDescriptor.getInstallations().length > 0) { return sonarDescriptor.getInstallations()[0]; } for (SonarInstallation si : sonarDescriptor.getInstallations()) { if (StringUtils.equals(getInstallationName(), si.getName())) { return si; } } return null; } private boolean isSkip(AbstractBuild<?, ?> build, BuildListener listener, SonarInstallation sonarInstallation) { final String skipLaunchMsg; if (sonarInstallation == null) { skipLaunchMsg = Messages.SonarPublisher_NoInstallation(getInstallationName(), Hudson.getInstance().getDescriptorByType(DescriptorImpl.class).getInstallations().length); } else if (sonarInstallation.isDisabled()) { skipLaunchMsg = Messages.SonarPublisher_InstallDisabled(sonarInstallation.getName()); } else if (isUseGlobalTriggers()) { skipLaunchMsg = sonarInstallation.getTriggers().isSkipSonar(build); } else { skipLaunchMsg = getTriggers().isSkipSonar(build); } if (skipLaunchMsg != null) { listener.getLogger().println(skipLaunchMsg); return true; } return false; } @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) { final SonarInstallation sonarInstallation = getInstallation(); if (isSkip(build, listener, sonarInstallation)) { return true; } build.addAction(new BuildSonarAction()); boolean sonarSuccess = executeSonar(build, launcher, listener, sonarInstallation); if (!sonarSuccess) { // returning false has no effect on the global build status so need to do it manually build.setResult(Result.FAILURE); } LOG.info("Sonar build completed: " + build.getResult()); return sonarSuccess; } public MavenModuleSet getMavenProject(AbstractBuild build) { return (build.getProject() instanceof MavenModuleSet) ? (MavenModuleSet) build.getProject() : null; } private String getPomName(AbstractBuild<?, ?> build) { String pomName; MavenModuleSet mavenModuleProject = getMavenProject(build); if (mavenModuleProject != null) { pomName = mavenModuleProject.getRootPOM(); } else { pomName = getRootPom(); } if (StringUtils.isEmpty(pomName)) { pomName = "pom.xml"; } return pomName; } private String getPomName(AbstractBuild<?, ?> build, BuildListener listener) throws IOException, InterruptedException { String pomName = getPomName(build); // TODO Godin: why we should expand it? // Expand, because pomName can be "${VAR}/pom.xml" EnvVars env = build.getEnvironment(listener); pomName = env.expand(pomName); return pomName; } private boolean executeSonar(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener, SonarInstallation sonarInstallation) { try { String pomName = getPomName(build); FilePath root = build.getModuleRoot(); if (isUseSonarLight()) { LOG.info("Generating " + pomName); SonarPomGenerator.generatePomForNonMavenProject(getLightProject(), root, pomName); } String mavenInstallationName = getMavenInstallationName(); if (isMavenBuilder(build.getProject())) { MavenModuleSet mavenModuleSet = getMavenProject(build); if (null != mavenModuleSet.getMaven().getName()) { mavenInstallationName = mavenModuleSet.getMaven().getName(); } } // Execute maven return SonarMaven.executeMaven(build, launcher, listener, mavenInstallationName, pomName, sonarInstallation, this); } catch (IOException e) { Util.displayIOException(e, listener); e.printStackTrace(listener.fatalError("command execution failed")); return false; } catch (InterruptedException e) { return false; } catch (Exception e) { e.printStackTrace(listener.fatalError("command execution failed")); return false; } } protected String getSonarUrl(AbstractProject<?, ?> project) { SonarInstallation sonarInstallation = getInstallation(); if (sonarInstallation == null) { return null; } String url = sonarInstallation.getServerLink(); if (project instanceof AbstractMavenProject) { // Maven Project AbstractMavenProject mavenProject = (AbstractMavenProject) project; if (mavenProject.getRootProject() instanceof MavenModuleSet) { MavenModuleSet mms = (MavenModuleSet) mavenProject.getRootProject(); MavenModule rootModule = mms.getRootModule(); if (rootModule != null) { ModuleName moduleName = rootModule.getModuleName(); url = sonarInstallation.getProjectLink( moduleName.groupId, moduleName.artifactId, getBranch() ); } } } /** * Free-style job: * If project was built by maven, then pom.xml already exists * If project wasn't built by maven, then there is should be generated pom.xml */ try { AbstractBuild<?, ?> lastBuild = project.getLastBuild(); if (lastBuild != null) { MavenXpp3Reader reader = new MavenXpp3Reader(); Model model = reader.read(new InputStreamReader(lastBuild.getWorkspace().child(getPomName(lastBuild)).read())); String groupId = model.getGroupId(); String artifactId = model.getArtifactId(); url = sonarInstallation.getProjectLink( groupId, artifactId, getBranch() ); } } catch (IOException e) { // ignore } catch (XmlPullParserException e) { // ignore } catch (NullPointerException e) { // ignore something in the line can be null for maven project // Model model = reader.read(new InputStreamReader(lastBuild.getWorkspace().child(getPomName(lastBuild)).read())); } return url; } @Override public Action getProjectAction(AbstractProject<?, ?> project) { return new ProjectSonarAction(getSonarUrl(project)); } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } @Extension public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { @CopyOnWrite private volatile SonarInstallation[] installations = new SonarInstallation[0]; //NOSONAR public DescriptorImpl() { super(SonarPublisher.class); load(); } @Override public String getDisplayName() { return "Sonar"; } @Override public String getHelpFile() { return MagicNames.PLUGIN_HOME + "/help.html"; } /** * @return all configured {@link hudson.plugins.sonar.SonarInstallation} */ public SonarInstallation[] getInstallations() { return installations; } public void setInstallations(SonarInstallation... installations) { this.installations = installations; save(); } @Override public boolean configure(StaplerRequest req, JSONObject json) { List<SonarInstallation> list = req.bindJSONToList(SonarInstallation.class, json.get("inst")); setInstallations(list.toArray(new SonarInstallation[list.size()])); return true; } @SuppressWarnings({"UnusedDeclaration", "ThrowableResultOfMethodCallIgnored"}) public FormValidation doCheckMandatory(@QueryParameter String value) { return StringUtils.isBlank(value) ? FormValidation.error(Messages.SonarPublisher_MandatoryProperty()) : FormValidation.ok(); } @SuppressWarnings({"UnusedDeclaration", "ThrowableResultOfMethodCallIgnored"}) public FormValidation doCheckMandatoryAndNoSpaces(@QueryParameter String value) { return (StringUtils.isBlank(value) || value.contains(" ")) ? FormValidation.error(Messages.SonarPublisher_MandatoryPropertySpaces()) : FormValidation.ok(); } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { // eventually check if job type of FreeStyleProject.class || MavenModuleSet.class return true; } } }