GWT UiBinder with JAX-RS Jersey

Today, I’ll primarily demonstrate how to use GWT UiBinder with JAX-RS Jersey running on top of Google App Engine Java. I’ll also discuss about Objectify and Simple libraries. Objectify is a beautiful simple easy to use library to persist Java objects to and from Google App Engine’s DataStore without using DataStore JDO or JPA interface. Simple, as the name suggest, is a simple XML to Java serialization/deserialization framework.

Following REST style a Players resource will be manipulated through uris listed bellow.  Only methods marked as “implemented” has been created.

Mapping URIs to Http Methods:

URI: /players
GET                Read all players   [implemented in this tutorial]
POST              Create multiple players X
DELETE        Delete multiple Players X
PUT                Update multiple players X

URI: /players/{name}
GET               Read detail of a player by name id    [implemented in this tutorial]
POST             Create a player by name id    [implemented in this tutorial]
DELETE        Delete a player by name id    [implemented in this tutorial]
PUT               Update a player information by name id X

I’ll take a bottom up approach to describe this tutorial. First we will discuss how REST styled methods were created using Jersey. If you need help with Jersey setup with Google App Engine please read my previous tutorial. Objectify will be discussed to show how Java Objects can be persisted to Google’s DataStore. Then Simple XML framework code will be examined to show how to serialize Java Object to XML. I’ve used XML as my payload between GWT Client and Jersey backend. Finally, I’ll show how GWT UiBinder was used as a simple HTML form.

Sandbox Environment:

Setting up:

Configure your Eclipse IDE with App Engine 1.3.0 sdk and GWT 2.0 with Google Eclipse Plugin. Set up Jersey as described here. Drop objectify.jar to /WEB-INF/lib folder to enable Objectify. Drop simple-xml-2.2.jar, stax-1.2.0.jar, and stax-api-1.0.1.jar to configure Simple XML. Your /WEB-INF/lib folder should look something like the following image.

WEB-inf/lib contents:


Steps:

Creating Resources:

This tutorial is only composed of one type of resource, Players. Players resource contains a persist-able class Player.

Player.java

Player class is a classical Java BEAN annotated with Java Persistence API and Simple XML annotations. Objectify uses Java Persistence annotations to identify a object’s persistable nature  (i.e. @Entity, @Id). To make a Object Objectifyable one need not to have getters and setters though. Getters and setters is needed by Simple XML framework for xml serialization. @Root, @Element are annotations to identify class and fields for xml serialization.

As per Objectify “Best Practices” document, put and delete DataStore methods embedded with Player Object to be persisted is not suggested. For production grade code one should consider using DAO design pattern.

Simple XML has been used to make Player object transportable as XML payload. You can also use plain text or JSON encoding as payload. Jersey has built in methods to support JSON and XML. I’ve chosen Simple XML mostly because of its simplicity. Also Jersey’s xml transport depends on Jaxb which was not supported in Google App Engine before and I have not tested Jaxb yet in Google App Engine after Jaxb made the whitelist. Maybe I’ll write a Jaxb tutorial next. :)

package com.dclonline.jerseyresources;

import javax.persistence.Entity;
import javax.persistence.Id;

import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;

import com.google.appengine.api.datastore.EntityNotFoundException;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;

@Entity
@Root
public class Player {

	@Element 	@Id 	String name;
	@Element 	String email;
	@Element	String phoneNumber;

	public Player() {
	}

	public Player(String name){
		this.name = name;
	}
	public Player(String name, String email, String phoneNumber) {
		this.name = name;
		this.email = email;
		this.phoneNumber = phoneNumber;
	}

	public void save() {
		Objectify ofy = ObjectifyService.begin();
		ofy.put(this);
	}

	public void delete() {
		Objectify ofy = ObjectifyService.begin();
		ofy.delete(this);
	}

	public Player get() throws EntityNotFoundException {
		Objectify ofy = ObjectifyService.begin();
		return ofy.get(Player.class,this.name);
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getPhoneNumber() {
		return phoneNumber;
	}

	public void setPhoneNumber(String phoneNumber) {

		this.phoneNumber = phoneNumber;
	}
	@Override
	public String toString() {
		return "Player [name=" + name + ", email=" + email + ",  phoneNumber="
				+ phoneNumber + "]";
	}
}

Players.java

Players is a container class to hold a collection of Player. Players only has Simple XML annotations to make it transportable over http as XML. I’m using Objectify’s query interface to retrieve Player objects from DataStore to populate the container Players List.  

package com.dclonline.jerseyresources;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import org.simpleframework.xml.ElementList;
import org.simpleframework.xml.Root;

import com.googlecode.objectify.Query;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;

@Root
public class Players {
	Logger log = Logger.getLogger(Players.class.getName());
	private List<Player> players;
	@ElementList
	public List<Player> getPlayers() {
		Objectify ofy = ObjectifyService.begin();
		Query<Player> q = ofy.query(Player.class);
		log.info("Count of players in query: " + q.count());
		Iterable<Player> playerItbl = q.fetch();
		players = new ArrayList<Player>();
		for (Player p : playerItbl)
		{
			players.add(p);
		}
		log.info("Count of players in List<Player> : " + players.size());
		return players;
	}

