Jun 19

Building and publishing a play framework 2.x module to maven central

There are a handful of articles on how to build a module for version 2.x of the Play Framework. While those articles are an excellent starting point, they were lacking information on configuration, exposing a plugin, and publishing them to maven central. This post will explain how to build, maintain, publish, and use your Play Framework 2.x module in other projects.

Create your empty module project

The root project directory will be called play-module-twitter. Inside that directory there will be two sub-directories named module and sample. First, create your root project directory:

mkdir play-module-twitter
cd play-module-twitter

To create a Java-based module, do the following in your root project directory code>play-module-twitter :

play new module

What is the application name? [module]
> mfz-play-module-twitter

Which template do you want to use for this new application? 
  1             - Create a simple Scala application
  2             - Create a simple Java application
> 2

Your root project directory will now contain a sub-directory called module and once you open your module project in your IDE it will be named mfz-play-module-twitter.

We will now do the same for the sample project that will demo the use of your module:

play new sample

What is the application name? [sample]
> mfz-play-module-twitter-sample

Which template do you want to use for this new application? 
  1             - Create a simple Scala application
  2             - Create a simple Java application
> 2

Now will be the time to add any root project files such as your README.

touch README.md

Your project directory structure should now look like this:

play-module-twitter/
  module/
  sample/
  README.md

Your module project/code can include almost anything a normal Play Framework project can such as controllers, views, models, and general Scala/Java code. Its primarily important to delete all configuration files so they aren't included in the final .jar -- since they will conflict with the projects that will use your module. I also like to delete any controllers, assets, tests, or views that are generally included with a standard Play project:

