Chrome, HTML5, JavaScript, mobile, webdev

HTML5 Modern Web App and Google Cloud Endpoints (Part 2 Of 3)

Pre-reqs

  1. Google Plugin for Eclipse
  2. Google API JavaScript client library
  3. Sign up for Cloud Endpoints

User Experiences demands are pushing modern web apps to a more distributed architecture.  A pattern many developers have used is using a MVC framework on the client and communicate to the server with REST. Google App Engine’s easy to build, easy to manage environment makes it ideal for REST APIs for Web backends.  At Google IO 2012, we made it much easier to build REST APIs on App Engine with Cloud Endpoints.

Cloud Endpoints enables you to build REST and RPC APIs in App Engine.  It leverages the same infrastructure  Google uses for its own API and as such, you get strongly typed clients for Android, and IOS as well as a lightweight client for JavaScript which we will be walking through in this presentation.

In getting ready for IO, Ido and I thought we’d build a modern web application using Cloud Endpoints.  We decided on a topic that would be relevant to both App Engine and the general web developer community, something we both have some interest in and something generally useful…. a Beer rating and review web application.

Try it out yourself at:  http://birra-io2012.appspot.com/

The general architecture of the app is very basic.  We are storing beer metadata in the data store, doing some application logic on the app engine app, then exposing a REST based interface to a web client.

