Google App Engine Endpoints (REST services), with Java, Maven & Intellij (Part 1)

Sunday, May 19, 2013

App Engine endpoints allow you to create a RESTful service on Google’s infrastructure, which can be used as the backend for a mobile phone app, html site or some other client. Of course there are other similar and more standard solutions such as JAX-RS but I’m drawn to Google’s offering because I trust them to scale my service and, most importantly for me they have a built in authentication mechanism.

In this first post I’ll use Intellij and Maven on the iMac. In all honesty I think you would find the development process of the Google Eclipse plugin more polished - but I want to stick to standard development tooling i.e. Maven. The bottom line is I don’t want a plugin performing voodoo which I don’t understand, incase it breaks one day and leaves me clueless.

The easiest way to get started is to use the Google guestbook archetype. There is also a skeletal archetype (minus the guestbook), but that hasn’t been updated since 1.7.5. The guestbook project can be created via the Mac’s Terminal:

  1. mvn archetype:generate \
  2. -DgroupId=co.uk.planetjones \
  3. -DartifactId=test-blood-pressure-monitor \
  4. -Dversion=1.0-SNAPSHOT \
  5. -DpackageName=co.uk.planetjones \
  6. -DarchetypeGroupId=com.google.appengine.archetypes \
  7. -DarchetypeArtifactId=guestbook-archetype \
  8. -DarchetypeVersion=1.7.7 \
  9. -DinteractiveMode=false