	@ElementList
	public void setPlayers(List<Player> players) {
		for (Player p: players){
			players.add(p);
		}
	}
}

ObjectifyContextListener.java

In order to make a object persist-able using Objectify library, the object must be registered with ObjectifyService. To make sure Player object is ready to be objectified, servlet context listener is used. A listener will be invoked before any servlets in a web application and will make sure our registration piece of code gets run. If you plan to use DAOBase, a helper class provided by Objectify library, there would be no need for context listener though.

package com.dclonline.jerseyresources;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import com.googlecode.objectify.ObjectifyService;

public class ObjectifyContextListener implements ServletContextListener {

	public void contextInitialized(ServletContextEvent event) {
			ObjectifyService.factory().register(Player.class);
	}
	public void contextDestroyed(ServletContextEvent event) {
		//
	}
}

REST Method Implementation:

I’ve described in detail how to make Jersey work with Google App Engine on a tutorial before. For this tutorial I’ll only highlight code that is required to get GWT working with Jersey. Jersey related files consist of PlayersResource.java, MainJersey.java, and web.xml.

PlayersResource.java

This class is the main controller where all HTTP call to Player resources will be routed from. For the sake of this tutorial we are using GWT as our client, but you can use any HTTP client to interact with Player resource. Resource retrieval and storing code is embedded but for production quality project we probably want them in separate model class(es).

package com.dclonline.jerseyresources;

import java.io.StringWriter;
import java.util.logging.Logger;

import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;

import org.simpleframework.xml.Serializer;
import org.simpleframework.xml.core.Persister;

import com.google.appengine.api.datastore.Email;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.PhoneNumber;

@Path("/players")
public class PlayersResource {
	Logger log = Logger.getLogger(PlayersResource.class.getName());

	@GET
	@Produces("text/plain")
	public String getAllPlayers() {
		Players players = new Players();

		Serializer serializer = new Persister();
		StringWriter stringWriter = new StringWriter();
		try {
			serializer.write(players, stringWriter);
		} catch (Exception e) {
			return "Exception while converting to xml " + e.getMessage();
		}
		log.info(stringWriter.toString());

		return stringWriter.toString();
	}

	@POST
	@Produces("text/plain")
	public String createMultiplePlayers() {
		return "@POST : 'Create multiple players' has not been implemented yet";
	}

	@PUT
	@Produces("text/plain")
	public String updateMultiplePlayers() {
		return "@PUT : 'Update multiple players' has not been implemented yet";
	}

	@DELETE
	@Produces("text/plain")
	public String deleteMultiplePlayers() {
		return "@DELETE : 'Delete multiple Players' has not been implemented yet";
	}

	@GET
	@Path("{name}")
	@Produces("application/xml")
	public String getPlayerByName(@PathParam("name") String name){
		Player player = null;
		try {
			player = new Player(name).get();
		} catch (EntityNotFoundException e) {
			return "Player not found";
		}

		Serializer serializer = new Persister();
		StringWriter stringWriter = new StringWriter();
		try {
			serializer.write(player, stringWriter);
		} catch (Exception e) {
			return "Exception while converting to xml " + e.getMessage();
		}
		log.info(stringWriter.toString());
		return stringWriter.toString();
	}

	@POST
	@Path("{name}")
	@Consumes("application/x-www-form-urlencoded")
	@Produces("text/plain")
	public String createPlayerByName(	@PathParam("name") String name,
										@FormParam("email") String email,
										@FormParam("telephone") String telephone) {

		Player player = new Player(name,email,telephone);
		player.save();

		return player.toString();
	}

	@PUT
	@Path("{name}")
	@Produces("text/plain")
	public String updatePlayer(@PathParam("name") String name) {
		return "@PUT : Update player's info has not been implemented yet";
	}

