Tuesday, January 21, 2014

Gradle project template for Eclipse with GAE nature

I have been using Maven for years, but after some brief readings about Gradle, I am quite impressed by its flexibiliy. Under gradle, the build customization is straightforward using script language, provided that user has some basic Groovy knowledge.

I am going to create a project template using gradle. The following has been tested against Eclipse 4.3 (Kelper), Gradle 1.10 and Google App Engine SDK 1.8.9. It is also assumed that the Google GAE plugin has been installed in Eclipse. With this project template one should be able to:
  • Build a war file by running
       gradle war
    
  • Create an eclipse project with Google App Engine (GAE) nature by running
       gradle cleanEclipse eclipse
    
Note: there is an eclipse plugin for gradle provided by Spring (https://github.com/spring-projects/eclipse-integration-gradle/wiki). However, at the time of writting, it supports Java project nature only. Thus I still prefer to use gradle directly to generate all eclipse + GAE stuff for my project.

1. Apply the plugins


In build.gradle, firstly we need to add the required plugins:
apply plugin: 'war'
apply plugin: 'eclipse-wtp'
apply plugin: 'gae'
The eclipse-wtp is added as the Eclipse GAE plugin will create an GAE project as web project, so I generate an eclipse web project too.

2. Copy artifacts from local App Engine SDK folder


Originally I want to pull the GAE artifacts from google repositories, just like other 3rd party maven dependencies. So I configure the following maven repository:
repositories {
   mavenCentral()
   maven {url 'https://oss.sonatype.org/content/repositories/google-releases/'}
}

However, eclipse will complain about the different jar size with the App Engine SDK for datanucleus-appengine-2.1.2.jar. Therefore, if there is already App Engine SDK installed locally, I would recommend to copy the artifacts from the SDK folder; otherwise, just specify the maven dependencies as usual.

In this template, I assume the SDK exists locally and an environment variable APPENGINE_HOME must be set to point to the SDK location. The dependencies are listed as follow,
ext {
   springVersion = '3.2.6.RELEASE'
   appEngineHome = "$System.env.APPENGINE_HOME"
   appEngineSdkVersion = '1.8.9'
}

dependencies {
   providedCompile "javax.servlet:servlet-api:2.5"
   
   testCompile "junit:junit:4.+"
   
   compile "org.springframework:spring-webmvc:$springVersion"

   // AppEngine dependencies, copy from APPENGINE_HOME lib folders
   compile files(
      "$appEngineHome/lib/user/appengine-api-1.0-sdk-${appEngineSdkVersion}.jar", 
      "$appEngineHome/lib/opt/user/appengine-api-labs/v1/appengine-api-labs.jar",
      "$appEngineHome/lib/opt/user/appengine-endpoints/v1/appengine-endpoints.jar",
      "$appEngineHome/lib/opt/user/appengine-endpoints/v1/appengine-endpoints-deps.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/asm-4.0.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/datanucleus-core-3.1.3.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/datanucleus-api-jpa-3.1.3.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/datanucleus-api-jdo-3.1.3.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/datanucleus-appengine-2.1.2.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/geronimo-jpa_2.0_spec-1.0.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/jdo-api-3.0.1.jar",
      "$appEngineHome/lib/opt/user/datanucleus/v2/jta-1.1.jar",
      "$appEngineHome/lib/opt/user/jsr107/v1/jsr107cache-1.1.jar",
      "$appEngineHome/lib/opt/user/jsr107/v1/appengine-jsr107cache-${appEngineSdkVersion}.jar")

   // runtime dependencies
   runtime "javax.servlet:jstl:1.1.2"
}
As I would like to setup Spring later, the Spring MVC dependency is added.

I would also like to populate the WEB-INF/lib folder by copying the dependencies so I do not need to check-in the jars to source control system, so I add the following:
// task to clean WEB-INF/lib folder
task cleanWarLibDir(type: Delete) {
   delete fileTree(dir: "war/WEB-INF/lib")
}

// task to populate WEB-INF/lib folder from the compile dependencies
task populateWarLib(type: Copy) {
   into('war/WEB-INF/lib')
   from configurations.compile
}

// populate the WEB-INF/lib
tasks.eclipse.dependsOn('checkenv','populateWarLib')
tasks.populateWarLib.dependsOn('cleanWarLibDir')

The checkenv task is just used to check the APPENGINE_HOME environment variable. If the variable is not set, it will throw an exception.

3. Setup the eclipse project configurations


The Eclipse GAE plugin is picky about the project settings. Although it is kind of web project, it does not expect the existence of web container library. Thus the jars in WEB-INF/lib will not be added to the build path automatically in Eclipse, and we need to add the needed jars as external libraries manually in eclipse IDE.

Here is the eclipse part of the gradle configuration:
eclipse {
   // add build commands and specify the project natures
   project {
      natures.clear()
      natures 'org.eclipse.jdt.core.javanature',
         'com.google.appengine.eclipse.core.gaeNature',
         'org.eclipse.wst.common.project.facet.core.nature'
      buildCommands.clear()
      buildCommand 'org.eclipse.wst.common.project.facet.core.builder'
      buildCommand 'org.eclipse.jdt.core.javabuilder'
      buildCommand 'com.google.gdt.eclipse.core.webAppProjectValidator'
      buildCommand 'com.google.appengine.eclipse.core.gaeProjectChangeNotifier'
      buildCommand 'com.google.appengine.eclipse.core.projectValidator'
      buildCommand 'com.google.appengine.eclipse.core.enhancerbuilder'
   }

   classpath {
      defaultOutputDir = file("${project.projectDir}/war/WEB-INF/classes")

      // correct classpaths as needed by GAE eclipse plugin
      containers.clear()
      containers.add 'com.google.appengine.eclipse.core.GAE_CONTAINER'
      containers.add 'org.eclipse.jdt.launching.JRE_CONTAINER'
      file {
         whenMerged { classpath ->
            classpath.entries.removeAll {
               entry -> entry.kind == 'lib' || 
               (entry.kind == 'con' && entry.path == 'org.eclipse.jst.j2ee.internal.web.container')
            } 
            classpath.entries.findAll {
               entry -> entry.hasProperty('exported') 
            }*.exported = false
         }
      }
   }

   // GAE application needs Java 1.7
   wtp {
      facet {
         facet name: 'java', version: '1.7'
      }
   }
}
We also need to add two google specific properties files in the .setting folder:
// fix the GAE eclipse prop file
tasks.eclipse.doLast {

   // write settings file 'com.google.appengine.eclipse.core.prefs'
   ant.propertyfile(file: ".settings/com.google.appengine.eclipse.core.prefs") {
      ant.entry(key: "eclipse.preferences.version", value: "1")
      ant.entry(key: "gaeDatanucleusVersion", value: "v2")
      ant.entry(key: "gaeHrdEnabled", value: "true")
   }

   // write settings file 'com.google.gdt.eclipse.core.prefs'
   ant.propertyfile(file: ".settings/com.google.gdt.eclipse.core.prefs") {
      ant.entry(key: "eclipse.preferences.version", value: "1")
      ant.entry(key: "warSrcDir", value: "war")
      ant.entry(key: "warSrcDirIsOutput", value: "true")
   }
}
Without the above files the Eclipse GAE plugin will complain about the version of jars in WEB-INF/lib.

Okay, that's it. One should be able to import this project after running "gradle cleanEclipse eclipse". If you have already created an application in google appengine console, you should be able to deploy it in Eclipse using the GAE plugin.

Note that I also include the gradle gae plugin, so even there is no eclipse one should be able to do the deployment using gradle directly.

The project template is available on GitHub.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.