(anything which doesn't begin with -Darchetype should be changed to fit your own project)

Now load Intellij and select import project. Find the folder your maven archetype created, which is test-blood-pressure-monitor in this example. Select OK and keep clicking the dialogs until the JDK selection appears.

Select Maven Project Select Maven Projects to Import

At the JDK selection, you should select a Java 1.7 SDK, as App Engine is moving to support only this Java version.

Your Intellij workbench should load. If you click "Maven Projects" you should see that 1.7.7 of the appengine-maven-plugin is available. We want to upgrade everything to version 1.8.0 though, so open the pom.xml and change to:

  1. <appengine.target.version>1.8.0</appengine.target.version>

The archetype doesn't create the project with endpoint support, so you need to add in this dependency:

  1. <dependency>
  2. <groupId>com.google.appengine</groupId>
  3. <artifactId>appengine-endpoints</artifactId>
  4. <version>${appengine.target.version}</version>
  5. </dependency>

Finally change pom.xml to add the endpoint discovery stuff to the appengine-maven-plugin and modify the war plugin so your endpoints will be available when deploying (each time the WAR plugin executes the endpoints_get_discovery_doc goal will also be executed):

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-war-plugin</artifactId>
  4. <version>2.3</version>
  5. <configuration>
  6. <webXml>${project.build.directory}/generated-sources/appengine-endpoints/WEB-INF/web.xml</webXml>
  7. <webResources>
  8. <resource>
  9. <directory>${basedir}/src/main/webapp/WEB-INF/</directory>
  10. <filtering>true</filtering>
  11. <targetPath>WEB-INF</targetPath>
  12. <excludes>
  13. <exclude>web.xml</exclude>
  14. </excludes>
  15. </resource>
  16. <resource>
  17. <directory>${project.build.directory}/generated-sources/appengine-endpoints</directory>
  18. <includes>
  19. <include>WEB-INF/*.discovery</include>
  20. <include>WEB-INF/*.api</include>
  21. </includes>
  22. <filtering>true</filtering>
  23. </resource>
  24. </webResources>
  25. </configuration>
  26. </plugin>
  27. <plugin>
  28. <groupId>com.google.appengine</groupId>
  29. <artifactId>appengine-maven-plugin</artifactId>
  30. <version>${appengine.target.version}</version>
  31. <configuration>
  32. <enableJarClasses>false</enableJarClasses>
  33. <oauth2>false</oauth2>
  34. </configuration>
  35. <executions>
  36. <execution>
  37. <goals>
  38. <goal>endpoints_get_discovery_doc</goal>
  39. </goals>
  40. </execution>
  41. </executions>
  42. </plugin>

You usually need to update the Maven dependencies via: right-clicking your project > Maven > Remimport. All the App Engine dependencies should change to 1.8.0. Usually I find the App Engine Maven plugin fails to download. If this is the case you can add it as a dependency in your pom.xml to force it to download (then delete the dependency once it's in your local repository).

Eventually you should see a Maven toolbar which looks like:

Maven Projects Toolbar

At this point I usually delete all .java, .jsp and web.xml entries pertinent to the guestbook - this gives a nice empty project to play with.

Before we can do endpoint generation we need to define our Endpoint class. For now I'll just work on the API and hardcode their implementations. In a subsequent post I'll introduce datastore persistence. Let's create a simple entity class:

  1. public class BloodPressureReading {
  2.  
  3. private Integer id;
  4. private Date created;
  5. private Short systolic;
  6. private Short diastolic;
  7.  
  8. public BloodPressureReading() {
  9. }
  10.  
  11. // just for the example
  12. public BloodPressureReading (Integer id, Short systolic, Short diastolic) {
  13. this.id = id;
  14. this.created = new Date();
  15. this.systolic = systolic;
  16. this.diastolic = diastolic;
  17. }
  18.  
  19. @Override
  20. public String toString() {
  21. return "BloodPressureReading{" +
  22. "id=" + id +
  23. ", created=" + created +
  24. ", systolic=" + systolic +
  25. ", diastolic=" + diastolic +
  26. '}';
  27. }

And let's create the Endpoint class for this:

  1. @Api(name="bp",
  2. version = "v1",
  3. description = "Test API for Blood Pressure Readings")
  4. public class BloodPressureReadingEndpoint {
  5.  
  6. private static List testData = new ArrayList<>();
  7. static {
  8. testData.add(new BloodPressureReading(1, (short)100, (short)200));
  9. testData.add(new BloodPressureReading(2, (short)80, (short)130));
  10. testData.add(new BloodPressureReading(3, (short)60, (short)100));
  11. }
  12. @ApiMethod(
  13. name = "bpreading.list",
  14. path = "bpreading",
  15. httpMethod = ApiMethod.HttpMethod.GET
  16. )
  17. public List list(@Nullable @Named("limit") Integer limit) {
  18. System.out.println("Limit was: " + limit);
  19. return testData;
  20. }
  21. @ApiMethod(
  22. name = "bpreading.get",
  23. path = "bpreading/{id}",
  24. httpMethod = ApiMethod.HttpMethod.GET
  25. )
  26. public BloodPressureReading get(@Named("id") Integer id) {
  27. System.out.println("ID is " + id);
  28. for(BloodPressureReading reading : testData) {
  29. if(reading.getId().equals(id)) {
  30. return reading;
  31. }
  32. }
  33. return null;
  34. }
  35. @ApiMethod(
  36. name = "bpreading.add",
  37. path = "bpreading",
  38. httpMethod = ApiMethod.HttpMethod.POST
  39. )
  40. public void add(BloodPressureReading reading) {
  41. System.out.println("Adding: " + reading);
  42. }
  43. @ApiMethod(
  44. name = "bpreading.delete",
  45. path = "bpreading/{id}",
  46. httpMethod = ApiMethod.HttpMethod.DELETE
  47. )
  48. public void delete(@Named("id") Long id) {
  49. System.out.println("Deleting: " + id);
  50. }
  51. }

To test this locally execute:

  1. appengine:endpoints_get_client_lib
  2. appengine:devserver

Intellij's Maven support makes the execution of these goals simple:

Maven run app engine maven goals

After running the devserver goal, if you navigate to:

  1. http://localhost:8080/_ah/api/discovery/v1/apis

via your browser you should see the service description:

  1. {
  2. "kind" : "discovery#directoryList",
  3. "discoveryVersion" : "v1",
  4. "items" : [ {
  5. "kind" : "discovery#directoryItem",
  6. "id" : "bp:v1",
  7. "name" : "bp",
  8. "version" : "v1",
  9. "description" : "Test API for Blood Pressure Readings",
  10. "discoveryRestUrl" : "https://webapis-discovery.appspot.com/_ah/api/discovery/v1/apis/bp/v1/rest",
  11. "discoveryLink" : "./apis/bp/v1/rest",
  12. "icons" : {
  13. "x16" : "http://www.google.com/images/icons/product/search-16.gif",
  14. "x32" : "http://www.google.com/images/icons/product/search-32.gif"
  15. },
  16. "preferred" : true
  17. } ]
  18. }

From the Terminal we can use curl to test the full service:

  1. curl "http://localhost:8080/_ah/api/bp/v1/bpreading"
  2.  
  3. console displays: [INFO] Limit was: null
  4.  
  5. returns:
  6.  
  7. "items" : [ {
  8. "id" : 1,
  9. "created" : "2013-05-19T10:40:47.919+02:00",
  10. "systolic" : 100,
  11. "diastolic" : 200
  12. }, {
  13. "id" : 2,
  14. "created" : "2013-05-19T10:40:47.919+02:00",
  15. "systolic" : 80,
  16. "diastolic" : 130
  17. }, {
  18. "id" : 3,
  19. "created" : "2013-05-19T10:40:47.919+02:00",
  20. "systolic" : 60,
  21. "diastolic" : 100
  22. } ]
  23. }
  24.  
  25. ----
  26.  
  27. curl "http://localhost:8080/_ah/api/bp/v1/bpreading?limit=2"
  28.  
  29. console displays: [INFO] Limit was: 2
  30.  
  31. ----
  32.  
  33. curl "http://localhost:8080/_ah/api/bp/v1/bpreading/1"
  34.  
  35. returns:
  36.  
  37. {
  38. "id" : 1,
  39. "created" : "2013-05-18T18:45:41.061+02:00",
  40. "systolic" : 100,
  41. "diastolic" : 200
  42. }
  43.  
  44. ----
  45.  
  46. curl -i -X POST -H Accept:application/json -H Content-Type:application/json
  47. -d '{"systolic" : 80, "diastolic" : 78}' 'http://localhost:8080/_ah/api/bp/v1/bpreading'
  48.  
  49. console displays:
  50. [INFO] Adding: BloodPressureReading{id=null, created=null, systolic=80, diastolic=78}
  51.  
  52. ----
  53.  
  54. curl -i -X DELETE -H Accept:application/json -H Content-Type:application/json
  55. 'http://localhost:8080/_ah/api/bp/v1/bpreading/2'
  56.  
  57. console displays: [INFO] Deleting: 2
  58.  
  59. ----

If your responses are as expected then it's a success! If you ever get problems then it's a good idea to look into the:

  1. target/generated-sources/appengine-endpoints

folder. This is where the Maven plugin puts its auto-generated files, so you can see the .api file which defines your endpoints.

After each change to the Endpoint class you must rerun appengine:endpoints_get_client_lib. Both the update and devserver goals will run the WAR plugin, so running endpoints_get:discovery_doc separately is not required.

Deploying to Google App Engine

This should be as simple as running appengine:update. However, I had an authentication problem which gave the error message:

  1. Unable to update app: Error posting to URL:
  2. https://appengine.google.com/api/appversion/create?app_id=planetjones-experiments&version=1&
  3. 404 Not Found
  4. This application does not exist (app_id=u'planetjones-experiments').

By default Google uses OAuth 2 authentication when deploying. I just disabled this by adding the:

  1. <oauth2>false</oauth2>

element to the configuration of the appengine-maven-plugin (see code line 48, earlier in this post). Doing this gives a prompt where you can enter your username and password for uploading to App Engine, when running the update goal.

In appengine-web.xml the application element must match the id of the application you created on appengine.google.com e.g. I changed mine to:

  1. planetjones-experiments

When all this has changed you should just execute appengine:update and hopefully you get the magical:

  1. BUILD SUCCESS

Testing on App Engine

You should be able to repeat all of the curl tests by substituting the localhost URL to your actual URL e.g.

  1. https://planetjones-experiments.appspot.com/_ah/api/bp/v1/bpreading/1

Note the use of HTTPS.

In my case this gives:

  1. {
  2. "id": 1,
  3. "created": "2013-05-19T08:50:59.609Z",
  4. "systolic": 100,
  5. "diastolic": 200,
  6. "kind": "bp#bpreadingItem",
  7. "etag": "\"YF3eBGlNw_xRlLJpCVeJTVgJcSY/AX5SxpQiWTPtPBki3i0dp2DqjZA\""
  8. }

As Maven said:

SUCCESS

I have added this project to Github, so feel free to look around. Next I am going to secure this service, so only authenticated clients can access. And I will, of course, make the service interact with Google's datastore. But I feel it's beneficial in breaking this down into separate iterations, to aid understanding.

Feel free to tweet, mail or comment with any questions.