	@DELETE
	@Path("{name}")
	@Produces("text/plain")
	public String deletePlayer(@PathParam("name") String name) {
		Player player = new Player(name);
		try {
			player.get();
			player.delete();
			return ("Player named: '"+name+ " got deleted");
		}catch (EntityNotFoundException e){
			return "Player named : '"+name+"' could not be found on DataStore";
		}
	}
}

MainJersey.java

Resource class PlayersResource is getting registered as resource here.

package com.dclonline.jerseyresources;

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.core.Application;

public class MainJersey extends Application {
	public Set<Class<?>> getClasses() {
		Set<Class<?>> s = new HashSet<Class<?>>();
		s.add(PlayersResource.class);
		return s;
	}
}

Creating Front-End with GWT UiBinder:

Let me show the screen shot of the finished GWT form. I’ve used two GWT modules on player.html host page. Upper portion (blue background) of player.html is rendered using PlayersEntryModule and the lower portion is rendered with PlayersDisplayModule. PlayersDisplayModule is almost identical in nature to PlayersEntryModule. If you need detail step by step instruction to create GWT UiBinder widgets please view tutorials listed in “Resource” section. In this tutorial I’ll mostly describe PlayersEntryModule and associated files and how they are integrated with Jersey.

players.html

GWT uses standard html file to display modules. In GWT world these html files are called host pages. There is nothing special we need to do as far as Jersey integration goes.

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>mywidgetbinder</title>
    <script type="text/javascript" language="javascript" src="com.dclonline.PlayersEntryModule/com.dclonline.PlayersEntryModule.nocache.js"></script>
    <script type="text/javascript" language="javascript" src="com.dclonline.PlayersDisplayModule/com.dclonline.PlayersDisplayModule.nocache.js"></script>
  </head>

  <body>
  	<div id="DataEntry"></div>
  	<div id="DataDisplay"></div>
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>

  </body>
</html>

PlayersEntryModule.gwt.xml

This is a standard gwt module file. Note the line where HTTP is getting inherited to make the module capable of making HTTP method calls to Jersey as Jersey do not understand RPC methods.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC "-//Google Inc.//DTD Google Web Toolkit 2.0.0//EN" "http://google-web-toolkit.googlecode.com/svn/tags/2.0.0/distro-source/core/src/gwt-module.dtd">
<module>
	<inherits name="com.google.gwt.user.User" />
	<inherits name="com.google.gwt.http.HTTP" />
	<entry-point class="com.dclonline.binders.PlayersEntryBinderEntryPoint" />
	<source path="binders" />
</module>

PlayersEntryBinderEntryPoint.java

This entry point class exposes GWT binder class to host page “palyer.html” by adding PlayersEntryBinder.java to RootPanel. No Jersey specific coding task in here.

package com.dclonline.binders;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.user.client.ui.RootPanel;

public class PlayersEntryBinderEntryPoint implements EntryPoint {

	@Override
	public void onModuleLoad() {
		PlayersEntryBinder wb = new PlayersEntryBinder();
		RootPanel.get("DataEntry").add(wb);
	}
}

PlayersEntryBinder.ui.xml

This xml template file is a part of standard UiBinder files. Again no Jersey specific code embedded in this xml file.

<pre><!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
<ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder"
	xmlns:g="urn:import:com.google.gwt.user.client.ui">
	<ui:style>
		/* Add CSS here. See the GWT docs on UI Binder for more details */
		.important {
			font-weight: bold;
		}
	</ui:style>
	<g:HTMLPanel>

