VYPR
Moderate severityNVD Advisory· Published Jan 13, 2021· Updated Aug 3, 2024

CVE-2021-21613

CVE-2021-21613

Description

Jenkins TICS Plugin 2020.3.0.6 and earlier does not escape TICS service responses, resulting in a cross-site scripting (XSS) vulnerability exploitable by attackers able to control TICS service response content.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Jenkins TICS Plugin 2020.3.0.6 and earlier does not escape service responses, leading to stored XSS when an attacker controls the TICS service.

Vulnerability

Overview CVE-2021-21613 is a cross-site scripting (XSS) vulnerability in the Jenkins TICS Plugin, affecting versions 2020.3.0.6 and earlier [3]. The plugin fails to escape content received from the TICS service before rendering it in the Jenkins UI [1]. This allows an attacker who can control or manipulate the TICS service response to inject arbitrary HTML and JavaScript into the Jenkins interface.

Exploitation

To exploit this vulnerability, an attacker must have the ability to influence the content of a TICS service response [1]. This could be achieved by compromising the TICS server, performing a man-in-the-middle attack on the communication between Jenkins and the TICS service, or by tricking a Jenkins administrator into configuring a malicious TICS endpoint [4]. No authentication is required beyond the existing Jenkins session of a user viewing a page that displays TICS data, making the attack surface relatively broad for users with access to affected Jenkins views.

Impact

Successful exploitation results in stored XSS, meaning the injected script executes in the browser of any user who views the affected Jenkins page [1][3]. An attacker could use this to steal session cookies, redirect users to malicious sites, or perform actions on behalf of an authenticated Jenkins user. The Jenkins Security Advisory rates this as a high-severity issue due to the potential for privilege escalation within the Jenkins environment [1].

Mitigation

The vulnerability is fixed in TICS Plugin version 2020.3.0.7 and later [1]. Administrators should update the plugin immediately. There is no workaround documented; ensuring the TICS service endpoint is trusted and the communication channel is secured can reduce the risk of exploitation, but upgrading is the only complete mitigation [2][4].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
io.jenkins.plugins:ticsMaven
< 2020.3.0.72020.3.0.7

Affected products

3

Patches

1
a64493ccf81a

[SECURITY-2098]