cd module
rm -Rf conf/* public/* test/* app/controllers app/views

Modify project for IDE and eventual publishing

Since I use Eclipse as my IDE for Play projects, I now like to create the Eclipse project with the command play eclipse.

Since modules should be published either locally or remotely so they can be used in your other Play projects, it's important to modify the project/Build.scala file to control how your artifact is published. This is how your current Build.scala file should look like:

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {
  val appName         = "mfz-play-plugin-twitter"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    // Add your own project settings here      
  )
}

By default Play will publish your artifact with an organization that matches your project name to your local/remote repository. If we were to play publish-local without modifying Build.scala, the artifact would be published to {play21 dir}/repository/local/play-plugin-twitter/play-plugin-twitter_2.10/1.0-SNAPSHOT/jars/play-plugin-twitter_2.10.jar. My organization can easily be set like so:

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {
  val appName         = "mfz-play-plugin-twitter"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    organization := "com.mfizz",
    organizationName := "Mfizz Inc",
    organizationHomepage := Some(new URL("http://mfizz.com"))
  )
}

The artifact will now be published to {play21 dir}/repository/local/com.mfizz/mfz-play-plugin-twitter_2.10/1.0-SNAPSHOT/jars/mfz-play-plugin-twitter_2.10.jar. For Java developers familiar with Maven, you may wonder what the trailing "_2.10" is on your artifact -- it is added by Play's underlying use of SBT (simple build tool) to publish your library for use by a specific Scala version.

Write your module

You are now ready to start writing your module code. I put all my module code in a specific package to prevent conflicts with other modules. In the module/app directory, I create the following package com.mfizz.play.twitter.

Our project will include a few utility classes and one Play plugin named TwitterPlugin. This is the special class that will be loaded at startup by Play and configured via the application.conf config file. All the details of each file in the project won't be covered here since you can view the source at GitHub. We will just some of the more important aspects of writing a module that includes a plugin.

Add a plugin that's configured at startup

Plugins for play extend the class play.Plugin and can override the method onStart() which is run when Play starts/restarts. This is where the code will be added to configure TwitterPlugin from application.conf. Your constructor will be passed one parameter which is of type play.Application that you will want to save in your class.

public TwitterPlugin(Application application) {
  this.application = application;
}

The application.conf config file will support a new namespace called twitter that will contain the settings the plugin requires. The config file will eventually look like this:

# Twitter Plugin
twitter.access-token = "replace with access token from dev.twitter.com"
twitter.access-secret =  "replace with access secret from dev.twitter.com"
twitter.consumer-key = "replace with consumer key from dev.twitter.com"
twitter.consumer-secret = "replace with consumer secret from dev.twitter.com"
twitter.refresh-interval = 60m

Using the application object saved in your constructor, the onStart method will use it to get a reference to the play.Configuration object.

@Override
public void onStart() {
  play.Configuration configuration = application.configuration();
  this.accessToken = configuration.getString("twitter.access-token");
}

Start a periodic job to refresh tweets

The onStart method in the plugin will also start a job that runs every configured refresh-interval to pull tweets from Twitter. If the api call fails, the existing tweets pulled from Twitter will remain in memory. The job consists of a class that implements Runnable and instructing Akka to run it periodically.

// create job that will be run to update
RefreshJob job = new RefreshJob(this);

// create job to run every X milliseconds
String dispatcherName = "TwitterUpdateJob";
MessageDispatcher dispatcher = Akka.system().dispatchers().lookup(dispatcherName);
Akka.system().scheduler().schedule(
  FiniteDuration.create(0, TimeUnit.MILLISECONDS),
  FiniteDuration.create(this.refreshInterval, TimeUnit.MILLISECONDS),
  job,
  dispatcher
);

Add external dependencies

The module depends on two external libraries. They are twitter4j-core and twitter-text. For Java developers familiar with Maven, normally this means the following would be added to the pom file.

<dependencies>
    <dependency>
        <groupId>org.twitter4j</groupId>
        <artifactId>twitter4j-core</artifactId>
        <version>[3.0.3,)</version>
    </dependency>
    <dependency>
        <groupId>com.twitter</groupId>
        <artifactId>twitter-text</artifactId>
        <version>[1.6.1,)</version>
    </dependency>
</dependencies>

In Play (or really SBT since that's what Play uses for builds), you'll add the dependencies to your Build.scala file. Expanding on the example of Build.scala above, the appDependencies section now looks like:

val appDependencies = Seq(
    "org.twitter4j" % "twitter4j-core" % "[3.0.3,)",
    "com.twitter" % "twitter-text" % "[1.6.1,)",
    javaCore,
    javaJdbc,
    javaEbean
)

Note that the % symbol between the organization and artifact name is used for Java (non-Scala) Maven-based dependencies. If you use double percentage symbols such as %% then Play will append "_2.10" onto the artifact name. A double percentage symbol %% will instruct Play's build system SBT to only use dependencies compiled for a specific Scala version such as 2.10.

Since we added new dependencies and we'd like our IDE to include them as references, you'll also need to re-run the command play eclipse and refresh your project to pick up the dependency changes.

Write a sample application to demo module

To speed up the code/run development cycle, its preferable to have the sample application depend on the module project so that code changes in either project will trigger re-compilation of both projects. Your sample Build.scala project will look like:

import sbt._
import Keys._
import play.Project._

object ApplicationBuild extends Build {
  val appName         = "mfz-play-module-twitter-sample"
  val appVersion      = "1.0-SNAPSHOT"

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean
  )
  
  // sub-project this depends on
  val module = RootProject(file("../module"))

  val main = play.Project(appName, appVersion, appDependencies).settings(
    organization := "com.mfizz",
    organizationName := "Mfizz Inc",
    organizationHomepage := Some(new URL("http://mfizz.com"))
  ).dependsOn(module)
}

The two important changes are val module = RootProject(file("../module")) that uses a relative path and adding .dependsOn(module) to the play.Project. When you run your sample project with the command play run, notice that both projects are loaded:

~play-module-twitter/sample $ play run
[info] Loading project definition from /play-module-twitter/sample/project
[info] Loading project definition from /play-module-twitter/module/project
[info] Set current project to mfz-play-module-twitter-sample (in build file:/play-module-twitter/sample/)

--- (Running the application from SBT, auto-reloading is enabled) ---

[info] play - Listening for HTTP on /0:0:0:0:0:0:0:0:9000

To enable the plugin, add it to a new conf/play.plugins file:

1000:com.mfizz.play.twitter.TwitterPlugin

Note that you'll need to modify conf/application.conf with your Twitter api values for the sample app to run.

Publish module to maven central (via sonatype)

Publishing your Play2 framework module to the central maven repository will make its use by other projects a piece of cake. If you've never published any library to Maven central previously, you'll need to follow the instructions from Sonatype about setting up an account.

Assuming you've followed those instructions and have a username, password, central sync approval, and a PGP key properly setup, enabling your Play project module isn't too complicated. A helpful start for this article was locate here.

Verify your PGP key is setup:

gpg --list-keys

In your module's project directory, add addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8") to the project/plugins.sbt file. It will now look like this:

// Comment to get more information during initialization
logLevel := Level.Warn

// The Typesafe repository
resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

// Use the Play sbt plugin for Play projects
addSbtPlugin("play" % "sbt-plugin" % "2.1.1")

// for PGP signing
addSbtPlugin("com.typesafe.sbt" % "sbt-pgp" % "0.8")

Sonatype requires a number of properties to be set before it will approve a release. These are added to the underlying "pom.xml" maven file which Play's build system SBT creates behind the scenes. You can add them by editing the Build.scala file. The new Build.scala file:

import sbt._
import Keys._
import play.Project._
import com.typesafe.sbt.SbtPgp._

object ApplicationBuild extends Build {

  val appName         = "mfz-play-module-twitter"
  val appVersion      = "1.0"

  val appDependencies = Seq(
    "org.twitter4j" % "twitter4j-core" % "[3.0.3,)",
    "com.twitter" % "twitter-text" % "[1.6.1,)",
    javaCore,
    javaJdbc,
    javaEbean
  )

  val main = play.Project(appName, appVersion, appDependencies).settings(
    organization := "com.mfizz",
    organizationName := "Mfizz Inc",
    organizationHomepage := Some(new URL("http://mfizz.com")),
    
    // required for publishing artifact to maven central via sonatype
    publishMavenStyle := true,
    publishTo <<= version { v: String =>
	  val nexus = "https://oss.sonatype.org/"
	  if (v.trim.endsWith("SNAPSHOT"))
	    Some("snapshots" at nexus + "content/repositories/snapshots")
	  else
	    Some("releases" at nexus + "service/local/staging/deploy/maven2")
	},
	
	// in order to pass sonatype's requirements the following properties are required as well
	startYear := Some(2013),
	description := "Play framework 2.x module to fetch, cache, and display tweets from Twitter",
    licenses := Seq("Apache 2" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt")),
    homepage := Some(url("http://mfizz.com/oss/play-module-twitter")),
    scmInfo := Some(ScmInfo(url("https://github.com/mfizz-inc/play-module-twitter"), "https://github.com/mfizz-inc/play-module-twitter.git")),
    pomExtra := (
      <developers>
        <developer>
    	  <name>Mfizz Inc (twitter: @mfizz_inc)</name>
          <email>oss@mfizz.com</email>
        </developer>
        <developer>
    	  <name>Joe Lauer (twitter: @jjlauer)</name>
        </developer>
      </developers>
    )
  )
}

When you're ready to publish your module to Sonatype, run the command "play publish-signed" which will sign the artifacts using the PGP plugin and publish them to Sonatype. You'll then need to follow the instructions on Sonatype to close and release your artifacts.

Final thoughts

Your play2 framework module can include any number of plugins or utility classes. It can even include views, controllers, etc. that you may want to reuse in your own projects. You also may simply want to publish your module locally as well with the play publish-local command.

View the source for play-module-twitter

View the project for play-module-twitter

Updates? Need assistance?

Follow @mfizz_inc on Twitter for future updates and latest news.

If you have specific issues, questions, or problems, please contact us with your inquiry or consulting request.

Tags

Archives