May 03 2016

On the XWiki project it's been years that we've been checking automatically for backward compatibilities in our APIs as part of our build (It's even documented here).

Recently, we've moved to Java 8 for XWiki 8.1 and we've discovered that the tool we were using, CLIRR, doesn't work anymore with Java 8. So I searched, without much hope, for an alternative, and I was surprised (and delighted) to find that there are 2 valid solutions nowadays that support Java 8:

I've tried both and they both had issues. However, I've discovered that the maintainers or these 2 solutions were really cool guys and they very quickly fixed any issue I've raised. Big thanks to Lukas Krejci and Martin Mois, you're awesome guys! emoticon_smile

In the end the XWiki project has had to make a difficult choice and we chose Revapi (Some reasons mentioned here but honestly they're both valid choices).

So here's how we use it now:

  • In our top level POM:
    ...
         <plugins>
    ...
           <!-- Used for checking backward compatibility (binary and source) -->
           <plugin>
             <groupId>org.revapi</groupId>
             <artifactId>revapi-maven-plugin</artifactId>
             <!-- Lock down plugin version for build reproducibility -->
             <version>0.4.5</version>
             <dependencies>
               <dependency>
                 <groupId>org.revapi</groupId>
                 <artifactId>revapi-java</artifactId>
                 <version>0.9.0</version>
               </dependency>
             </dependencies>
             <executions>
               <execution>
                 <id>revapi-check</id>
                 <goals>
                   <goal>check</goal>
                 </goals>
               </execution>
             </executions>
             <configuration>
               <oldVersion>${xwiki.compatibility.previous.version}</oldVersion>
               <skip>${xwiki.revapi.skip}</skip>
               <analysisConfiguration>
                {
                  "revapi": {
                    "java": {
                      "filter": {
                        "packages": {
                          "regex": true,
                          "include": ["org\\.xwiki\\..*"],
                          "exclude": ["org\\.xwiki\\..*\\.internal(\\..*)?", "org\\.xwiki\\..*\\.test(\\..*)?"]
                        }
                      }
                    }
                  }
                }
               </analysisConfiguration>
             </configuration>
           </plugin>
    ...
  • Then in specific module POMs we override the configuration to add excludes (for backward and source incompatibilities that we know about and have voluntarily decided to do). For example:
    ...
         <plugin>
           <groupId>org.revapi</groupId>
           <artifactId>revapi-maven-plugin</artifactId>
           <configuration>
             <analysisConfiguration><![CDATA[
                {
                  "revapi": {
                    "java": {
                      "filter": {
                        "packages": {
                          "regex": true,
                          "include": ["org\\.xwiki\\..*"],
                          "exclude": ["org\\.xwiki\\..*\\.internal(\\..*)?", "org\\.xwiki\\..*\\.test(\\..*)?"]
                        }
                      }
                    },
                    "ignore": [
                      {
                        "code": "java.method.returnTypeTypeParametersChanged",
                        "old": "method java.util.List<? extends org.xwiki.extension.ExtensionDependency> org.xwiki.extension.AbstractExtension::getDependencies()",
                        "new": "method java.util.List<org.xwiki.extension.ExtensionDependency> org.xwiki.extension.AbstractExtension::getDependencies()",
                        "justification": "? return type makes signature more complex for nothing"
                      },
                      {
                        "code": "java.method.returnTypeTypeParametersChanged",
                        "old": "method java.util.Collection<? extends org.xwiki.extension.ExtensionDependency> org.xwiki.extension.Extension::getDependencies()",
                        "new": "method java.util.Collection<org.xwiki.extension.ExtensionDependency> org.xwiki.extension.Extension::getDependencies()",
                        "justification": "? return type makes signature more complex for nothing"
                      },
                      {
                        "code": "java.method.returnTypeTypeParametersChanged",
                        "old": "method java.util.Collection<? extends org.xwiki.extension.ExtensionDependency> org.xwiki.extension.wrap.WrappingExtension<E extends org.xwiki.extension.Extension>::getDependencies()",
                        "new": "method java.util.Collection<org.xwiki.extension.ExtensionDependency> org.xwiki.extension.wrap.WrappingExtension<E extends org.xwiki.extension.Extension>::getDependencies()",
                        "justification": "? return type makes signature more complex for nothing"
                      }         
                    ]
                  }
                }
              ]]>
    </analysisConfiguration>
           </configuration>
         </plugin>
    ...
  • Now the interesting part is in generating reports. We've chosen an original way of doing this: We dynamically generate the Revapi report on our release notes wiki pages using Groovy. The big advantage is that there's no work to be done for the Release Manager:
    • Here's an example from the XWiki 8.1M1 Release Notes

      releasenotes.png

    • The Groovy script below does the following:
      • Use the GitHub REST API to get the content of the pom.xml containing the Revapi excludes.
      • Parse the XML using Groovy
      • Display a report out of it
    • And here's the Groovy script packaged as a Wiki Macro for the curious ones:
      {{groovy}}
      import groovy.json.*

      def getIgnores(def repo, def path)
      {
       def url = "https://api.github.com/repos/xwiki/${repo}/contents/${path}".toURL().text
       def result = new JsonSlurper().parseText(url)
       def xml = new String(result.content.decodeBase64())
        result = new XmlSlurper().parseText(xml)
       def revapi = result.build.plugins.plugin.'**'.find { node ->
          node.artifactId.text() == 'revapi-maven-plugin'
       }
       if (revapi) {
          result = new JsonSlurper().parseText(revapi.configuration.analysisConfiguration.text())
         return result.revapi.ignore
       } else {
         return ''
       }
      }

      def displayIgnores(def ignores)
      {
        result = new JsonSlurper().parseText(ignores)
        result.each() {
          it.each() {
            println "* {{{${it.justification}}}}"
            println "** Violation type: {{code}}${it.code}{{/code}}"
            println "** Old: {{code}}${it.old}{{/code}}"
           if (it.new) {
              println "** New: {{code}}${it.new}{{/code}}"
           }
         }
       }
      }

      def getViolations(def version)
      {
       def xobject = doc.getObject('ReleaseNotes.BackwardCompatibility')
       if (!xobject) {
          xobject = doc.newObject('ReleaseNotes.BackwardCompatibility')
         def commonsTag
         def renderingTag
         def platformTag
         if (version == 'master') {
            commonsTag = renderingTag = platformTag = 'master'
         } else {
            commonsTag = "xwiki-commons-${version}"
            renderingTag = "xwiki-rendering-${version}"
            platformTag = "xwiki-platform-${version}"
         }
         def jsonCommons = getIgnores('xwiki-commons', "xwiki-commons-core/pom.xml?ref=${commonsTag}")
         def jsonRendering = getIgnores('xwiki-rendering', "pom.xml?ref=${renderingTag}")
         def jsonPlatform = getIgnores('xwiki-platform', "xwiki-platform-core/pom.xml?ref=${platformTag}")
          xobject.set('violations', JsonOutput.prettyPrint(JsonOutput.toJson([jsonCommons, jsonRendering, jsonPlatform])))
          doc.save('Added backward-compatiblity violations data', true)
       }
       return xobject.getProperty('violations').value
      }

      displayIgnores(getViolations(xcontext.macro.params.version))
      {{/groovy}}

In conclusion we're very happy with the move, especially since we now have Java 8 support but also because Revapi provides more checks than what CLIRR was doing.

Tags:
Created by Vincent Massol on 2016/05/03 10:53
Tags:
Created by Vincent Massol on 2016/05/03 10:53
This wiki is licensed under a Creative Commons 2.0 license