		<div style="background-color:#5CADFF;width:800px">
			<h3>Create / Delete Player</h3>
			<table>
			<tr><td><b>Name</b></td><td><g:TextBox ui:field="txtBox_Name"></g:TextBox></td></tr>
			<tr><td><b>E-mail</b></td><td><g:TextBox ui:field="txtBox_EMail"></g:TextBox></td></tr>
			<tr><td><b>Telephone</b></td><td><g:TextBox ui:field="txtBox_Telephone"></g:TextBox></td></tr>
			<tr>
				<td><g:Button styleName="{style.important}" ui:field="SavePlayerInfoBtn" /></td>
				<td><g:Button styleName="{style.important}" ui:field="DeletePlayerInfoBtn" /></td>
			</tr>
			</table>
		</div>
		<div>
			<g:Grid ui:field="myGrid"></g:Grid>
		</div>
	</g:HTMLPanel>
</ui:UiBinder>

PlayersEntryBinder.java

This the UiBinder owner class for PlayersEntryBinder.ui.xml template file. This is where UI logic and event handlers resides. HTTP Calls to Jersey are invoked using GWT’s RequestBuilder class. I have two buttons to save and delete player on ui.xml template files. For example, if an user clicks “save” button then I’ll capture the onClick event handler in @UiHandler event handler logic and route the “save” action to “savePlayer(….)” method. “savePlayer(…)” method calls appropriate HTTP method (i.e. GET, POST….) with the right URI using RequestBuilder. To understand communication between GWT and Jersey we will need to examine PlayersEntryBinder.java along with PlayersResource .java (Jersey controller listed above). It is always helpful to sketch a map of HTTP methods with URI to their expected functions to comprehend routing logic as shown at the beginning of this tutorial. Code to implement “delete” button is very similar to “save” button. On GWT version 2.0.0 RequestBuilder did not support Http “DELETE” or “PUT” methods. Workaround to implement “DELETE” or “PUT” is to subclass RequestBuilder class with a inner class RequestBuilderDELETE as shown. GWT version 2.0.1 is supposed to come out with native support for “DELETE” and “PUT”. Also note the use of call back handlers. GWT RequestBuilder makes asynchronous call to server and you will always need call back handler to process returned results from server. This asynchronous nature should be familiar to seasoned Ajax programmers.

<pre>package com.dclonline.binders;

import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.http.client.Request;
import com.google.gwt.http.client.RequestException;
import com.google.gwt.http.client.Response;
import com.google.gwt.http.client.RequestBuilder;
import com.google.gwt.http.client.RequestCallback;
import com.google.gwt.http.client.URL;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.Widget;

public class PlayersEntryBinder extends Composite {

	private static PlayersEntryBinderUiBinder uiBinder = GWT
			.create(PlayersEntryBinderUiBinder.class);

	interface PlayersEntryBinderUiBinder extends UiBinder<Widget, PlayersEntryBinder> {
	}

	@UiField 	TextBox txtBox_Name;
	@UiField 	TextBox txtBox_EMail;
	@UiField 	TextBox txtBox_Telephone;
	@UiField 	Button SavePlayerInfoBtn;
	@UiField 	Button DeletePlayerInfoBtn;
	@UiField 	Grid myGrid;

	public PlayersEntryBinder() {
		initWidget(uiBinder.createAndBindUi(this));

		// Can access @UiField after calling createAndBindUi
		SavePlayerInfoBtn.setText("Save");
		DeletePlayerInfoBtn.setText("Delete");
	}

	@UiHandler( { "SavePlayerInfoBtn", "DeletePlayerInfoBtn" })
	void onClick(ClickEvent e) {
		GWT.log("at UiHandler button", null);

		String name = txtBox_Name.getText();
		String email = txtBox_EMail.getText();
		String telephone = txtBox_Telephone.getText();

		if (e.getSource().equals(SavePlayerInfoBtn)) {
			if (name.isEmpty() || email.isEmpty() || telephone.isEmpty()) {
				Window
						.alert("Please provide Player's name, email, and phonenumber");
			} else {
				savePlayer(name, email, telephone);
			}
		} else if (e.getSource().equals(DeletePlayerInfoBtn)) {
			if (name.isEmpty()) {
				Window
						.alert("I don't know which player to delete. Please provide player's name to delete");
			} else {
				deletePlayer(name);
			}

		}
	}

	public class RequestBuilderDELETE extends RequestBuilder {
		public RequestBuilderDELETE(String url) {
			super("DELETE", url);
		}
	}

	private String deletePlayer(String name) {
		String url = "/rest/players/" + name;
		RequestBuilder builder = new RequestBuilderDELETE(URL.encode(url));
		builder.setCallback(deletePlayerCallBackHandler);
		try {
			Request request = builder.send();
		} catch (RequestException e) {
			GWT.log("RequestException ", e);
		}

		return "ok";
	}

	RequestCallback deletePlayerCallBackHandler = new RequestCallback() {
		public void onError(Request request, Throwable exception) {
			GWT.log("Error", exception);
		}

		public void onResponseReceived(Request request, Response response) {
			String responsetext = response.getText();
			GWT.log("responsetext", null);
			GWT.log(responsetext, null);
			myGrid.resize(1, 1);
			myGrid.setBorderWidth(1);
			myGrid.setSize("500px", "30px");
			myGrid.setVisible(true);
			myGrid.setText(0, 0, "Response from Server  : " + responsetext);

		}
	};

