Wednesday 18 June 2014

Scala By Stealth, Part 1: SBTifying your Mavenized Build

I was faced with updating and extending some old Java code of mine recently, and it seemed like much more of a chore than it used to. The code in question does a lot of collection manipulation, and I was looking at the Java code (which was, if I say so myself, not too bad - clean, thoroughly-tested and using nice libraries like Google Guava where at all possible) thinking "ugh - that would be a couple of lines in Scala and way more readable at the same time".

At this point I realised it would be a perfect candidate for a step-by-step guide for converting a simple Maveni[sz]ed Java library project (e.g. resulting in a JAR file artifact) to an SBT-based, Scala library.

Shortly after that I realised this could be a terrific way for a traditional "Java shop" where everything up until now has been delivered as JARs (and/or WARs) into a private Nexus to get its feet wet with Scala without having to go with a risky "big-bang" approach. An iterative migration, if you will. So let's get started!

A tiny bit of background first though - I'm not going to bother anonymising the library I'll be migrating, because I will almost certainly forget to do so somewhere in the example snippets I'll be including. So I'll say it here: the library is called brickhunter, and it's the "engine" behind the web-scraping LEGO search engine you can use at The site itself is a Java/Spring MVC/JQuery webapp that I launched in late 2012, and was the last significant bit of Java I ever wrote. It includes brickhunter.jar as a standard Maven dependency, pulling it from my private Maven repo hosted by CloudBees.

Step 0 (A Precondition): A Cared-For Maven Java Project

You need to be doing this migration for a library that has redeeming qualities, and not one that suffers from neglect, lack of test coverage, or a non-standard building process. Generally, using Maven will have made the latter difficult, but if, somehow, weird stuff is still going on, fix that. And make sure your tests are in order - comprehensive, relevant and not disabled!

Step 1: An SBTified Java Project

  • Create a new directory alongside the "legacy" project directory with a suitable name. For me, the obvious one was brickhunter-scala.
  • Now recursively copy everything under src from legacy to new. Hopefully that gets everything of importance; if not, see Step 0 and decide what should be done.
  • While a number of people have written helpers to automate the creation of a build.sbt from a pom.xml, unless you have a truly enormous number of dependencies, you're probably better-off just writing it yourself. For one thing, it's the obvious entry point to the enormous world of SBT, and there's plenty to learn;
  • In a typical Maven shop you may have quite a stack of parent POMs bringing in various dependencies - I found the quickest way to get all of them into SBT style was by invoking mvn dependency:tree which for my project, gave me:
    [INFO] +- org.jsoup:jsoup:jar:1.6.1:compile
    [INFO] +- commons-lang:commons-lang:jar:2.6:compile
    [INFO] +-
    [INFO] |  \-
    [INFO] +- log4j:log4j:jar:1.2.16:compile
    [INFO] +- org.slf4j:slf4j-api:jar:1.6.4:compile
    [INFO] +- org.slf4j:slf4j-log4j12:jar:1.6.4:compile
    [INFO] +- com.themillhousegroup:argon:jar:1.1-SNAPSHOT:compile
    [INFO] +- org.testng:testng:jar:6.3.1:test
    [INFO] |  +- junit:junit:jar:3.8.1:test
    [INFO] |  +- org.beanshell:bsh:jar:2.0b4:test
    [INFO] |  +- com.beust:jcommander:jar:1.12:test
    [INFO] |  \- org.yaml:snakeyaml:jar:1.6:test
    [INFO] +- org.mockito:mockito-all:jar:1.9.0:test
    [INFO] \- org.hamcrest:hamcrest-all:jar:1.1:test
  • Anything transitive (i.e. indented once or more) can be omitted as SBT will work that out for us just as Maven did.
  • The eagle-eyed might notice an in-house dependency (argon) which clearly isn't going to be found in the usual public repos - it will need its own resolver entry in build.sbt.
  • Here's how mine looked at this point:
  • name := "brickhunter-scala"
    organization := "com.themillhousegroup"
    version := "0.1"
    scalaVersion := "2.10.3"
    credentials += Credentials(Path.userHome / ".ivy2" / ".credentials")
    resolvers += "tmg-private-repo" at ""
    libraryDependencies ++= Seq(
      "org.jsoup"             % "jsoup"           % "1.6.1",
      "commons-lang"          % "commons-lang"    % "2.6",
      ""      % "guava"           % "11.0.1",
      "log4j"                 % "log4j"           % "1.2.16",
      "org.testng"            % "testng"          % "6.3.1"         % "test",
      "org.mockito"           % "mockito-all"     % "1.9.0"         % "test",
      "com.themillhousegroup" % "argon"           % "1.1-SNAPSHOT"  % "test"
  • At this point, firing up SBT and giving it a compile command should be successful. If so, pat yourself on the back, and commit all pertinent files in source control. This is a good milestone!

Step 2: A Tested SBTified Java Project

  • Compiling is all very well but you can't really be sure your SBT-ification has been a success until all the tests are passing, just like they did in Maven. They did all pass in Maven, didn't they?
  • Here's where I hit my first snag, as my Java tests were written using the TestNG framework, which SBT has no idea how to invoke. And thus, the brickhunter-scala project gets its first plugin, the sbt-testng-interface.
  • But now when running sbt test, instead of "0 Tests Found", I get a big stack trace - the plugin is expecting to find a src/test/resources/testng.yaml and I don't have one, because Maven "just knows" how to run a load of TestNG-annotated tests it finds in src/test/java, and I've never needed to define what's in the default test suite.
  • The fix is to create the simplest possible testng.yaml that will pick up all the tests:
    name: BrickhunterSuite
    threadCount: 4
      - name: All
        - com.themillhousegroup.brickhunter
  • And now we should have the same number of tests running as under Maven, and all passing. Commit all the changes!

Next time: Publishing the new artifact to your private repository.

No comments:

Post a Comment

Comments welcome - spam is not. Spam will be detected, deleted and the source IP blocked.