It’s not an easy definition since web technology is running very fast. Nevertheless, we can find certain features across successful web apps:

  • Self Contained & Functional– They all have one specific goal and they do their best to provide the users the functionality to get to her goal. Few examples:
    1. New York Times – Consume news.
    2. Hipmunk – Find a flight that is the ‘perfect’ for your needs.
    3. Gojee – Find the recipe you would taste and say WOW after it.
  • “Offline first” – You will want your web app to work offline. It’s an important feature that will let your users be productive on places like: planes, subways etc’. Another very important benefit will be the improve in performance. Since the app will work locally (first) and then sync the state/data the users will get responsiveness that are far better from an app that need to ‘wait’ for network on every action that the user does.
  • Client Side Architecture – Since we are moving big parts of our ‘logic’ to the client we need to think about methods that will keep your code readable and maintainable. I guess this is the main reason why we see so many MVC Frameworks. The only advice we can give here is to try few of the popular ones and see how they approach the separation between the data and the UI. If you have some time go over The Top 10 Javascript MVC Frameworks Reviewed. Then, after you try ‘some’ you will be able to pick the one that fit the bill in the best way. For the busy developer (I know… most of us don’t have too much free time – Go and play with these three MVC:
    1. Ember.js – Don’t waste time making trivial choices
    2. Angular.js -Lets you extend HTML vocabulary for your application
    3. Backbone.js – gives structure to web applications by providing models with binding, collections and views.
  • Device Aware – We all know that mobile is going ‘up and right’. So when you design your web app you should think on progressive enhancement and how it will fit  to different screen sizes. Good examples to look at are: Gmail, Google plus etc’. If you want to go deeper on this interesting subject there is a webcast on ‘mobile web apps’ I did with Oreilly three weeks ago.

Let’s take a look at what it takes to build this application. I have Eclipse and the latest version of the Google Plugin for Eclipse installed.  We create a new Web Application, call it birra.

Then, if we are going to build a Beer rating app… the first thing we are going to need is… beer!

Let’s create a Beer class.

It is just a standard POJO for storing information about a beer..

public class Beer {

  private Long id;
  private String beerName;
  private String kindOfBeer;
  private Long score;
  private Long numberOfDrinks;
  private Text image;
  private String country;
  private String description;
  private Double latitude;
  private Double longitude;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getBeerName() {
    return beerName;
  }

  public void setBeerName(String beerName) {
    this.beerName = beerName;
  }

  public String getKindOfBeer() {
    return kindOfBeer;
  }

  public void setKindOfBeer(String kindOfBeer) {
    this.kindOfBeer = kindOfBeer;
  }

  public Long getScore() {
    return score;
  }

  public void setScore(Long score) {
    this.score = score;
  }

  public Long getNumberOfDrinks() {
    return numberOfDrinks;
  }

  public void setNumberOfDrinks(Long numberOfDrinks) {
    this.numberOfDrinks = numberOfDrinks;
  }

  public Text getImage() {
    return image;
  }

  public void setImage(Text image) {
    this.image = image;
  }

  public String getCountry() {
    return country;
  }

  public void setCountry(String country) {
    this.country = country;
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public Double getLatitude() {
    return latitude;
  }

  public void setLatitude(Double latitude) {
    this.latitude = latitude; 
  }

  public Double getLongitude() {
    return longitude;
  }

public void setLongitude(Double longitude) {
  this.longitude = longitude;
}

}

Ok, that is a great Beer — now we want to teach App Engine how to persist instances of this beer class in the datastore.  To do that, we will use the classic Java JDO support.  You can of course use any persistence layer (or roll your own) with Cloud Endpoints.

import javax.jdo.annotations.IdGeneratorStrategy;
import javax.jdo.annotations.IdentityType;
import javax.jdo.annotations.PersistenceCapable;
import javax.jdo.annotations.Persistent;
import javax.jdo.annotations.PrimaryKey;
@PersistenceCapable(identityType = IdentityType.APPLICATION)

public class Beer {

  @PrimaryKey
  @Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
  private Long id;

Now we need to expose these Beers to the JavaScript client.  To do that, we need to use the new cloud Endpoints feature.

That creates a template class that does the basic List, Add, Update, and Delete operations.  The code is meant to be a starter that you can modify to add your application logic.

@Api(name = "birra")
In line 1 above we use the @Api attribute.
This attribute tells App Engine to expose this class as a REST\RPC endpoints.
Be aware that all the public methods on this class will be accessible via REST endpoint.
I have also changed the name to birra to match with the rest of the application.
We will later see this name show up the REST URL.
public class BeerEndpoint {
/**
* This method lists all the entities inserted in datastore.
* It uses HTTP GET method.
*
* @return List of all entities persisted.
*/
@SuppressWarnings({"cast", "unchecked"})
public List<Beer> listBeer() {
PersistenceManager mgr = getPersistenceManager();
List<Beer> result = null;
try{
Query query = mgr.newQuery(Beer.class);
result = (List<Beer>) query.execute();
// Tight loop for fetching all entities from datastore and accommodate
// for lazy fetch.
for (Beer beer:result);
} finally {
mgr.close();
}
return result;
}
In 2-24, GPE has defined a basic list function.
It simply returns all the Beers in the datastore.
This method will be exposed as a http GET on method
/**
* This method gets the entity having primary key id.
* It uses HTTP GET method.
*
* @param id the primary key of the java bean.
* @return The entity with primary key id.
*/
public Beer getBeer(@Named("id") Long id) {
PersistenceManager mgr = getPersistenceManager();
Beer beer = null;
try {
beer = mgr.getObjectById(Beer.class, id);
} finally {
mgr.close();
}
return beer;
}
In the above section, GPE has given us a basic get method.
Given an ID for a beer, it will return the full beer instance.
It is exposed as an HTTP GET.. for example beer/42
/**
* This inserts the entity into App Engine datastore.
* It uses HTTP POST method.
*
* @param beer the entity to be inserted.
* @return The inserted entity.
*/
public Beer insertBeer(Beer beer) {
PersistenceManager mgr = getPersistenceManager();
try {
mgr.makePersistent(beer);
} finally {
mgr.close();
}
return beer;
}
In the above section, GPE has given us a basic insert method.
It takes the Beer and stores it in the datastore, which gives it
an ID, then we return it back to the client.
It is exposed as an HTTP POST.
/**
* This method is used for updating a entity.
* It uses HTTP PUT method.
*
* @param beer the entity to be updated.
* @return The updated entity.
*/
public Beer updateBeer(Beer beer) {
PersistenceManager mgr = getPersistenceManager();
try {
mgr.makePersistent(beer);
} finally {
mgr.close();
}
return beer;
}
In the above section, GPE has given us a basic update method.
It takes the Beer updates it in the database.
It is exposed as an HTTP PUT.
/**
* This method removes the entity with primary key id.
* It uses HTTP DELETE method.
*
* @param id the primary key of the entity to be deleted.
* @return The deleted entity.
*/
public Beer removeBeer(@Named("id") Long id) {
PersistenceManager mgr = getPersistenceManager();
Beer beer = null;
try {
beer = mgr.getObjectById(Beer.class, id);
mgr.deletePersistent(beer);
} finally {
mgr.close();
}
return beer;
}
In the above section, GPE has given us a basic delete method.
It takes an ID of a beer and removes it from database.
It is exposed as an HTTP DELETE.
private static PersistenceManager getPersistenceManager() {
return PMF.get().getPersistenceManager();
}
}

Let’s test this locally with curl to ensure it is working well before we start to build out a our web client.   First we start the local development time server by hitting debug.

Now we see our local development time server up and running.  From there we also get hint as to the right URL to use for curl.

% curl http://localhost:8888/_ah/api/birra/v1/beer
{
  "items" : [ ]
}

Above you can see we are hitting our local development time server.  Then we use the _ah namespace which is the standard app engine reserved namespace… this ensures there are not conflicts with any web pages in your app.  The we use the /api/birra path.  “birra” comes from the @Api() attribute at the top of the BeerEndpoints.java file.  Then we access the beer endpoint. This comes from the name of the type the List method returns. The results is an empty list encoded as JSON.

Let’s look at how we add an item.

% curl  -H 'Content-Type: appilcation/json' -d '{"beerName": "bud"}' http://localhost:8888/_ah/api/birra/v1/beer

{
  "id" : 1,
  "beerName" : "bud",
  "kindOfBeer" : null,
  "score" : null,
  "numberOfDrinks" : null,
  "image" : null,
  "country" : null,
  "description" : null,
  "latitude" : null,
  "longitude" : null
}

Above you can see we are doing an HTTP POST passing a json encoded blob where we set the beerName to bud.   The results is a JSON blob of the beer after it has been added to the datastore.  Notice it has an ID set which we can use to query for it later.

%  curl http://localhost:8888/_ah/api/birra/v1/beer/1

{
  "id" : 1,
  "beerName" : "bud",
  "kindOfBeer" : null,
  "score" : null,
  "numberOfDrinks" : null,
  "image" : null,
  "country" : null,
  "description" : null,
  "latitude" : null,
  "longitude" : null
}

The above code calls the getBeer method passing beerId of 1 and this returns a JSON blob of the beer. As we will see later, we can use the Google API JavaScript client library to access our methods.  To control what they look like in the JavaScript code, we can use the @Api attribute in the com.google.api.server.spi.config.ApiMethod package.    We will look a fair bit at the customization options this attribute gives us later in the post.

@ApiMethod(name=”beers.delete”)

public Beer removeBeer(@Named(“id”) Long id) {

That is great that we can do this level of testing on the client, but let’s take a look at a version of this I deployed earlier.  You can get a list of all the APIs on your app engine app by hitting:

https://<appid&gt;.appspot.com/_ah/api/discovery/v1/apis

Above you can see that discovery is tell us we have two APIs. The top one is the discovery API itself and the bottom one is the birra API we just created. Clicking on the discoveryRestUrl link we see the discovery doc for our API.  This is the exact same discovery doc format we use for most Google APIs and as such works with our Google Client Libraries.

In addition, we can use the Google API Explorer with our custom API.  It is very easy to get to by using the explorer shortcut.  http://<appid&gt;.appspot.com/_ah/api/explorer

Above we see  a list of all the methods we defined in our API.  Clicking on any of them allows me to interact with that method live.

Notice that we see the exact http request and the response.  This is a very nice way to debug the API independently from the client.  It also gives you all the insight into request and response formats to help you build out great clients.

Now, let’s take a look at using this API from JavaScript..

Client side code to get a list of beers

As we all know, in software development it’s good to take small steps. So first thing we will try to do is to run a simple ajax request.

var apiUrl = "https://birra-io2012.appspot.com/_ah/api/birra/v1/beer";
$.ajax({
  url: apiUrl,
  dataType: 'json',
  contentType: 'application/json',
  type: "GET",
  success: function(data) {
    showList(data);
  },
  error: function(xhr, ajaxOptions, thrownError) {
    console.error("Beer list error: " + xhr.status);
  }
});

You can try this generic test page from: https://github.com/greenido/backbone-bira/blob/master/test-page/index_ini_1.html

or test it live:
http://birra-io2012.appspot.com/test/

After we were successful in getting the list of beers with plain vanilla JavaScript, it’s time to use google api JavaScript library. Why? Well, you are getting some powerful features like:

  1. RPC Batch
  2. CORS
  3. Authentication out of the box
  4. Version control
  5. Super Simple to use.
  6. Generic

The call to get the JS library:

<script src="https://apis.google.com/js/client.js?onload=loadGapi">
{ "client": {},
  "googleapis.config": {
    root: "https://birra-io2012.appspot.com/_ah/api"
  }
}
</script>

The callback that will be called after Google API JS library was loaded. In our case, we wish to set the API key and load our amazing beer service: ‘birra’.
function loadGapi() {
  // Set the API key
  gapi.client.setApiKey('AIzaSyD_mrsCOGa_cip-_O9YzmruYQ831uQcqPE');
  // Set: name of service, version and callback function
  gapi.client.load('birra', 'v1', getBeers);
}

The callback function that will be called after our Birra service was loaded. In this case we fetch a list of beers (top 20 beers) andshow them to the user.
function getBeers() {
  var req = gapi.client.birra.listBeer();
  req.execute(function(data) {
    showList(data);
  });
}

Here are some of the basic Adding, Editing and Removing operations:

// Delete beer
$("#beerDelBut").click(function() {
$('#results').html(beerApp.callingServerHtml);
var req = gapi.client.birra.beers.delete({
'id' : $("#gbeerId").val()
});
req.execute(function(data) {
beerApp.showList(data);
});
});
// Add new beer
$("#beerAddBut").click(function(data) {
beerApp.clearFields();
$("#beerLocation").val(beerApp.curLocation);
$("#beerScore").change();
// Using our Geo information to have a small map of the area around us
var mapImg = '<img border=0 src="http://maps.googleapis.com/maps/api/staticmap?center=&#39; +
beerApp.curLocation + '&zoom=14&size=262x112&maptype=roadmap&markers=color:blue%7Clabel:S%7C' +
beerApp.curLocation + '&sensor=true"/>';
$("#localMap").html(mapImg);
$('#beerDetailsModal').modal('show');
});
// Actions for the modal
$("#cancelBeer").click(function(data) {
beerApp.clearFields();
$('#beerDetailsModal').modal('hide');
});
//
// Save beer - Add new or update
//
$("#saveBeer").click(function() {
console.log("Going to save the beer...");
var features = {};
// extract all the info from the form's fields
$("#beerDetailsModal input[id^='beer']").each(function() {
features[$(this).attr('name')] = $(this).val();
});
delete features['undefined'];
var latLong = features['location'].split(",");
delete features['location'];
// Get the select value as well.
features[$('#BeerNumDrinks').attr('name')] = $('#BeerNumDrinks').val();
if ($('#upImg')[0] !== null) {
var tmpImg = $('#upImg')[0]; // keep it one image per beer
var beerImg64 = getBase64Image(tmpImg);
features['image'] = {"value" : beerImg64};
}
var req;
if ( !features['beerId']) {
// In case we have an empty beerId, do not send it,
// so the server will take it as a new beer
delete features['beerId'];
features['latitude'] = beerApp.lang;
features['longitude'] = beerApp.long;
// Add new beer
req = gapi.client.birra.beers.insert( features );
}
else {
// It's an update of a beer
features['id'] = features['beerId'];
delete features['beerId'];
features['latitude'] = latLong[0];
features['longitude'] = latLong[1];
req = gapi.client.birra.beers.update( features );
}
req.execute(function(data) {
var tmpHTML;
if (data.error && data.error.code > 200) {
console.error("Err Code: " + data.error.code + " Err: " + data.error.message);
tmpHTML = data.error.message;
}
else {
tmpHTML = '<h4>Your Beer is Safe</h4>';
tmpHTML += "<img src='img/beer24.jpg'/>"
tmpHTML += 'id: ' + data.id + " Name: " + data.beerName;
}
$('#results').html("");
$('#alertContent').html(tmpHTML);
$('.alert').show();
});
$('#beerDetailsModal').modal('hide');
});

Let’s take a look at adding Comment support to this application.  I may want to allow people to leave comments on the beers.  To do that, let’s add a new Comment class and enable it for persistence into the datastore.

@PersistenceCapable(identityType = IdentityType.APPLICATION)
public class Comment {
@PrimaryKey
@Persistent(valueStrategy = IdGeneratorStrategy.IDENTITY)
private Long commentId;
private User user;
private String date;
private Long beerId;
private String comment;
public Long getCommentId() {
return commentId;
}
public void setCommentId(Long commentId) {
this.commentId = commentId;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public Long getBeerId() {
return beerId;
}
public void setBeerId(Long beerId) {
this.beerId = beerId;
}
public String getComment() {
return comment;
}
public void setComment(String comment) {
this.comment = comment;
}
}

Now, we need to expose Comment as part of our Birra REST API.    We use the Google->Generate Cloud Endpoint class option in eclipse.

We change the name to be birra to have it be part of the same API.

@Api(name = "birra")
public class CommentEndpoint {
/*
Then for listComment() method, notice it is returning all
the comments, but that is unlikely to be what we want on
the client.  We likely want to return just the comments
on a giving Beer.  To do that, we need to pass a BeerId in.
Notice we customize the path to make it very intuitive to
use as a REST interface. We also customize query for comments,
to only get those with a particular beer ID
*/
@ApiMethod(name="beers.comments.list",
path="beers/{beerId}/comments")
public List<Comment> listComment(@Named ("beerId"Long beerId) {
PersistenceManager mgr = getPersistenceManager();
List<Commentresult = null;
try{
Query query = mgr.newQuery(Comment.class"beerId == " + beerId);
result = (List<Comment>) query.execute()
We use the same sort of syntax all the methods on comment...
@ApiMethod(name="beers.comments.get",
path="beers/{beerId}/comments/{id}")
public Comment getComment(@Named ("beerId"Long beerId, @Named("id"Long id) {
@ApiMethod(name="beers.comments.insert",
path="beers/{beerId}/comments")
public Comment insertComment(@Named ("beerId"Long beerId, Comment comment) {
comment.setBeerId(beerId);
/*
Now, let’s take a look at the curl syntax for calling this method.
Here we do a HTTP GET for all the comments on Beer with the ID 1.
% curl http://localhost:8888/_ah/api/birra/v1/beers/1/comments
{
"items" : [ ]
}
There are none there, so let’s add one.
% curl   -H 'Content-Type: appilcation/json'  -d '{"comment": "nice head"}'  http://localhost:8888/_ah/api/birra/v1/beers/1/comments
{
"commentId" : 2,
"user" : null,
"date" : null,
"beerId" : 1,
"comment" : "nice head"
}
*/

It’s becoming much too long for a post… so here is the full article.

Also, if you miss the first part of this series: https://greenido.wordpress.com/2012/06/22/modern-web-apps-at-scale-with-google-app-engine-part-1-out-of-3/

As always, if you have any comments please share…

Advertisement
Standard

7 thoughts on “HTML5 Modern Web App and Google Cloud Endpoints (Part 2 Of 3)

  1. Kevin Lippiatt says:

    Thanks for the article. It’s a great follow-up to the I/O session video. As Cloud Endpoints are currently under a Trusted Tester programme – when will they be available to all as standard?

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s