	private String savePlayer(String name, String email, String telephone) {
		String url = "/rest/players/" + name;
		String data = "email=" + email + "&" + "telephone=" + telephone;

		RequestBuilder builder = new RequestBuilder(RequestBuilder.POST, URL
				.encode(url));
		builder.setHeader("Content-type", "application/x-www-form-urlencoded");
		builder.setRequestData(data);
		builder.setCallback(savePlayerCallbackHandler);
		try {
			Request request = builder.send();
		} catch (RequestException e) {
			GWT.log("RequestException ", e);
		}

		return "ok";
	}

	RequestCallback savePlayerCallbackHandler = new RequestCallback() {
		public void onError(Request request, Throwable exception) {
			GWT.log("Error", exception);
		}

		public void onResponseReceived(Request request, Response response) {
			String responsetext = response.getText();
			GWT.log("responsetext", null);
			GWT.log(responsetext, null);
			myGrid.resize(1, 1);
			myGrid.setBorderWidth(1);
			myGrid.setSize("500px", "30px");
			myGrid.setVisible(true);
			myGrid.setText(0, 0, "Response from Server  : " + responsetext
					+ " got created!!");
		}
	};
}

To make this tutorial short I have not listed any code related to PlayersDisplayModule and related files (i.e. PlayersDisplayBinder.java, PlayersDisplayBinder.ui.xml, PlayersDisplayEntryPoint.java, and PlayersDisplayModule.gwt.xml) that is responsible for rendering bottom portion of the GWT form (i.e. Green background section of form). Please download full source code to view these files from Resource section.

Project’s web.xml

Examine web.xml for configuration of Jersey and Objectify

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"

	version="2.5">

	<!-- Jersey Servlets -->
	<servlet>
		<servlet-name>Jersey Web Application</servlet-name>
		<servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
		<init-param>
			<param-name>javax.ws.rs.Application</param-name>
			<param-value>com.dclonline.jerseyresources.MainJersey</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>Jersey Web Application</servlet-name>
		<url-pattern>/rest/*</url-pattern>
	</servlet-mapping>

	<!-- Objectify Context Listener -->
	<listener>
		<listener-class>com.dclonline.jerseyresources.ObjectifyContextListener</listener-class>
	</listener>

	<!-- Default page to serve -->
	<welcome-file-list>
		<welcome-file>players.html</welcome-file>
	</welcome-file-list>
</web-app>

Source Code Folder Structure:

Conclusion:

Hope this tutorial help you out with your GWT UiBinder and Jersey projects in some ways. Again to understand the integration between GWT UiBinder and Jersey please focus on PlayersEntryBinder.java, PlayersDisplayBinder.java. and PlayersResource.java files. I’ve posted location to complete source code of this project in Resource section.

Resources:

Comments

  1. Great tutorial!

    At my company I have developed a similar setup with GWT but with added authentication. Authentication is a difficult topic in JAX-RS webservices. If you are interested I will share it on my blog :)

    Thanks again, I will retweet this for sure.

    • iqbalyusuf says:

      Thanks… I would also like you to post your blog’s url here so that people can hyper-link to similar contents. :)

  2. Sure! The url is http://thezukunft.com :) You an also click my name to get there.

    Cheers, Jonas

  3. Harald says:

    Nice tutorial!

    As I have several GWT clients talking to RESTful apps with XML representations, I wrote a little XML mapper for GWT. If someone is interested, please take a look at http://code.google.com/p/piriti/.

    Cheers Harald

  4. Awesome tutorial. I’ve decided to use this setup in a new project. Using the IvyDE plugin for Eclipse, you can setup Ivy to easily handle the the dependencies. Here’s the code…

    ivysettings.xml:

    /////
    ivy.xml:

  5. Lol… no xml allowed =)

  6. Ben says:

    Seriously do NOT put underscores in your Java variable names. It’s against the standard.

  7. I have Mac and would like to know will it work through “Flux”? This is soft for web-programming.

  8. I would like to deploy this Jersey-gWT integration on Tomcat instead of the appengine. I am trying to do this, but tomcat cannot find the servlets while the application is working fine under appengine. Please could you post any ideas on how In can do that.

    Thanks

    • iqbalyusuf says:

      Thanks for the inquiry. I had a non-polished how to tutorial to help my developers to build GWT+Jersey on Tomcat using Eclipse environment. This is a new tutorial and the reason I did not extend this Google App Engine oriented tutorial is that this project has a Objectify dependency which only works in Google App Engine Java.

      Now here is the link to my new tutorial. Hope this helps.

      http://blog.iparissa.com/javasnippets/jersey-gwt-on-tomcat-in-eclipse/

      You would find project files at bitbucket at Resource section. The reason I did not extend this Google App Engine oriented tutorial is that this project has a Objectify dependency which only works in Google App Engine Java.

Leave a Reply