https://github.com/jenkinsci/tics-pluginDennie ReniersDec 8, 2020via ghsa
19 files changed · +661 944
  • Development Readme.md+0 118 removed
    @@ -1,118 +0,0 @@
    -Developing the "TICS Publisher" Jenkins plugin
    
    -==============================================
    
    -
    
    -Getting started
    
    ----------------
    
    --   Start Eclipse
    
    --   Choose File -> Import -> Existing Maven project to import the JenkinsPlugin project
    
    --   Right click project -> Maven -> Update Project
    
    --   Right-click the project and choose Run As -> Maven Build
    
    --   In "Goals" type hpi:run
    
    --   Wait a long time while Eclipse is downloading dependencies and storing them in C:/Users/dreniers/m2 repository
    
    --   Eventually a Jetty HTTP server should be running on "localhost:8080" or "localhost:8080/jenkins" serving Hudson/Jenkins
    
    --   Go to http://localhost:8080/jenkins/pluginManager/installed : our plugin named "TICS Publisher" should be listed
    
    --   However, you might see that although plugin is loaded, as it was listed in installed plugins, there is no post-build step available
    
    -    that is called "Publish TICS results".
    
    -    Cause: Eclipse by default does not do annotation processing (a JDK6 feature). The annotations @DataBoundConstructor and @Extension
    
    -    should be processed by the annotation processor, but these are not processed when Eclipse builds the java files.
    
    -    They _are_ processed when maven does the build.
    
    --   If you did build using maven, but you do not see the TICS plugin, try an "mvn clean -e -X" followed by an "mvn compile hpi:run".
    
    --   Disable automatic Eclipse building ("Project->Build automatically") and instead create a "Run Configuration", by right-clicking project and choosing "Run As" -> "Maven Build ...". Enter "compile hpi:run" as parameter.
    
    --   Now each time you make changes to the code. 1) Stop the local Jenkins Jetty server in Eclipse's Debug panel (Ctrl+3 -> Debug). 2) Run the mvn compile hpi:run task you created (Alt+Shift+X -> M) and wait for about 10 seconds
    
    --   Hint: for changes to Jelly files you do not have to restart Jenkins.
    
    -
    
    -
    
    -Deployment
    
    -----------
    
    -- Do not forget to update the version number in the build.gradle file   
    
    -- The plugin should automatically be published on the download site
    
    -- To deploy the plugin in Jenkins, go to "http://192.168.1.88:8080/pluginManager/advanced" and choose Upload Plugin.
    
    -
    
    -
    
    -Gradle-specific information
    
    ----------------------------
    
    -- Building the tics.hpi file can be done using the 'jpi' task
    
    -- Starting a local Jetty server can be done using the 'server' task
    
    -- The server task appears to hang at 83%, but it's actually running a server at port 8080 on localhost!
    
    -- For faster debugging start the server with the following command: 'gradlew -Dstapler.trace=false -Dstapler.jelly.noCache=false -Ddebug.YUI=false server'
    
    -- Updating the version of the plugin is done automatically
    
    -
    
    -
    
    -Frequently Asked Questions
    
    ---------------------------
    
    -### I want to remove the link to my plugin in the left-hand sidebar
    
    -
    
    -Return null in getIconName()
    
    -
    
    -### What is the ${%Foo} syntax?
    
    -
    
    -This is for internationalization
    
    -
    
    -### I get "Caused by: java.lang.AssertionError: class hudson.plugins.tics.TicsPublisher is missing its descriptor"
    
    -
    
    -Occurs when Eclipse builds the project, either due to "Build automatically" being enabled, or by doing a Clean from Eclipse. Disable it, do a Clean using Maven, and build again using Maven.
    
    -
    
    -### I get an error while running mvn package during the tests "org.jvnet.hudson.test.JellyTestSuiteBuilder$JellyTestSuite(org.jvnet.hudson.test.junit.FailedTest)java.io.IOException: Failed to clean up temp dirs"
    
    -
    
    -I tried suggestions from  http://stackoverflow.com/questions/20611211/jenkins-plugin-build-error, but this did not seem to work. In the end, I just disabled the tests by clicking the checkbox "Skip tests" in the Eclipse -> Run As -> Maven Build ...
    
    -
    
    -### I want to add a dependency to a 3rd party library
    
    -
    
    -Lookup the maven artifactId and groupId and add it to dependencies section of the POM, e.g.:
    
    -
    
    -    <dependencies>
    
    -        <dependency>
    
    -        <groupId>com.google.code.gson</groupId>
    
    -        <artifactId>gson</artifactId>
    
    -        <version>2.3.1</version>
    
    -        </dependency>
    
    -    </dependencies>
    
    -
    
    -### How to get something on the right-hand on the project page?
    
    -
    
    -Create a floatingBox.jelly for the project action. Note that this does not work for the build action.  
    
    -
    
    -### How to reuse Jelly code?
    
    -
    
    -Use the following syntax. The class name is looked up using Java's Class.forName(). 
    
    -
    
    -    <st:include page="table.jelly" class="hudson.plugins.tics.TicsPublisher"/>
    
    -
    
    -### When creating a new job in jenkins, I get an exception "Array index out of range: -1" at com.thoughtworks.xstream.core.util.OrderRetainingMap.entrySet(OrderRetainingMap.java:77)
    
    -
    
    -On account of this https://issues.jenkins-ci.org/browse/JENKINS-19031 seems to be a problem in the XStream library:
    
    -
    
    -    "XSTR-739 and XSTR-746: OrderRetainingMap fails if HashMap.putAll(Map) of Java Runtime is not implemented calling put for every element within the map."    
    
    -
    
    -The reason that why it occurs now and not before is that we switched to Java8. The solution I took is to switch back to Java6, which is better anyway for users that do not have JRE8 yet. An alternative would have been to switch Jenkins version to 1.557.
    
    -
    
    -
    
    -### Eclipse gives a compiler errors, such as on getIconFileName(): "Must override a method"
    
    -
    
    -Make sure you do a Maven Update by right-clicking the project and chosing Mave -> Update Project.
    
    -
    
    -
    
    -### I get "Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/maven/cli/MavenCli : Unsupported major.minor version 51.0" when doing "mvn compile hpi:run"
    
    -
    
    -I have no idea. Remove the "compile" target or building from the command line.
    
    -
    
    -
    
    -
    
    -Useful reading
    
    ---------------
    
    -General plugin development:
    
    -
    
    -- [https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins](https://wiki.jenkins-ci.org/display/JENKINS/Extend+Jenkins)
    
    -- [http://hudson-ci.org/docs/HudsonArch-View.pdf](http://hudson-ci.org/docs/HudsonArch-View.pdf)
    
    -- [http://javaadventure.blogspot.nl/2008/02/writing-hudson-plug-in-part-5-reporting.html](http://javaadventure.blogspot.nl/2008/02/writing-hudson-plug-in-part-5-reporting.html)
    
    -- [https://jenkins-ci.org/maven-site/jenkins-core/jelly-taglib-ref.html#form:block] Jelly tags
    
    - 
    
    -
    
    -Example plugins:
    
    -
    
    -- [Hello World plugin](https://github.com/jenkinsci/hello-world-plugin) (on which our plugin was based)
    
    -- [Clover Coverage](https://github.com/atlassian/clover-jenkins-plugin/blob/master/src/main/java/hudson/plugins/clover/CloverPublisher.java) (mentioned in ticket 14915)
    
    -- [Jacoco Coverage](https://github.com/jenkinsci/jacoco-plugin/tree/master/src/main/resources/hudson/plugins/jacoco)
    
    -- [Disk usage](https://github.com/jenkinsci/disk-usage-plugin/blob/master/src/main/resources/hudson/plugins/disk_usage/DiskUsagePlugin/index.jelly)
    
    -
    
    -
    
    
  • Jenkinsfile+6 1 modified
    @@ -1 +1,6 @@
    -buildPluginWithGradle()
    +// Builds a module using https://github.com/jenkins-infra/pipeline-library
    +buildPlugin(useAci: true, configurations: [
    +        [ platform: "linux", jdk: "8" ],
    +        [ platform: "windows", jdk: "8" ],
    +        [ platform: "linux", jdk: "11" ]
    +])
    
  • makefile+0 50 removed
    @@ -1,50 +0,0 @@
    -# $Id$
    -# This file is part of the overall build.
    -.SILENT:
    -.SUFFIXES:
    -
    -.PHONY: all check clean build clean server rebuild package relnotes clean_relnotes publish
    -
    -DAEMON := --no-daemon # to prevent using the Gradle Daemon in CI
    -SVNVERSION := $(shell svn info . | sed -n "s/Last Changed Rev: //p")
    -GRADLE := $(CURDIR)/gradlew -PSVNVERSION="$(SVNVERSION)" $(DAEMON)
    -TOOL := TICSJenkins
    -
    -all: build check
    -
    -build:
    -	$(GRADLE) jpi
    -
    -check:
    -	$(GRADLE) check -x test
    -
    -clean: clean_relnotes
    -	$(GRADLE) clean
    -
    -rebuild: clean all
    -
    -server:
    -	$(GRADLE) server
    -
    -TICSVERSION=$(shell cat ../../make/TICSVERSION)
    -
    -package: build
    -
    -# The SVN repository number from which revisions onwards one must
    -# collect release notes.
    -STARTREV := 28789
    -relnotes:
    -ifeq ($(OS),Windows_NT)
    -	svn log --xml -r $(SVNVERSION):$(STARTREV) | msxsl -o $(TOOL)-relnotes.html - svn-log.xslt
    -else
    -	svn log --xml -r $(SVNVERSION):$(STARTREV) | xsltproc -o $(TOOL)-relnotes.html svn-log.xslt -
    -endif
    -
    -clean_relnotes:
    -	rm -f $(TOOL)-relnotes.html
    -
    -DEST=absolem:/home/wilde/ticsweb/pub/plugins/jenkins
    -
    -publish: package relnotes
    -	scp build/libs/tics.hpi $(DEST)/$(TOOL)-$(TICSVERSION).$(SVNVERSION).hpi
    -	scp $(TOOL)-relnotes.html $(DEST)
    
  • pom.xml+109 64 modified
    @@ -16,8 +16,27 @@
             <changelist>-SNAPSHOT</changelist>
             <jenkins.version>2.164.3</jenkins.version>
             <java.level>8</java.level>
    +
    +        <!-- http://docs.codehaus.org/display/MAVENUSER/POM+Element+for+Source+File+Encoding -->
    +        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    +        <jenkins.version>2.164.3</jenkins.version>
    +        <maven.compiler.source>1.8</maven.compiler.source>
    +        <maven.compiler.target>1.8</maven.compiler.target>
    +        <java.level>8</java.level>
    +        <hpi.compatibleSinceVersion>2020.3.0.6</hpi.compatibleSinceVersion>
    +		
             <!-- define all plugin versions -->
    +        <maven-clean-plugin.version>2.4.1</maven-clean-plugin.version>
    +        <maven-compiler-plugin.version>2.3.2</maven-compiler-plugin.version>
    +        <maven-dependency-plugin.version>2.1</maven-dependency-plugin.version>
    +        <maven-jar-plugin.version>2.3.1</maven-jar-plugin.version>
    +        <maven-release-plugin.version>2.1</maven-release-plugin.version>
    +        <maven-resources-plugin.version>2.4.3</maven-resources-plugin.version>
    +        <maven-site-plugin.version>2.1.1</maven-site-plugin.version>
    +        <maven-source-plugin.version>2.1.2</maven-source-plugin.version>
    +        <maven-surefire-plugin.version>2.7.2</maven-surefire-plugin.version>
         </properties>
    +	
         <name>TICS Plugin</name>
         <url>https://github.com/jenkinsci/${project.artifactId}-plugin</url>
         <licenses>
    @@ -102,74 +121,100 @@
                 <version>27.0.1-jre</version>
             </dependency>
             <dependency>
    -            <groupId>net.jcip</groupId>
    -            <artifactId>jcip-annotations</artifactId>
    -            <version>1.0</version>
    -            <optional>true</optional>
    -        </dependency>
    -        <dependency>
    -            <groupId>org.apache.maven.plugins</groupId>
    -            <artifactId>maven-deploy-plugin</artifactId>
    -            <version>2.8.2</version>
    +            <groupId>org.jsoup</groupId>
    +            <artifactId>jsoup</artifactId>
    +            <version>1.13.1</version>
             </dependency>
         </dependencies>
     
         <build>
    -        <plugins>
    -            <plugin>
    -                <artifactId>maven-resources-plugin</artifactId>
    -                <executions>
    -                    <execution>
    -                        <id>copy-common-files</id>
    -                        <phase>process-sources</phase>
    -                        <goals>
    -                            <goal>copy-resources</goal>
    -                        </goals>
    -                        <configuration>
    -                            <outputDirectory>${project.build.directory}/classes/archetype-resources</outputDirectory>
    -                            <resources>
    -                                <resource>
    -                                    <directory>${basedir}/../common-files</directory>
    -                                    <excludes>
    -                                        <exclude>archetype-post-generate.groovy</exclude>
    -                                    </excludes>
    -                                </resource>
    -                            </resources>
    -                        </configuration>
    -                    </execution>
    -                    <execution>
    -                        <id>copy-post-generate</id>
    -                        <phase>process-sources</phase>
    -                        <goals>
    -                            <goal>copy-resources</goal>
    -                        </goals>
    -                        <configuration>
    -                            <outputDirectory>${project.build.directory}/classes/META-INF</outputDirectory>
    -                            <resources>
    -                                <resource>
    -                                    <directory>${basedir}/../common-files</directory>
    -                                    <includes>
    -                                        <include>archetype-post-generate.groovy</include>
    -                                    </includes>
    -                                </resource>
    -                            </resources>
    -                        </configuration>
    -                    </execution>
    -                </executions>
    -            </plugin>
    -            <plugin>
    -                <groupId>org.apache.maven.plugins</groupId>
    -                <artifactId>maven-enforcer-plugin</artifactId>
    -                <executions>
    -                  <execution>
    -                    <phase>none</phase>
    -                  </execution>
    -                </executions>
    -                <configuration>
    -                  <skip>true</skip>
    -                </configuration>
    -            </plugin>
    -        </plugins>
    +        <pluginManagement>
    +            <plugins>
    +                <plugin>
    +                    <groupId>org.apache.maven.plugins</groupId>
    +                    <artifactId>maven-scm-plugin</artifactId>
    +                    <version>1.11.2</version>
    +                    <configuration>
    +                        <connectionType>developerConnection</connectionType>
    +                    </configuration>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-clean-plugin</artifactId>
    +                    <version>${maven-clean-plugin.version}</version>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-compiler-plugin</artifactId>
    +                    <version>${maven-compiler-plugin.version}</version>
    +                    <configuration>
    +                        <source>${compileSource}</source>
    +                        <target>${compileTarget}</target>
    +                        <showDeprecation>true</showDeprecation>
    +                        <showWarnings>true</showWarnings>
    +                    </configuration>
    +                </plugin>
    +                <plugin>
    +                    <groupId>org.apache.maven.plugins</groupId>
    +                    <artifactId>maven-enforcer-plugin</artifactId>
    +                    <executions>
    +                        <execution>
    +                            <phase>none</phase>
    +                        </execution>
    +                    </executions>
    +                    <configuration>
    +                        <skip>true</skip>
    +                    </configuration>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-jar-plugin</artifactId>
    +                    <version>${maven-jar-plugin.version}</version>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-release-plugin</artifactId>
    +                    <version>${maven-release-plugin.version}</version>
    +                    <configuration>
    +                        <allowTimestampedSnapshots>true</allowTimestampedSnapshots>
    +                        <autoVersionSubmodules>true</autoVersionSubmodules>
    +                        <goals>clean deploy</goals>
    +                        <preparationGoals>clean deploy</preparationGoals>
    +                        <releaseProfiles>release</releaseProfiles>
    +                    </configuration>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-resources-plugin</artifactId>
    +                    <version>${maven-resources-plugin.version}</version>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-site-plugin</artifactId>
    +                    <version>${maven-site-plugin.version}</version>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-source-plugin</artifactId>
    +                    <version>${maven-source-plugin.version}</version>
    +                </plugin>
    +                <plugin>
    +                    <artifactId>maven-surefire-plugin</artifactId>
    +                    <version>${maven-surefire-plugin.version}</version>
    +                    <configuration>
    +                        <parallel>methods</parallel>
    +                        <testFailureIgnore>true</testFailureIgnore>
    +                        <threadCount>4</threadCount>
    +                    </configuration>
    +                </plugin>
    +                <plugin>
    +                    <groupId>com.github.spotbugs</groupId>
    +                    <artifactId>spotbugs-maven-plugin</artifactId>
    +                    <version>4.1.3</version>
    +                    <executions>
    +                        <execution>
    +                            <phase>none</phase>
    +                        </execution>
    +                    </executions>
    +                    <configuration>
    +                        <skip>true</skip>
    +                    </configuration>
    +                </plugin>
    +            </plugins>
    +        </pluginManagement>
         </build>
     
     </project>
    
  • src/main/java/hudson/plugins/tics/AbstractApiCall.java+10 2 modified
    @@ -13,6 +13,7 @@
     import org.apache.http.auth.AuthScope;
     import org.apache.http.auth.UsernamePasswordCredentials;
     import org.apache.http.client.CredentialsProvider;
    +import org.apache.http.client.config.RequestConfig;
     import org.apache.http.impl.client.BasicCredentialsProvider;
     import org.apache.http.impl.client.CloseableHttpClient;
     import org.apache.http.impl.client.HttpClientBuilder;
    @@ -36,7 +37,14 @@ public AbstractApiCall(final String apiCallName, final PrintStream logger, final
         }
     
         protected final CloseableHttpClient createHttpClient() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
    -        HttpClientBuilder builder = HttpClients.custom();
    +        final int timeoutMs = 300 * 1000;
    +        final RequestConfig requestConfig = RequestConfig.custom()
    +                .setConnectTimeout(timeoutMs)
    +                .setSocketTimeout(timeoutMs)
    +                .setConnectionRequestTimeout(timeoutMs)
    +                .build();
    +        HttpClientBuilder builder = HttpClients.custom()
    +                .setDefaultRequestConfig(requestConfig);
             if (credentials.isPresent()) {
                 final String username = credentials.get().getUsername();
                 final String password = credentials.get().getPassword().getPlainText();
    @@ -68,7 +76,7 @@ protected void throwIfStatusNotOk(final HttpResponse response, final String body
                 throw new MeasureApiCallException(apiCallPrefix + " " + statusCode + " " + reason + " - " + formattedError.get());
             } else {
                 // Do not give body in exception, as it might contain html, potentially messing up the Jenkins view.
    -            logger.println(body);
    +            logger.println(apiCallPrefix + " " + body);
                 throw new MeasureApiCallException(apiCallPrefix + " " + statusCode + " " + reason + " - See the build log for a detailed error report.");
             }
         }
    
  • src/main/java/hudson/plugins/tics/HtmlTag.java+0 80 removed
    @@ -1,80 +0,0 @@
    -package hudson.plugins.tics;
    -
    -import java.util.Collection;
    -import java.util.Map.Entry;
    -
    -import com.google.common.base.Joiner;
    -import com.google.common.base.MoreObjects;
    -import com.google.common.collect.ArrayListMultimap;
    -import com.google.common.collect.ImmutableList;
    -
    -public class HtmlTag {
    -    private final String tag;
    -    private final ArrayListMultimap<String, String> attrs;
    -
    -    private HtmlTag(final String tag, final ArrayListMultimap<String, String> attrs) {
    -        this.tag = tag;
    -        this.attrs = attrs;
    -    }
    -
    -    public static HtmlTag from(final String tag) {
    -        return new HtmlTag(tag, ArrayListMultimap.<String, String>create());
    -    }
    -
    -    public HtmlTag attr(final String name, final String value) {
    -        return attr(name, ImmutableList.of(value));
    -    }
    -
    -    public HtmlTag attr(final String name, final Iterable<String> values) {
    -        final ArrayListMultimap<String, String> copy = ArrayListMultimap.create(attrs);
    -        copy.putAll(name, values);
    -        return new HtmlTag(tag, copy);
    -    }
    -
    -    public HtmlTag attrIf(final boolean state, final String key, final String value) {
    -        if (state) {
    -            return attr(key, value);
    -        } else {
    -            return this;
    -        }
    -    }
    -
    -    public String open() {
    -        final StringBuilder sb = new StringBuilder();
    -        sb.append("<");
    -        sb.append(this.tag);
    -        if (attrs != null) {
    -            for (final Entry<String, Collection<String>> entry : attrs.asMap().entrySet()) {
    -                sb.append(" ");
    -                final String valueSeparator;
    -                if ("style".equals(entry.getKey())) {
    -                    valueSeparator = "; ";
    -                } else if ("data-bind".equals(entry.getKey())) {
    -                    valueSeparator = ", ";
    -                } else {
    -                    valueSeparator = " ";
    -                }
    -                sb.append(entry.getKey() + "=\"" + Joiner.on(valueSeparator).join(entry.getValue()) + "\"");
    -            }
    -        }
    -        sb.append(">");
    -        return sb.toString();
    -    }
    -    public String close() {
    -        return "</" + tag + ">";
    -    }
    -
    -    public String openClose() {
    -        return open() + close();
    -    }
    -
    -    public String openClose(final String inner) {
    -        return open() + inner + close();
    -    }
    -
    -    @Override
    -    public String toString() {
    -        return MoreObjects.toStringHelper(this).add("tag", tag).toString();
    -    }
    -
    -}
    
  • src/main/java/hudson/plugins/tics/MeasureApiCall.java+4 12 modified
    @@ -11,7 +11,6 @@
     import org.apache.http.client.utils.URIBuilder;
     import org.apache.http.impl.client.CloseableHttpClient;
     import org.apache.http.util.EntityUtils;
    -import org.joda.time.Instant;
     
     import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
     import com.google.common.base.Preconditions;
    @@ -24,6 +23,7 @@
     import hudson.plugins.tics.MeasureApiSuccessResponse.TqiVersion;
     
     public class MeasureApiCall extends AbstractApiCall {
    +    public static final TypeToken<MeasureApiSuccessResponse<Number>> RESPONSE_NUMBER_TYPETOKEN = new TypeToken<MeasureApiSuccessResponse<Number>>(){/**/};
         public static final TypeToken<MeasureApiSuccessResponse<Double>> RESPONSE_DOUBLE_TYPETOKEN = new TypeToken<MeasureApiSuccessResponse<Double>>(){/**/};
         public static final TypeToken<MeasureApiSuccessResponse<TqiVersion>> RESPONSE_TQIVERSION_TYPETOKEN = new TypeToken<MeasureApiSuccessResponse<TqiVersion>>(){/**/};
         public static final TypeToken<MeasureApiSuccessResponse<List<Run>>> RESPONSE_RUNS_TYPETOKEN = new TypeToken<MeasureApiSuccessResponse<List<Run>>>(){/**/};
    @@ -54,22 +54,14 @@ public MeasureApiCall(final PrintStream logger, final String measureApiUrl, fina
         }
     
         public <T> T execute(final TypeToken<T> typeToken, final String paths, final String metrics) throws MeasureApiCallException {
    -        return execute(typeToken, paths, metrics, Optional.empty());
    -    }
    -
    -    public <T> T execute(final TypeToken<T> typeToken, final String paths, final String metrics, final Optional<Instant> date) throws MeasureApiCallException {
             URIBuilder builder;
             try {
    -            builder = new URIBuilder(measureApiUrl)
    +            builder = new URIBuilder(this.measureApiUrl)
                     .setParameter("nodes", paths)
                     .setParameter("metrics", metrics);
             } catch (final URISyntaxException e) {
                 throw new MeasureApiCallException("Invalid URL: " + e.getMessage());
             }
    -        if (date.isPresent()) {
    -            final long seconds = date.get().getMillis() / 1000;
    -            builder = builder.setParameter("dates", ""+seconds);
    -        }
             final String url;
             try {
                 url = builder.build().toString();
    @@ -84,11 +76,11 @@ public <T> T execute(final TypeToken<T> typeToken, final String paths, final Str
             logger.println(TicsPublisher.LOGGING_PREFIX + httpGet.toString());
     
             final String body;
    -        try (CloseableHttpClient httpclient = createHttpClient();
    +        try (CloseableHttpClient httpclient = this.createHttpClient();
                     CloseableHttpResponse response = httpclient.execute(httpGet);
                     ) {
                     body = EntityUtils.toString(response.getEntity());
    -                throwIfStatusNotOk(response, body);
    +                this.throwIfStatusNotOk(response, body);
             } catch (final ConnectException e) {
                 throw new MeasureApiCallException(e.getMessage());
             } catch (final Exception e) {
    
  • src/main/java/hudson/plugins/tics/MeasureApiSuccessResponse.java+1 0 modified
    @@ -15,6 +15,7 @@
     public class MeasureApiSuccessResponse<T> {
         public static class MetricValue<T> {
             public T value;
    +        public String status;
             public String letter;
             public String formattedValue;
         }
    
  • src/main/java/hudson/plugins/tics/MetricData.java+76 0 added
    @@ -0,0 +1,76 @@
    +package hudson.plugins.tics;
    +
    +import java.util.List;
    +
    +import javax.annotation.Nullable;
    +
    +import org.joda.time.Instant;
    +
    +import com.google.common.collect.ImmutableList;
    +
    +/**
    + * Holds metric data produced by {@link TicsPublisherResultBuilder}.
    + */
    +public class MetricData {
    +
    +    public static class MetricValue {
    +        public final String status;
    +        public final String formattedValue;
    +        public final String letter;
    +
    +        public MetricValue(
    +                final String status,
    +                final String formattedValue,
    +                final String letter
    +                ) {
    +            this.status = status;
    +            this.formattedValue = formattedValue;
    +            this.letter = letter;
    +        }
    +    }
    +
    +    public static class Run {
    +        public final String name;
    +        public final String description;
    +        public final List<String> metricNames;
    +        public final List<MetricValue> metricValues;
    +        /** Date of the run in ISO format */
    +        public final String date;
    +
    +        public Run(
    +                final String name,
    +                final String description,
    +                final List<String> metricNames,
    +                final List<MetricValue> metricValues,
    +                final String date
    +                ) {
    +            this.name = name;
    +            this.description = description;
    +            this.metricNames = metricNames;
    +            this.metricValues = metricValues;
    +            this.date = date;
    +        }
    +    }
    +
    +    public final String ticsPath;
    +    public final List<String> metrics;
    +    public final String measurementDate = Instant.now().toString();
    +    public final List<Run> runs;
    +    public final String errorMessage;
    +
    +    public MetricData(
    +            final List<String> metrics,
    +            final List<Run> runs,
    +            final String ticsPath,
    +            final @Nullable String errorMessage
    +            ) {
    +        this.metrics = metrics;
    +        this.runs = runs;
    +        this.ticsPath = ticsPath;
    +        this.errorMessage = errorMessage;
    +    }
    +
    +    public static MetricData error(final String ticsPath, final String message) {
    +        return new MetricData(ImmutableList.of(), ImmutableList.of(), ticsPath, message);
    +    }
    +}
    
  • src/main/java/hudson/plugins/tics/Metrics.java+2 1 modified
    @@ -165,7 +165,8 @@ public ImmutableList<String> getEnabledMetrics() {
                 try {
                     enabled = (Boolean)f.get(this);
                 } catch (final Exception e) {
    -                throw Throwables.propagate(e);
    +                Throwables.throwIfUnchecked(e);
    +                throw new RuntimeException(e);
                 }
                 if (enabled) {
                     out.add(f.getName());
    
  • src/main/java/hudson/plugins/tics/QualityGateApiCall.java+59 0 added
    @@ -0,0 +1,59 @@
    +package hudson.plugins.tics;
    
    +
    
    +import java.io.IOException;
    
    +import java.security.KeyManagementException;
    
    +import java.security.KeyStoreException;
    
    +import java.security.NoSuchAlgorithmException;
    
    +import java.util.Optional;
    
    +
    
    +import org.apache.http.client.methods.CloseableHttpResponse;
    
    +import org.apache.http.client.methods.HttpGet;
    
    +import org.apache.http.impl.client.CloseableHttpClient;
    
    +import org.apache.http.util.EntityUtils;
    
    +
    
    +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
    
    +import com.google.gson.Gson;
    
    +
    
    +import hudson.model.TaskListener;
    
    +import hudson.plugins.tics.MeasureApiCall.MeasureApiCallException;
    
    +
    
    +
    
    +public class QualityGateApiCall extends AbstractApiCall {
    
    +
    
    +    private static final String LOGGING_PREFIX = "[TICS Quality Gating] ";
    
    +    private final String qualityGateUrl;
    
    +    private final String project;
    
    +    private final String branch;
    
    +
    
    +    public QualityGateApiCall(final String qualityGateUrl, final String ticsPath, final Optional<StandardUsernamePasswordCredentials> credentials, final TaskListener listener) {
    
    +        super(LOGGING_PREFIX, listener.getLogger(), credentials);
    
    +        final String projectAndBranch = ticsPath.split("://")[1];
    
    +        final String[] parts = projectAndBranch.split("/", 2);
    
    +        this.project = parts[0];
    
    +        this.branch = parts[1];
    
    +        this.qualityGateUrl = qualityGateUrl;
    
    +    }
    
    +
    
    +    public QualityGateData retrieveQualityGateData() {
    
    +        final String url = this.qualityGateUrl + "?project=" + this.project + "&branch=" + this.branch;
    
    +        final String response = this.performHttpRequest(url);
    
    +
    
    +        final QualityGateApiResponse resp = new Gson().fromJson(response, QualityGateApiResponse.class);
    
    +        return QualityGateData.success(this.project, this.branch, resp);
    
    +    }
    
    +
    
    +    private String performHttpRequest(final String url) {
    
    +        final HttpGet httpGet = new HttpGet(url);
    
    +        try (final CloseableHttpClient httpclient = this.createHttpClient();
    
    +             final CloseableHttpResponse response = httpclient.execute(httpGet);
    
    +        ) {
    
    +            final String body = EntityUtils.toString(response.getEntity());
    
    +            this.throwIfStatusNotOk(response, body);
    
    +            return body;
    
    +        } catch (KeyManagementException | NoSuchAlgorithmException | KeyStoreException | IOException | MeasureApiCallException ex) {
    
    +            throw new RuntimeException("Error while performing API request to " + url, ex);
    
    +        }
    
    +    }
    
    +
    
    +
    
    +}
    
    
  • src/main/java/hudson/plugins/tics/QualityGateApiResponse.java+30 0 added
    @@ -0,0 +1,30 @@
    +package hudson.plugins.tics;
    +
    +import java.util.ArrayList;
    +import java.util.List;
    +
    +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
    +
    +/**
    + * Response model for 'api/public/v1/QualityGateStatus'.
    + */
    +@SuppressFBWarnings({"UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD"})
    +public class QualityGateApiResponse {
    +    public static class Condition {
    +        public boolean passed;
    +        public boolean error;
    +        public String message;
    +    }
    +
    +    public static class Gate {
    +        public boolean passed;
    +        public String name;
    +        public List<Condition> conditions = new ArrayList<>();
    +    }
    +
    +    public boolean passed;
    +    public String message;
    +    public String url;
    +    public List<Gate> gates = new ArrayList<>();
    +
    +}
    
  • src/main/java/hudson/plugins/tics/QualityGateData.java+42 0 added
    @@ -0,0 +1,42 @@
    +package hudson.plugins.tics;
    +
    +import java.util.Optional;
    +
    +import javax.annotation.Nullable;
    +
    +import org.joda.time.Instant;
    +
    +/**
    + * Holds data produced by {@link @QualityGateApiCall}.
    + */
    +public class QualityGateData {
    +    public final @Nullable String project;
    +    public final @Nullable String branch;
    +    public final @Nullable QualityGateApiResponse apiResponse;
    +    public final String measurementDate = Instant.now().toString();
    +    public final @Nullable String errorMessage;
    +    /** True when ApiCall was successful and project passed quality gate. */
    +    public final boolean passed;
    +
    +    public QualityGateData(
    +            final @Nullable String project,
    +            final @Nullable String branch,
    +            final @Nullable QualityGateApiResponse apiResponse,
    +            final @Nullable String errorMessage
    +            ) {
    +        this.project = project;
    +        this.branch = branch;
    +        this.apiResponse = apiResponse;
    +        this.errorMessage = errorMessage;
    +        this.passed = Optional.ofNullable(apiResponse).map(resp -> resp.passed).orElse(false);
    +    }
    +
    +    public static QualityGateData error(final String message) {
    +        return new QualityGateData(null, null, null, message);
    +    }
    +
    +    public static QualityGateData success(final String project, final String branch, final QualityGateApiResponse apiResponse) {
    +        return new QualityGateData(project, branch, apiResponse, null);
    +    }
    +
    +}
    \ No newline at end of file
    
  • src/main/java/hudson/plugins/tics/TicsPublisherBuildAction.java+58 33 modified
    @@ -7,19 +7,16 @@
     import java.util.List;
     
     import org.apache.http.client.utils.URIBuilder;
    -import org.joda.time.Instant;
    +import org.joda.time.DateTime;
     
    -import com.google.common.base.Strings;
    -import com.google.common.base.Throwables;
    +import com.google.common.collect.ImmutableMap;
     
     import hudson.model.AbstractProject;
     import hudson.model.Action;
     import hudson.model.Descriptor;
     import hudson.model.ProminentProjectAction;
     import hudson.model.Run;
     import hudson.plugins.tics.TicsPublisher.InvalidTicsViewerUrl;
    -import hudson.plugins.tics.TicsQualityGate.QualityGateResult;
    -import hudson.plugins.tics.TqiPublisherResultBuilder.TqiPublisherResult;
     import hudson.tasks.Publisher;
     import hudson.util.DescribableList;
     import jenkins.tasks.SimpleBuildStep;
    @@ -37,27 +34,21 @@ public class TicsPublisherBuildAction extends AbstractTicsPublisherAction implem
          * I tried adding the publisher as a field here, but that just stores the fields, not the reference.
          **/
         private final Run<?, ?> run;
    -    public final String tableHtml;
    -    public final String ticsPath;
    -    public final String measurementDate;
    -    public final String qualityGateTableHtml;
    -    public final String qualityGateViewerUrl;
    +    public final MetricData tqiData;
    +    public final QualityGateData gateData;
         private final String tiobeWebBaseUrl;
     
         private final List<TicsPublisherProjectAction> projectActions;
     
         public TicsPublisherBuildAction(
                 final Run<?, ?> run,
    -            final TqiPublisherResult tqiPublisherResult,
    -            final QualityGateResult qualityGateResult,
    +            final MetricData tqiData,
    +            final QualityGateData QualityGateData,
                 final String tiobeWebBaseUrl
         ) {
             this.run = run;
    -        this.tableHtml = tqiPublisherResult != null ? tqiPublisherResult.tableHtml : null;
    -        this.ticsPath = tqiPublisherResult != null ? tqiPublisherResult.ticsPath : null;
    -        this.measurementDate = tqiPublisherResult != null ? tqiPublisherResult.measurementDate : null;
    -        this.qualityGateTableHtml = qualityGateResult != null ? qualityGateResult.tableHtml : null;
    -        this.qualityGateViewerUrl = qualityGateResult != null ? qualityGateResult.viewerGateUrl : null;
    +        this.tqiData = tqiData;
    +        this.gateData = QualityGateData;
             final List<TicsPublisherProjectAction> actions = new ArrayList<>();
             actions.add(new TicsPublisherProjectAction(run));
             this.projectActions = actions;
    @@ -71,13 +62,6 @@ public String getIconFileName() {
             return null;
         }
     
    -    public String getMeasurementDate() {
    -        if (Strings.isNullOrEmpty(measurementDate)) {
    -            return "?";
    -        }
    -        return new Instant(measurementDate).toDateTime().toString("yyyy-MM-dd HH:mm");
    -    }
    -
         /**
          * From: http://grepcode.com/file_/repo1.maven.org/maven2/org.hudsonci.plugins/artifactory/2.1.3-h-2/org/jfrog/hudson/action/ActionableHelper.java/?v=source
          *
    @@ -98,17 +82,13 @@ public static <T extends Publisher> T getPublisher(final Run<?, ?> run, final Cl
             return null;
         }
     
    -    public final String getViewerQualityGateDetails() {
    -        if (this.qualityGateViewerUrl == null) {
    -            return "";
    -        }
    -        final String escapedQualityGateViewerUrl = "/" + this.qualityGateViewerUrl.replace("(", "%28").replace(")", "%29");
    -        return openInViewerUrl(escapedQualityGateViewerUrl, "");
    -    }
     
         public final String getOpenInViewerUrl() {
    +        if (this.tqiData == null) {
    +            return null;
    +        }
             final String dashboardFilePath = "/TqiDashboard.html";
    -        final String fragment = "axes=" + this.ticsPath;
    +        final String fragment = "axes=" + tqiData.ticsPath;
             return openInViewerUrl(dashboardFilePath, fragment);
         }
     
    @@ -135,14 +115,59 @@ public final String openInViewerUrl(final String link, final String fragment) {
                         .setFragment(fragment)
                         .build();
             } catch (final URISyntaxException e) {
    -            throw Throwables.propagate(e);
    +            throw new RuntimeException(e);
             }
             return uri.toString();
    +    }
    +
    +    public String getLetterForegroundColor(final String letter) {
    +        final ImmutableMap<String, String> colors = ImmutableMap.<String, String>builder()
    +                .put("A", "white")
    +                .put("B", "white")
    +                .put("C", "black")
    +                .put("D", "black")
    +                .put("E", "white")
    +                .put("F", "white")
    +                .build();
    +        return colors.getOrDefault(letter, "black");
    +    }
    +
    +    public String getLetterBackgroundColor(final String letter) {
    +        final ImmutableMap<String, String> colors = ImmutableMap.<String, String>builder()
    +                .put("A", "#006400")
    +                .put("B", "#64AE00")
    +                .put("C", "#FFFF00")
    +                .put("D", "#FF950E")
    +                .put("E", "#FF420E")
    +                .put("F", "#BE0000")
    +                .build();
    +        return colors.getOrDefault(letter, "#CCC");
    +    }
    +
    +    public String formatDate(final String date) {
    +        try {
    +            return new DateTime(date).toString("YYYY-MM-dd HH:mm:ss");
    +        } catch (final IllegalArgumentException ex) {
    +            return "-";
    +        }
    +    }
     
    +    public long countConditions(final QualityGateApiResponse.Gate gate, final boolean passed) {
    +        return gate.conditions.stream().filter(c -> passed == c.passed).count();
         }
     
         @Override
         public Collection<? extends Action> getProjectActions() {
             return this.projectActions;
         }
    +
    +
    +    public final String getViewerQualityGateDetails() {
    +        if (gateData == null || gateData.apiResponse == null || gateData.apiResponse.url == null) {
    +            return "";
    +        }
    +        final String escapedQualityGateViewerUrl = "/" + this.gateData.apiResponse.url;
    +        return this.openInViewerUrl(escapedQualityGateViewerUrl, "");
    +    }
    +
     }
    
  • src/main/java/hudson/plugins/tics/TicsPublisher.java+48 31 modified
    @@ -47,8 +47,6 @@
     import hudson.model.Run;
     import hudson.model.TaskListener;
     import hudson.plugins.tics.MeasureApiCall.MeasureApiCallException;
    -import hudson.plugins.tics.TicsQualityGate.QualityGateResult;
    -import hudson.plugins.tics.TqiPublisherResultBuilder.TqiPublisherResult;
     import hudson.security.ACL;
     import hudson.tasks.BuildStepDescriptor;
     import hudson.tasks.BuildStepMonitor;
    @@ -120,8 +118,8 @@ public boolean getFailIfQualityGateFails() {
     
         @Override
         public void perform(@Nonnull final Run<?, ?> run, @Nonnull final FilePath workspace, @Nonnull final Launcher launcher, @Nonnull final TaskListener listener) throws IOException, RuntimeException, InterruptedException {
    -        final Optional<StandardUsernamePasswordCredentials> credentials = getStandardUsernameCredentials(run.getParent(), credentialsId);
    -        final String ticsPath1 = Util.replaceMacro(Preconditions.checkNotNull(Strings.emptyToNull(ticsPath), "Path not specified"), run.getEnvironment(listener));
    +        final Optional<StandardUsernamePasswordCredentials> credentials = getStandardUsernameCredentials(run.getParent(), this.credentialsId);
    +        final String ticsPath1 = Util.replaceMacro(Preconditions.checkNotNull(Strings.emptyToNull(this.ticsPath), "Path not specified"), run.getEnvironment(listener));
     
             final String measureApiUrl;
             final String qualityGateUrl;
    @@ -135,43 +133,62 @@ public void perform(@Nonnull final Run<?, ?> run, @Nonnull final FilePath worksp
                 throw new IllegalArgumentException(LOGGING_PREFIX + "Invalid TICS Viewer URL", ex);
             }
     
    -        final TqiPublisherResultBuilder builder = new TqiPublisherResultBuilder(
    -                listener.getLogger(),
    -                credentials,
    -                measureApiUrl,
    -                ticsPath1);
    +        final MeasureApiCall measureApiCall = new MeasureApiCall(listener.getLogger(), measureApiUrl, credentials);
    +        final MetricData tqiData = getTqiMetricData(listener.getLogger(), ticsPath1, measureApiCall);
    +
    +        QualityGateData gateData;
    +        if (checkQualityGate) {
    +            final QualityGateApiCall qgApiCall = new QualityGateApiCall(qualityGateUrl, ticsPath1, credentials, listener);
    +            gateData = retrieveQualityGateData(qgApiCall, listener, tiobeWebBaseUrl);
    +
    +            if (!gateData.passed && this.failIfQualityGateFails) {
    +                run.setResult(Result.FAILURE);
    +            }
    +        } else {
    +            gateData = null;
    +        }
     
    -        TqiPublisherResult tqiLabelResult;
    -        QualityGateResult qualityGateResult = null;
    +        run.addAction(new TicsPublisherBuildAction(run, tqiData, gateData, tiobeWebBaseUrl));
    +        run.setResult(Result.SUCCESS); // note that: "has no effect when the result is already set and worse than the proposed result"
    +    }
     
    +    private MetricData getTqiMetricData(final PrintStream logger, final String ticsPath1, final MeasureApiCall apiCall) {
    +        final TqiPublisherResultBuilder builder = new TqiPublisherResultBuilder(
    +                logger,
    +                apiCall,
    +                ticsPath1
    +                );
             try {
    -            tqiLabelResult = builder.run();
    +            return builder.run();
             } catch (final Exception e) {
    -            listener.getLogger().println(LOGGING_PREFIX + e.getMessage());
    -            listener.getLogger().println(Throwables.getStackTraceAsString(e));
    -            tqiLabelResult = null;
    +            logger.println(LOGGING_PREFIX + Throwables.getStackTraceAsString(e));
    +            return MetricData.error(ticsPath1, "There was an error while retrieving metric data. See the build log for more information.");
             }
    +    }
     
    -        if (checkQualityGate) {
    -            final TicsQualityGate qualityGate = new TicsQualityGate(qualityGateUrl, tiobeWebBaseUrl, failIfQualityGateFails, ticsPath1, credentials, run, listener);
    -            try {
    -                qualityGateResult = qualityGate.createQualityGateResult();
    -                // Add action only when Quality Gate is enabled
    -            } catch (final Exception e) {
    -                qualityGateResult = null;
    -                if (failIfQualityGateFails) {
    -                    // Mark the run as failure when failIfQualityGatingFails is set, and any exception was thrown during the Quality Gate calculation
    -                    run.setResult(Result.FAILURE);
    -                }
    -                listener.getLogger().println(Throwables.getStackTraceAsString(e));
    +    private QualityGateData retrieveQualityGateData(
    +            final QualityGateApiCall apiCall,
    +            final TaskListener listener,
    +            final String tiobeWebBaseUrl
    +            ) {
    +        try {
    +            final QualityGateData gateData = apiCall.retrieveQualityGateData();
    +
    +            if (gateData.apiResponse != null) {
    +                final boolean passed = gateData.apiResponse.passed;
    +                final String encodedQualityGateViewerUrl = tiobeWebBaseUrl + "/" + gateData.apiResponse.url.replace("(", "%28").replace(")", "%29");
    +
    +                listener.getLogger().println(LOGGING_PREFIX + " Quality Gate " + (passed ? "passed": "failed")
    +                        + ". Please check the following url for more information: " + encodedQualityGateViewerUrl);
                 }
    -        }
     
    -        run.addAction(new TicsPublisherBuildAction(run, tqiLabelResult, qualityGateResult, tiobeWebBaseUrl));
    -        run.setResult(Result.SUCCESS); // always succeed
    +            return gateData;
    +        } catch (final Exception e) {
    +            listener.getLogger().println(LOGGING_PREFIX + Throwables.getStackTraceAsString(e));
    +            return QualityGateData.error("There was an error while retrieving the quality gate status. See the build log for more information.");
    +        }
         }
     
    -
         static Optional<StandardUsernamePasswordCredentials> getStandardUsernameCredentials(final Job<?, ?> job, final String credentialsId) {
             if (Strings.isNullOrEmpty(credentialsId)) {
                 return Optional.empty();
    
  • src/main/java/hudson/plugins/tics/TicsQualityGate.java+0 201 removed
    @@ -1,201 +0,0 @@
    -package hudson.plugins.tics;
    
    -
    
    -import java.net.ConnectException;
    
    -import java.util.Optional;
    
    -
    
    -import javax.annotation.Nonnull;
    
    -
    
    -import org.apache.http.client.methods.CloseableHttpResponse;
    
    -import org.apache.http.client.methods.HttpGet;
    
    -import org.apache.http.impl.client.CloseableHttpClient;
    
    -import org.apache.http.util.EntityUtils;
    
    -
    
    -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
    
    -import com.google.gson.JsonArray;
    
    -import com.google.gson.JsonElement;
    
    -import com.google.gson.JsonObject;
    
    -import com.google.gson.JsonParser;
    
    -import com.google.gson.JsonSyntaxException;
    
    -
    
    -import hudson.model.Result;
    
    -import hudson.model.Run;
    
    -import hudson.model.TaskListener;
    
    -
    
    -
    
    -public class TicsQualityGate extends AbstractApiCall {
    
    -
    
    -    public static class QualityGateResult {
    
    -        public String tableHtml;
    
    -        public String viewerGateUrl;
    
    -
    
    -        public QualityGateResult(final String html, final String viewerGateUrl) {
    
    -            this.tableHtml = html;
    
    -            this.viewerGateUrl = viewerGateUrl;
    
    -        }
    
    -
    
    -    }
    
    -
    
    -    public QualityGateResult createQualityGateResult() throws Exception {
    
    -        retrieveQualityGateStatus();
    
    -        final String tableHtml = createQualityGateHtml();
    
    -        return new QualityGateResult(tableHtml, viewerGateUrl);
    
    -    }
    
    -
    
    -    private static final String LOGGING_PREFIX = "[TICS Quality Gating] ";
    
    -    private static final String GREEN_FLAG = "/plugin/tics/greenFlag.png";
    
    -    private static final String RED_FLAG = "/plugin/tics/redFlag.png";
    
    -    public final String qualityGateUrl;
    
    -    public final boolean failIfQualityGateFails;
    
    -    public final Optional<StandardUsernamePasswordCredentials> credentials;
    
    -    public final Run<?, ?> run;
    
    -    public final TaskListener listener;
    
    -    public final String project;
    
    -    public final String branch;
    
    -    public final String tiobeWebBaseUrl;
    
    -    public JsonObject jsonObject;
    
    -    public String viewerGateUrl;
    
    -
    
    -    public TicsQualityGate(final String qualityGateUrl, final String tiobeWebBaseUrl, final boolean failIfQualityGateFails, final String ticsPath, final Optional<StandardUsernamePasswordCredentials> credentials, @Nonnull final Run<?, ?> run, final TaskListener listener) {
    
    -        super(LOGGING_PREFIX, listener.getLogger(), credentials);
    
    -        final String projectAndBranch = ticsPath.split("://")[1];
    
    -        this.project = projectAndBranch.split("/")[0];
    
    -        this.branch = projectAndBranch.split("/")[1];
    
    -        this.qualityGateUrl = qualityGateUrl;
    
    -        this.tiobeWebBaseUrl = tiobeWebBaseUrl;
    
    -        this.failIfQualityGateFails = failIfQualityGateFails;
    
    -        this.credentials = credentials;
    
    -        this.run = run;
    
    -        this.listener = listener;
    
    -    }
    
    -
    
    -    public void retrieveQualityGateStatus() throws Exception {
    
    -        final String qualityGateUrl1 = qualityGateUrl + "?project=" + project + "&branch=" + branch;
    
    -        final String body = getHttpRequest(qualityGateUrl1);
    
    -
    
    -        parseQualityGateJson(body);
    
    -    }
    
    -
    
    -    private String getHttpRequest(final String url) throws Exception {
    
    -        final String body;
    
    -        final HttpGet httpGet = new HttpGet(url);
    
    -        try (final CloseableHttpClient httpclient = createHttpClient();
    
    -             final CloseableHttpResponse response = httpclient.execute(httpGet);
    
    -        ) {
    
    -            body = EntityUtils.toString(response.getEntity());
    
    -            throwIfStatusNotOk(response, body);
    
    -        } catch (final ConnectException e) {
    
    -            throw new ConnectException(e.getMessage());
    
    -        } catch (final Exception e) {
    
    -            throw new Exception(e.toString() /* Includes exception name for more information*/);
    
    -        }
    
    -        return body;
    
    -    }
    
    -
    
    -    private void parseQualityGateJson(final String body) {
    
    -        try {
    
    -            final JsonElement jsonElement = new JsonParser().parse(body);
    
    -            this.jsonObject = jsonElement.getAsJsonObject();
    
    -        } catch (final JsonSyntaxException ex) {
    
    -            throw new RuntimeException(LOGGING_PREFIX + "Error parsing json: " + body, ex);
    
    -        }
    
    -    }
    
    -
    
    -    private String createQualityGateHtml() {
    
    -        this.viewerGateUrl = jsonObject.get("url").getAsString();
    
    -        final String qualityGateViewerUrl = tiobeWebBaseUrl + "/" + this.viewerGateUrl;
    
    -        final String escapedQualityGateViewerUrl = qualityGateViewerUrl.replace("(", "%28").replace(")", "%29");
    
    -        final boolean passed = jsonObject.get("passed").getAsBoolean();
    
    -        final String qualityGateStatus = passed ? "passed" : "failed";
    
    -        if (!passed && failIfQualityGateFails) {
    
    -            run.setResult(Result.FAILURE);
    
    -        }
    
    -        listener.getLogger().println(LOGGING_PREFIX + " Quality Gate " + qualityGateStatus
    
    -                + ". Please check the following url for more information: " + escapedQualityGateViewerUrl);
    
    -        final StringBuilder sb = new StringBuilder();
    
    -        sb.append("<p>");
    
    -        sb.append("<b>Project:</b> ");
    
    -        sb.append(project).append("/").append(branch);
    
    -        sb.append("</p>");
    
    -        sb.append("<p>");
    
    -        sb.append(jsonObject.get("message").getAsString());
    
    -        sb.append("</p>");
    
    -        final JsonArray gates = jsonObject.getAsJsonArray("gates");
    
    -        sb.append(qualityGateSummary(gates));
    
    -        return sb.toString();
    
    -    }
    
    -
    
    -    private String qualityGateSummary(final JsonArray gates) {
    
    -        final String greenFlagElement = "<img src='" + GREEN_FLAG + "' width='30' height='20'>";
    
    -        final String redFlagElement = "<img src='" + RED_FLAG + "' width='30' height='20'>";
    
    -
    
    -        final StringBuilder sb = new StringBuilder();
    
    -        for (final JsonElement gate : gates) {
    
    -            final String gateName = gate.getAsJsonObject().get("name").getAsString();
    
    -            final JsonArray gateConditions = gate.getAsJsonObject().getAsJsonArray("conditions");
    
    -            int failedConditionsNr = 0;
    
    -            int successfulConditionsNr = 0;
    
    -            for (final JsonElement condition : gateConditions) {
    
    -                if (condition.getAsJsonObject().get("passed").getAsBoolean()) {
    
    -                    successfulConditionsNr++;
    
    -                } else {
    
    -                    failedConditionsNr++;
    
    -                }
    
    -            }
    
    -
    
    -            final HtmlTag div = HtmlTag.from("div")
    
    -                    .attr("style", "margin-top: 15px");
    
    -            sb.append(div.open());
    
    -            sb.append("<div style='float: right; display: inline;'>")
    
    -                    .append(redFlagElement)
    
    -                    .append(" ")
    
    -                    .append(failedConditionsNr)
    
    -                    .append(" failed ")
    
    -                    .append("<img style='margin-left: 5px' src='" + GREEN_FLAG + "' width='30' height='20'>")
    
    -                    .append(" ")
    
    -                    .append(successfulConditionsNr)
    
    -                    .append(" passed")
    
    -                    .append("</div>");
    
    -            sb.append("<h4 style='margin-bottom: 6px'>").append(gateName).append("</h4>");
    
    -            sb.append(div.close());
    
    -            final HtmlTag table = HtmlTag.from("table")
    
    -                    .attr("id", "quality-gate")
    
    -                    .attr("style", "border-spacing: 0px")
    
    -                    .attr("style", "border-collapse: collapse")
    
    -                    .attr("style", "margin-bottom: 20px");
    
    -
    
    -            sb.append(table.open());
    
    -            sb.append("<thead>");
    
    -            sb.append("<tr>");
    
    -            sb.append("</tr>");
    
    -            sb.append("</thead>");
    
    -            sb.append("<colgroup><col><col style='width: 100%'><col></colgroup>");
    
    -            sb.append("<tbody>");
    
    -
    
    -            for (final JsonElement condition : gateConditions) {
    
    -                final boolean didConditionPass = condition.getAsJsonObject().get("passed").getAsBoolean();
    
    -                final String conditionFlag = didConditionPass ? greenFlagElement : redFlagElement;
    
    -                final String conditionMessage = condition.getAsJsonObject().get("message").getAsString();
    
    -                final HtmlTag tr = HtmlTag.from("tr")
    
    -                        .attr("style", "border-top: 1px solid #CCC");
    
    -                sb.append(tr.open());
    
    -
    
    -                HtmlTag td = HtmlTag.from("td")
    
    -                        .attr("style", "padding-top: 2px")
    
    -                        .attr("style", "background-color: #FFF")
    
    -                        .attr("style", "padding: 4px 5px ");
    
    -                sb.append(td.open());
    
    -                sb.append(conditionFlag);
    
    -                sb.append(td.close());
    
    -
    
    -                td = td.attr("style", "text-align: left");
    
    -                sb.append(td.open());
    
    -                sb.append(conditionMessage);
    
    -                sb.append(td.close());
    
    -            }
    
    -
    
    -            sb.append("</tbody>");
    
    -            sb.append("</table>");
    
    -        }
    
    -        return sb.toString();
    
    -    }
    
    -}
    
    
  • src/main/java/hudson/plugins/tics/TqiPublisherResultBuilder.java+91 272 modified
    @@ -1,189 +1,125 @@
     package hudson.plugins.tics;
     
    +import static java.util.stream.Collectors.joining;
    +import static java.util.stream.Collectors.toList;
    +
     import java.io.PrintStream;
    -import java.math.BigDecimal;
    -import java.math.RoundingMode;
    -import java.text.DecimalFormat;
    -import java.text.NumberFormat;
    +import java.util.ArrayList;
     import java.util.List;
    -import java.util.Locale;
     import java.util.Optional;
     
    -import org.checkerframework.checker.nullness.qual.Nullable;
    +import javax.annotation.Nullable;
    +
     import org.joda.time.Instant;
    -import org.joda.time.format.DateTimeFormat;
    +import org.jsoup.Jsoup;
    +import org.jsoup.safety.Whitelist;
     
    -import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
    -import com.google.common.base.Strings;
     import com.google.common.base.Supplier;
     import com.google.common.base.Suppliers;
     import com.google.common.collect.ImmutableList;
    -import com.google.common.collect.ImmutableMap;
    -import com.google.common.collect.Iterables;
    +import com.google.common.collect.Lists;
     
    -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     import hudson.plugins.tics.MeasureApiCall.MeasureApiCallException;
     import hudson.plugins.tics.MeasureApiSuccessResponse.Baseline;
    -import hudson.plugins.tics.MeasureApiSuccessResponse.Metric;
     import hudson.plugins.tics.MeasureApiSuccessResponse.MetricValue;
     import hudson.plugins.tics.MeasureApiSuccessResponse.Run;
     import hudson.plugins.tics.MeasureApiSuccessResponse.TqiVersion;
     
     public class TqiPublisherResultBuilder {
    -    public static class TqiPublisherResult {
    -        public String tableHtml;
    -        public String ticsPath;
    -        public String measurementDate = Instant.now().toString();
    -
    -        public TqiPublisherResult(final String html, final String ticsPath) {
    -            this.tableHtml = html;
    -            this.ticsPath = ticsPath;
    -        }
    -    }
     
    -    public static final String METRICS_3_11 = "tqi,tqiTestCoverage,tqiAbstrInt,tqiComplexity,tqiCompWarn,tqiCodingStd,tqiDupCode,tqiFanOut,tqiDeadCode,loc";
    -    public static final String METRICS_4_0 = "tqi,tqiTestCoverage,tqiAbstrInt,tqiComplexity,tqiCompWarn,tqiCodingStd,tqiDupCode,tqiFanOut,tqiSecurity,loc";
    +    public static final ImmutableList<String> METRICS_3_11 = ImmutableList.of("tqi","tqiTestCoverage","tqiAbstrInt","tqiComplexity","tqiCompWarn","tqiCodingStd","tqiDupCode","tqiFanOut","tqiDeadCode","loc");
    +    public static final ImmutableList<String> METRICS_4_0 = ImmutableList.of("tqi","tqiTestCoverage","tqiAbstrInt","tqiComplexity","tqiCompWarn","tqiCodingStd","tqiDupCode","tqiFanOut","tqiSecurity","loc");
         public static final String TQI_VERSION = "tqiVersion";
    -    private final PrintStream logger;
    -    private final DecimalFormat percentageFormatter;
    -    private final NumberFormat integerFormatter;
         private final String ticsPath;
         private final MeasureApiCall measureApiCall;
    +    private final PrintStream logger;
    +    private final Supplier<ImmutableList<String>> metrics;
     
    -    public TqiPublisherResultBuilder(final PrintStream logger,
    -            final Optional<StandardUsernamePasswordCredentials> credentials, final String measureApiUrl, final String ticsPath) {
    +    public TqiPublisherResultBuilder(
    +            final PrintStream logger,
    +            final MeasureApiCall apiCall,
    +            final String ticsPath
    +            ) {
             this.logger = logger;
             this.ticsPath = ticsPath;
    -        this.percentageFormatter = ((DecimalFormat)NumberFormat.getInstance(Locale.US));
    -        this.percentageFormatter.setMaximumFractionDigits(2);
    -        this.percentageFormatter.setMinimumFractionDigits(2);
    -        this.percentageFormatter.setRoundingMode(RoundingMode.FLOOR);
    -        this.integerFormatter = NumberFormat.getInstance(Locale.US);
    -        this.measureApiCall = new MeasureApiCall(logger, measureApiUrl, credentials);
    +        this.measureApiCall = apiCall;
    +        this.metrics = Suppliers.memoize(() -> {
    +            final boolean hasSecurity = this.doesTqiVersionIncludeSecurity();
    +            return hasSecurity ? METRICS_4_0 : METRICS_3_11;
    +        });
         }
     
    -    public TqiPublisherResult run() {
    -        String tableHtml;
    -        try {
    -            tableHtml = createTableHtml();
    -        } catch(final Exception ex) {
    -            ex.printStackTrace(logger);
    -            throw new RuntimeException("Could not publish the TICS results.", ex);
    -        }
    -        return new TqiPublisherResult(tableHtml, ticsPath);
    +    private String formatDate(final Instant date) {
    +        return date.toDateTime().toString("YYYY-MM-dd HH:mm:ss");
         }
     
    +    public @Nullable MetricData run() throws MeasureApiCallException {
    +        final List<Run> runDatesDesc = getRunDatesDescending();
    +        if (runDatesDesc.isEmpty()) {
    +            return MetricData.error(this.ticsPath, "Project has no runs yet");
    +        }
    +        final Run lastRun = runDatesDesc.get(0);
    +        final MetricData.Run currentRun = this.getRunDataOrThrow("Current", Optional.empty(), "TQI Scores at " + this.formatDate(lastRun.getStarted()));
    +
    +        final List<MetricData.Run> runsData = new ArrayList<>();
    +        runsData.add(currentRun);
    +        runDatesDesc.stream().skip(1).findFirst()
    +            .flatMap(run -> this.tryGetRunData("\u0394Previous", Optional.of(run.getStarted()), "Delta with previous run at " + this.formatDate(run.getStarted())))
    +            .ifPresent(runsData::add);
    +        this.baseline.get()
    +            .flatMap(bl -> this.tryGetRunData("\u0394" + bl.getName(), Optional.of(bl.getStarted()), "Delta with baseline '" + bl.getName() + "' at " + this.formatDate(bl.getStarted())))
    +            .ifPresent(runsData::add);
    +        return new MetricData(currentRun.metricNames, runsData, this.ticsPath, null);
    +    }
     
    -    @SuppressFBWarnings("UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD")
    -    private String createTableHtml() throws MeasureApiCallException {
    -        final boolean hasSecurity = doesTqiVersionIncludeSecurity();
    -        final String metrics = hasSecurity ? METRICS_4_0 : METRICS_3_11;
    -        final MeasureApiSuccessResponse<Double> mvsCurrent = measureApiCall.execute(MeasureApiCall.RESPONSE_DOUBLE_TYPETOKEN, ticsPath, metrics);
    -        final Optional<MeasureApiSuccessResponse<Double>> mvsPrevious = getPreviousRunDate().isPresent() ? tryQueryMetricsForDate(getPreviousRunDate().get(), metrics) : Optional.<MeasureApiSuccessResponse<Double>>empty();
    -        final Optional<MeasureApiSuccessResponse<Double>> mvsBaseline = baseline.get().isPresent() ? tryQueryMetricsForDate(baseline.get().get().getStarted(), metrics) : Optional.<MeasureApiSuccessResponse<Double>>empty();
    -
    -        final StringBuilder sb = new StringBuilder();
    -        final HtmlTag table = HtmlTag.from("table")
    -                .attr("style", "border-spacing: 0px")
    -                .attr("style", "border: 1px solid #CCC");
    -        sb.append(table.open());
    -        sb.append("<thead>");
    -        sb.append("<tr>");
    -        sb.append(HtmlTag.from("th").attr("style", "text-align: left").openClose("Metric"));
    -        HtmlTag th = HtmlTag.from("th")
    -                .attr("style", "text-align: right")
    -                .attr("style", "width: 80px");
    -        th = th.attr("style", "cursor: help");
    -        sb.append(th.attr("title", "Last TICS run was at\n" + getLastRunDate().orElse(Instant.now()).toDateTime().toString(DateTimeFormat.longDateTime())).openClose("Current"));
    -        sb.append("<th style=\"width: 26px\"><!-- Letter --></th>");
    -        if (mvsPrevious.isPresent()) {
    -            final String title = "Delta with previous TICS run at\n" + getPreviousRunDate().orElse(Instant.now()).toDateTime().toString(DateTimeFormat.longDateTime());
    -            sb.append(th.attr("title", title).openClose("&Delta;Previous"));
    -        }
    -        if (mvsBaseline.isPresent() && baseline.get().isPresent()) {
    -            final Baseline bl = baseline.get().get();
    -            final String title = "Delta with baseline '" + bl.getName() + "' at\n" + bl.getStarted().toDateTime().toString(DateTimeFormat.longDateTime());
    -            sb.append(th.attr("title", title).open());
    -            final HtmlTag div = HtmlTag.from("div")
    -                    .attr("style", "overflow: hidden")
    -                    .attr("style", "text-overflow: clip")
    -                    .attr("style", "white-space: nowrap")
    -                    .attr("style", "width: 80px")
    -                    ;
    -            sb.append(div.open());
    -            sb.append("&Delta;" + bl.getName());
    -            sb.append(div.close());
    +    private Optional<MetricData.Run> tryGetRunData(final String runName, final Optional<Instant> date, final String description) {
    +        try {
    +            return Optional.of(this.getRunDataOrThrow(runName, date, description));
    +        } catch (final MeasureApiCallException ex) {
    +            ex.printStackTrace(this.logger);
    +            return Optional.empty();
             }
    -        sb.append("</tr>");
    -        sb.append("</thead>");
    -        sb.append("<tbody>");
    -        final MetricValue<Double> EMPTY_METRICVALUE = new MetricValue<Double>();
    -        for (int i=0; i<mvsCurrent.data.size(); i++) {
    -            final Metric metric = mvsCurrent.metrics.get(i);
    -            final MetricValue<Double> mvNow = Iterables.get(mvsCurrent.data, i, EMPTY_METRICVALUE);
    -            if (mvNow == null) {
    -                continue;
    -            }
    -            final HtmlTag tr = HtmlTag.from("tr")
    -                    ;
    -            sb.append(tr.open());
    -
    -            HtmlTag td = HtmlTag.from("td")
    -                    .attr("style", "padding-top: 2px; padding-bottom: 2px;")
    -                    .attr("style", "background-color: " + (i % 2 == 0 ? "#EEE" : "#FFF"))
    -                    ;
    -            sb.append(td.open());
    -            sb.append(metric.fullName);
    -            sb.append(td.close());
    -
    -            // Output current value
    -            td = td.attr("style", "text-align: right");
    -            sb.append(td.open());
    -            if (mvNow.value == null) {
    -                sb.append("-");
    -            } else {
    -                if (isPercentageMetric(metric)) {
    -                    sb.append(percentageFormatter.format(mvNow.value));
    -                    sb.append("%");
    -                } else {
    -                    sb.append(integerFormatter.format(mvNow.value));
    -                }
    -            }
    -            sb.append(td.close());
    -
    -
    -            // Output letter
    -            sb.append(td.open());
    -            sb.append(getLetterToBadge(mvNow.letter));
    -            sb.append(td.close());
    -            if (mvsPrevious.isPresent()) {
    -                final @Nullable MetricValue<Double> mvpi = Iterables.get(mvsPrevious.get().data, i, EMPTY_METRICVALUE);
    -                if (mvpi != null) {
    -                    outputDeltaCell(sb, td, metric, mvNow.value, mvpi.value);
    -                }
    -            }
    -
    -            if (mvsBaseline.isPresent()) {
    -                final @Nullable MetricValue<Double> mvpi = Iterables.get(mvsBaseline.get().data, i, EMPTY_METRICVALUE);
    -                if (mvpi != null) {
    -                    outputDeltaCell(sb, td, metric, mvNow.value, mvpi.value);
    -                }
    -            }
    -
    +    }
     
    -            sb.append(tr.close());
    -        }
    -        sb.append("</tbody>");
    -        sb.append("</table>");
    -        return sb.toString();
    +    private MetricData.Run getRunDataOrThrow(final String runName, final Optional<Instant> deltaDate, final String description) throws MeasureApiCallException {
    +        final String metricExpr = deltaDate.isPresent()
    +                ? this.metrics.get().stream().map(m -> "Delta(" + m + "," + (deltaDate.get().getMillis()/1000L) +")").collect(joining(","))
    +                : this.metrics.get().stream().collect(joining(","));
    +        final MeasureApiSuccessResponse<Number> resp = this.measureApiCall.execute(MeasureApiCall.RESPONSE_NUMBER_TYPETOKEN, this.ticsPath, metricExpr);
    +        final List<String> metricNames = resp.metrics.stream()
    +                .map(m -> m.fullName)
    +                .collect(toList());
    +        final List<MetricData.MetricValue> metricValues = new ArrayList<>();
    +        for (int i = 0; i < metricNames.size(); i++) {
    +            final MetricValue<Number> mv = resp.data.get(i);
    +
    +            // We want to use formattedValue because it contains correct number of decimals, % symbol, thousand separators, etc.
    +            // However, formattedValue can contain HTML, such as for delta metrics and for errors.
    +            // We use JSoup for stripping this HTML. Note that the formattedValue will be rendered by Jelly (which does HTML-escaping),
    +            // so note that JSoup.clean() is not used for preventing XSS vulnerabilities.
    +            final String formattedValueStripped = Jsoup.clean(mv.formattedValue, Whitelist.none());
    +
    +            metricValues.add(new MetricData.MetricValue(
    +                    mv.status,
    +                    formattedValueStripped,
    +                    mv.letter
    +                    ));
    +        }
    +        return new MetricData.Run(
    +                runName,
    +                description,
    +                metricNames,
    +                metricValues,
    +                deltaDate.map(Instant::toString).orElse(null)
    +                );
         }
     
         private boolean doesTqiVersionIncludeSecurity() {
             final MeasureApiSuccessResponse<TqiVersion> resp;
             try {
    -            resp = measureApiCall.execute(MeasureApiCall.RESPONSE_TQIVERSION_TYPETOKEN, ticsPath, TQI_VERSION);
    +            resp = this.measureApiCall.execute(MeasureApiCall.RESPONSE_TQIVERSION_TYPETOKEN, this.ticsPath, TQI_VERSION);
             } catch (final MeasureApiCallException e) {
    -            e.printStackTrace(logger);
    +            e.printStackTrace(this.logger);
                 return false;
             }
             if (resp.data.isEmpty() || resp.data.get(0).value == null) {
    @@ -193,138 +129,21 @@ private boolean doesTqiVersionIncludeSecurity() {
             return tqiVersion.compareTo(new TqiVersion(4, 0)) >= 0;
         }
     
    -
    -    private String getLetterToBadge(final String letter) {
    -        if (Strings.isNullOrEmpty(letter)) {
    -            return "";
    -        }
    -        final ImmutableMap<String, String> tqiBgColors = ImmutableMap.<String, String>builder()
    -                .put("A", "#006400")
    -                .put("B", "#64AE00")
    -                .put("C", "#FFFF00")
    -                .put("D", "#FF950E")
    -                .put("E", "#FF420E")
    -                .put("F", "#BE0000")
    -                .build();
    -        final ImmutableMap<String, String> tqiFgColors = ImmutableMap.<String, String>builder()
    -                .put("A", "white")
    -                .put("B", "white")
    -                .put("C", "black")
    -                .put("D", "black")
    -                .put("E", "white")
    -                .put("F", "white")
    -                .build();
    -
    -        final String bgColor = tqiBgColors.get(letter);
    -        final String fgColor = tqiFgColors.get(letter);
    -        if (bgColor == null || fgColor == null) {
    -            return "";
    -        }
    -
    -        return HtmlTag.from("span")
    -                .attr("style", "color: " + fgColor)
    -                .attr("style", "background-color: " + bgColor)
    -                .attr("style", "padding: 0 7px 0 7px")
    -                .attr("style", "border-radius: 5px")
    -                .attr("style", "font-weight: bold")
    -                .attr("style", "text-align: center")
    -                .attr("style", "box-shadow: 0px 0px 3px #888888")
    -                .openClose(letter);
    -    }
    -
    -
    -    private void outputDeltaCell(final StringBuilder sb, final HtmlTag td0, final Metric metric, final Double now, final Double prev) {
    -        if (now == null || prev == null) {
    -            sb.append("-");
    -            return;
    -        }
    -        final BigDecimal delta;
    -        final String deltaAsText;
    -        if (isPercentageMetric(metric)) {
    -            final double diff = now.doubleValue() - prev.doubleValue();
    -            delta = new BigDecimal(diff).setScale(2, RoundingMode.UP);
    -            deltaAsText = delta.toPlainString();
    -        } else {
    -            final int diff = now.intValue() - prev.intValue();
    -            delta = new BigDecimal(diff).setScale(0);
    -            deltaAsText = integerFormatter.format(diff);
    -        }
    -
    -        HtmlTag td = td0.attr("style", "text-align: right");
    -        if (metric.getExpression().startsWith("tqi") && delta.signum() != 0) {
    -            td = td.attr("style", "color: " + (delta.signum() > 0 ? "green" : "red"));
    -        }
    -
    -        sb.append(td.open());
    -        if (delta.signum() > 0) {
    -            sb.append("+");
    -        }
    -        if (delta.signum() == 0) {
    -            sb.append("<span style=\"color: #BBB\">0.00</span>");
    -        } else {
    -            sb.append(deltaAsText);
    -        }
    -        sb.append(td.close());
    -    }
    -
    -    private boolean isPercentageMetric(final Metric metric) {
    -        final boolean isPercentageMetric = metric.getExpression().startsWith("tqi");
    -        return isPercentageMetric;
    -    }
    -
    -    private Optional<MeasureApiSuccessResponse<Double>> tryQueryMetricsForDate(final Instant date, final String metrics) {
    -        try {
    -            return Optional.of(measureApiCall.execute(MeasureApiCall.RESPONSE_DOUBLE_TYPETOKEN, ticsPath, metrics, Optional.of(date)));
    -        } catch (final MeasureApiCallException e) {
    -            e.printStackTrace(logger);
    -            return Optional.empty();
    -        }
    -    }
    -
    -
    -    private final Supplier<Optional<List<Run>>> runDates = Suppliers.memoize(new Supplier<Optional<List<Run>>>() {
    -        @Override
    -        public Optional<List<Run>> get() {
    -            final MeasureApiSuccessResponse<List<Run>> resp;
    -            try {
    -                resp = measureApiCall.execute(MeasureApiCall.RESPONSE_RUNS_TYPETOKEN, ticsPath, "runs", Optional.empty());
    -            } catch (final MeasureApiCallException e) {
    -                e.printStackTrace(logger);
    -                return Optional.empty();
    -            }
    -            if (resp.data.isEmpty()) {
    -                return Optional.empty();
    -            }
    -            final MetricValue<List<Run>> mv = resp.data.get(0);
    -            return Optional.of(mv.value);
    -        }
    -    });
    -
    -    private final Optional<Instant> getLastRunDate() {
    -        final List<Run> runs = runDates.get().orElse(ImmutableList.<Run>of());
    -        if (runs.isEmpty()) {
    -            return Optional.empty();
    -        }
    -
    -        final Run run = runs.get(runs.size()-1);
    -        return Optional.of(run.getStarted());
    -    }
    -
    -    private final Optional<Instant> getPreviousRunDate() {
    -        final List<Run> runs = runDates.get().orElse(ImmutableList.<Run>of());
    -        if (runs.size() <= 1) {
    -            return Optional.empty();
    +    private List<Run> getRunDatesDescending() throws MeasureApiCallException {
    +        final MeasureApiSuccessResponse<List<Run>> resp = measureApiCall.execute(MeasureApiCall.RESPONSE_RUNS_TYPETOKEN, TqiPublisherResultBuilder.this.ticsPath, "runs");
    +        if (resp.data.isEmpty()) {
    +            return new ArrayList<Run>();
             }
    -        final Run run = runs.get(runs.size()-2);
    -        return Optional.of(run.getStarted());
    -    }
    +        final MetricValue<List<Run>> mv = resp.data.get(0);
    +        return Lists.reverse(Optional.ofNullable(mv.value).orElseGet(ArrayList::new));
    +    };
     
         private final Supplier<Optional<Baseline>> baseline = Suppliers.memoize(new Supplier<Optional<Baseline>>() {
             @Override
             public Optional<Baseline> get() {
                 final MeasureApiSuccessResponse<List<Baseline>> resp;
                 try {
    -                resp = measureApiCall.execute(MeasureApiCall.RESPONSE_BASELINES_TYPETOKEN, ticsPath, "baselines", Optional.empty());
    +                resp = measureApiCall.execute(MeasureApiCall.RESPONSE_BASELINES_TYPETOKEN, ticsPath, "baselines");
                 } catch (final MeasureApiCallException e) {
                     e.printStackTrace(logger);
                     return Optional.empty();
    
  • src/main/resources/hudson/plugins/tics/TicsPublisher/table.jelly+125 17 modified
    @@ -1,6 +1,35 @@
    -<?jelly escape-by-default='false'?>
    +<?jelly escape-by-default='true'?>
     <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout"
    -         xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:local="local">
    +         xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt" xmlns:local="local"
    +         >
    +
    +<style type="text/css">
    +.table-metrics {
    +  border-spacing: 0px; 
    +  border: 1px solid #CCC;
    +  width: 100%;
    +}
    +.table-metrics th,
    +.table-metrics td {
    +  padding-top: 2px;
    +  padding-bottom: 2px; 
    +}
    +.table-metrics th > div,
    +.table-metrics td > div {
    +  white-space: nowrap;
    +  overflow: hidden;
    +  text-overflow: clip;
    +}
    +.table-striped tbody > tr:nth-child(odd) > td,
    +.table-striped tbody > tr:nth-child(odd) > th {
    +  background-color: #eee;
    +}
    +table.table-gate td {
    +  border-top: 1px solid #CCC;
    +  padding: 4px 5px;
    +}
    +</style>         
    +         
     
       <table style="width: 100%">
         <tr>
    @@ -11,37 +40,116 @@
         </tr>
         <tr>
           <td>
    -        <a href="${it.openInViewerUrl}" target="_blank" title="Open in Viewer">${it.ticsPath == null ? '-' : it.ticsPath}</a>
    +        <a href="${it.openInViewerUrl}" target="_blank" title="Open in Viewer">${it.tqiData.ticsPath == null ? '-' : it.tqiData.ticsPath}</a>
           </td>
         </tr>
       </table>
     
       <j:choose>
    -    <j:when test="${it.tableHtml == null}">
    +    <j:when test="${it.tqiData == null}">
           No results available. TICS Quality Indicators will be collected during the next build.
         </j:when>
    +    <j:when test="${it.tqiData.errorMessage != null}">
    +      ${it.tqiData.errorMessage}
    +    </j:when>
         <j:otherwise>
    -      ${it.tableHtml}
    -      <div style="margin-bottom: 20px">Measured at ${it.measurementDate}</div>
    +      <table class="table-metrics table-striped">
    +        <thead>
    +          <tr>
    +            <th style="text-align: left">Metric</th>
    +            <j:forEach var="run" items="${it.tqiData.runs}">
    +              <th style="text-align: right; width: 80px; cursor: help;"
    +                title="${run.description}"
    +              >
    +                <div style="max-width: 120px">
    +                  ${run.name}
    +                </div>
    +              </th>
    +              <j:if test="${run == it.tqiData.runs[0]}">
    +                <th style="width: 30px"><!--letter--></th>
    +              </j:if>
    +            </j:forEach>
    +          </tr>
    +        </thead>
    +        <tbody>
    +          <j:forEach var="metric" items="${it.tqiData.metrics}">
    +            <tr>
    +              <td><div>${metric}</div></td>
    +              <j:forEach var="run" items="${it.tqiData.runs}">
    +                <j:set var="mv" value="${run.metricValues[it.tqiData.metrics.indexOf(metric)]}" />
    +                <td style="text-align: right">
    +                  ${mv.status.startsWith('PRESENT') ? mv.formattedValue : '-'}
    +                </td>
    +                <j:if test="${run == it.tqiData.runs[0]}">
    +                  <td style="text-align: right">
    +                    <j:if test="${mv.letter != null}"> 
    +                      <span style="color: ${it.getLetterForegroundColor(mv.letter)}; background-color: ${it.getLetterBackgroundColor(mv.letter)}; padding: 0 7px 0 7px; border-radius: 5px; font-weight: bold; text-align: center; box-shadow: 0px 0px 3px #888888;">
    +                        ${mv.letter}
    +                      </span>
    +                    </j:if>
    +                  </td>
    +                </j:if>
    +              </j:forEach>
    +            </tr>
    +          </j:forEach>
    +        </tbody>
    +      </table>
    +      <div>Measured at ${it.formatDate(it.tqiData.measurementDate)}</div>
         </j:otherwise>
       </j:choose>
     
       <j:choose>
    -    <j:when test="${it.qualityGateTableHtml == null}">
    +    <table style="width: 100%; margin-top: 20px;">
    +      <tr>
    +        <td rowspan="2" style="width: 76px; vertical-align: middle;">
    +          <img src="${rootURL}/plugin/tics/tics.png" style="margin-top: 2px"></img>
    +        </td>
    +        <td><b>Quality Gate Summary</b></td>
    +      </tr>
    +    </table>
     
    +    <j:when test="${it.gateData == null}">
    +      No results available. TICS Quality Indicators will be collected during the next build.
    +    </j:when>
    +    <j:when test="${it.gateData.errorMessage != null}">
    +      ${it.gateData.errorMessage}
         </j:when>
         <j:otherwise>
    -      <table style="width: 100%">
    -        <tr>
    -          <td rowspan="2" style="width: 76px; vertical-align: middle;">
    -            <img src="${rootURL}/plugin/tics/tics.png" style="margin-top: 2px"></img>
    -          </td>
    -          <td><b>Quality Gate Summary</b></td>
    -        </tr>
    -      </table>
    -      ${it.qualityGateTableHtml}
    +      <p>
    +        <b>Project: </b>
    +        ${it.gateData.project}/${it.gateData.branch}
    +      </p>
    +      <p>${it.gateData.apiResponse.message}</p>
    +      
    +      <j:forEach var="gate" items="${it.gateData.apiResponse.gates}">
    +        <div style="margin-top: 15px">
    +          <div style="float: right; display: inline">
    +            <img src="${rootURL}/plugin/tics/redFlag.png" width="30" height="20"/>
    +            ${it.countConditions(gate, false)} failed
    +            <img src="${rootURL}/plugin/tics/greenFlag.png" width="30" height="20"/>
    +            ${it.countConditions(gate, true)} passed
    +          </div>
    +          <h4 style="margin-bottom: 6px">${gate.name}</h4>
    +          <table style="border-spacing: 0px; border-collapse: collapse; margin-bottom: 20px"
    +            class="table-gate"
    +          >
    +            <colgroup><col/><col style="width: 100%"/></colgroup>
    +            <tbody>
    +              <j:forEach var="condition" items="${gate.conditions}">
    +                <tr>
    +                  <td>
    +                    <img src="${rootURL}/plugin/tics/${condition.passed ? 'green' : 'red'}Flag.png" width="30" height="20"/>
    +                  </td>
    +                  <td>${condition.message}</td>
    +                </tr>
    +              </j:forEach>
    +            </tbody>
    +          </table> 
    +        </div>
    +      </j:forEach>
    +  
           <a target="_blank" href="${it.viewerQualityGateDetails}"> See results in TICS Viewer</a>
    -      <div style="margin-bottom: 20px">Measured at ${it.measurementDate}</div>
    +      <div style="margin-bottom: 20px">Measured at ${it.formatDate(it.gateData.measurementDate)}</div>
         </j:otherwise>
       </j:choose>
     
    
  • svn-log.xslt+0 62 removed
    @@ -1,62 +0,0 @@
    -<?xml version="1.0"?>
    -<xsl:stylesheet 
    -    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    -    version="1.0">
    -  <xsl:output 
    -    method="html" 
    -    encoding="UTF-8"
    -    indent="no"/>
    -
    -  <xsl:template match="/">
    -    <html>
    -      <head>
    -        <title>Release Notes</title>
    -        <link rel="SHORTCUT ICON" href="/images/tiobe.ico" type="image/x-icon" />
    -        <link rel="stylesheet" href="/tics/tics.css" type="text/css" />
    -      </head>
    -      <body>
    -        <h2>Release Notes</h2>
    -        <xsl:apply-templates select="/log"/>
    -      </body>
    -    </html>
    -  </xsl:template>
    -
    -  <xsl:template match="log">
    -    <table >
    -      <tr valign="top">
    -        <td><b>Release</b></td>
    -        <td><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text></td>
    -        <td><b>Date</b></td>
    -        <td><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text></td>
    -        <td><b>Note</b></td>
    -      </tr>
    -    <xsl:apply-templates select="logentry"/>
    -    </table>
    -  </xsl:template>
    -
    -  <xsl:template match="logentry">
    -    <xsl:if test="string(number(substring(msg, 1, 1)))!='NaN'">
    -      <xsl:if test="substring-before(substring-after(msg, '] '), ': ')!='Rework'">
    -        <tr valign="top">
    -          <td>
    -            <xsl:value-of select="@revision"/>
    -          </td>
    -          <td><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text></td>
    -          <td nowrap="nowrap">
    -            <xsl:value-of select="substring-before(date, 'T')"/>
    -          </td>
    -          <td><xsl:text disable-output-escaping="yes">&amp;nbsp;</xsl:text></td>
    -          <td>
    -            <xsl:if test="contains(msg, '&#10;&#10;')">
    -              <xsl:value-of select="substring-before(msg, '&#10;&#10;')"/>
    -            </xsl:if>
    -            <xsl:if test="not(contains(msg, '&#10;&#10;'))">
    -              <xsl:value-of select="msg"/>
    -            </xsl:if>
    -          </td>
    -        </tr>
    -      </xsl:if>
    -    </xsl:if>
    -  </xsl:template>
    -
    -</xsl:stylesheet>
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

1