Tuesday, February 5, 2013

A Google Cloud Endpoints Hello-World example in Java using Eclipse


Introduction

For the last couple of days I've been working to get a simple REST service up and running at Google AppEngine using the Google Cloud Endpoints framework presented at Google IO 2012. The introduction at the page just linked to in addition to the documentation gave the impression that this was a very simple and straight forward framework to use. It might be, but I still used three days to get a hello world application up and running. The reason for this is compound. I may be an idiot, but I will also put part of the blame on an incomplete documentation by Google. The Google Cloud Endpoints techonology ("Endpoints" from now) is still only in semi-closed beta though, so I should probably not expect more. In any case, I am writing this blog post so that others will only use the single hour needed to get a hello world app up and running instead of the full three days* I used.

*) Some of this time was spent testing other PaaS REST technologies as well as waiting for answers on forum posts, but still...

Access, installation and environment

The Endpoints technology is available for all to use - you do not need to join the trusted testers group other than for getting access to the documentation. You should still of course do this. I will also refer to this documentation in this post. I'm just saying that the framework is indeed embedded in the AppEngine SDK v1.7 which I am using.

I will be using the following IDE, framework and SDK versions in this example. Installation of these are out of the scope of this post but I will provide a couple of links:

After installing the AppEngine SDK and the plugin for Eclipse you shall have the Google Toolbar available as well. Use this as a verification that you're done with the installation phase.

Creating the project

  1. Go to File > New other or hit Ctrl+N in Eclipse. Select Google > Web Application Project.

  2. Enter a project name, a package name and uncheck the use of Google Web Toolkit which is a GUI framework we won't be needing for a back-end solution. I will be using "com.example" as package name in this example. Whenever I refer to this package name later, replace with your.
  3. Delete /src/com.example/HelloWorldServlet.java and /war/index.html as we will not be using these.
  4. Create a new class /src/com.example/HelloWorldEndpoint.java. Let the content be the following:
    package com.example;
    
        import com.google.api.server.spi.config.Api;
    
        @Api(name = "example")
        public class HelloWorldEndpoint {
         public Container getThing() {
          Container c = new Container();
          c.Text = "Hello world!";
          return c;
         }
         
         public class Container {
          public String Text;
         }
        }
       
  5. Edit /war/WEB-INF/web.xml and set the init-param SystemServiceServlet to point to HelloWorldEndpoint instead of HelloWorldServlet. You can delete the HelloWorld-servlet definition entierly, leaving the final web.xml to look like this (namespaces in web-app tag removed for brevity):
    
        
         
          SystemServiceServlet
          com.google.api.server.spi.SystemServiceServlet
          
           servicescom.example.HelloWorldEndpoint
         
         
          SystemServiceServlet
          /_ah/spi/*
         
        
    

Testing

Launch your app by selecting Run > Run while for instance HelloWorldEndpoint.Java file is open. Jetty Application Server will launch and the Console in Eclipse will display a bunch of output. One of the final lines should be this, which indicates you're good to go:

INFO: The admin console is running at http://localhost:8888/_ah/admin

Your port may vary from 8888. Use this info to launch a web browser at the following URL, replacing 8888 with whatever port your server is running at:

http://localhost:8888/_ah/api/example/v1/container

The result should be the following:

How URL's are generated

UPDATE 11th Feb 2013: The following section was written without knowledge of the path and httpMethod-parameters of the @ApiMethod annotation. See the documentation. While the below is still true, it's far less important to know how URL's are generated because you're not bound to it.

Now this is the part that got me. The official Google Cloud Endpoint documentation for Java (as per February 5th 2013) does not specify in detail how the URL's are generated. Instead it encourages you to go ahead and create the client libraries and consume the service that way. While that is the end goal for me as well, I like to know the intermediate steps so that I can debug better and possibly open up for creating clients not supported by the client-code-generating library, like Windows Phone.

The URL's to access your services in a HTTP REST-manner*** is generated in a combination of class/method annotation and code convention. This is the general rule from the point of view of a URL:

http://host:port/ServiceRoot/ApiName/Version/Entity/param1/param2/paramN

KeywordMaps to
ServiceRootAs configured in <url-pattern> of the servlet mapping in web.xml
ApiNameThe name-parameter of the @Api annotation of the class mapped in services class in web.xml (API-class from now).
VersionThe version parameter of the @Api annotation of the class mapped in services class in web.xml. (Default: "v1")
EntityThe class name in lowercase which an API method returns - except for remove-methods, where it's taken from the postfix of the method name (sigh...)
Param1/2/N(Optional) Value of the N'th @Named annotation of any method parameter of type String or int

***) The framework also generates endpoints accessible through some HTTP RPC URL-convention, which I don't know how works. Feedback on this appreciated.

In addition to this, the prefix of any method name in the API-class will determine to which HTTP request method it will respond. The full table of these prefixes, from my knowledge is:

PrefixHTTP methodReturn constraintInput constraint
getGETAny class TNone or any number of @Named parameter of type String/int
listGETT[] or List<T>None or any number of @Named parameter of type String/int
insertPOSTAny class T1One class T2**** and any number of @Named parameter of type String/int
updatePUTAny class T1One class T2 and any number of @Named parameter of type String/int
removeDELETEAny class TNone or any number of @Named parameter of type String/int

****) Should, but need not, be equal to type T1

In all of the examples, class T is typically any business entity class. It cannot be System.String.

Just to summarize and underline some of the facts I've deduced from these rules and from experimenting:
  1. The framework relies heavily on reflection and code convention to find out which methods to expose and how
  2. You do not need to annotate any methods, although the @ApiMethod can be used for convenience (see client library generation)
  3. All parameters must be annotaded with @Named.
  4. If you break any of these rules the framework will give you a 404 error without any further explanation.

A couple of examples:

1. From the hellow world-example with one get-method, this is how the framework maps it to a URL:

CODE

URL

HTTP GET http://localhost:8888/_ah/api/example/v1/container


2. An insert method with GET-parameter in addition to object payload

CODE

URL

HTTP POST http://localhost:8888/_ah/api/example/v2/container/4381294


3. Remove method

CODE

URL

HTTP DELETE http://localhost:8888/_ah/api/example/v1/container/4381294

No it's not a mistake that I've now red-boxed the method postfix "Container" in "removeContainer". The remove method does not follow the same code convention as the others. Thank you Google. I sincerely hope this is a bug.


Troubleshooting

Finally I'd like to mention a couple of pointers as to where to look if you can't make this work. These are pitfalls I've gone into. So, in no particular order:
  • Look for errors messages in the Jetty error console
  • Make sure war/WEB-INF/yourapname-v1.api is autogenerated after each compilation. Try removing the @Named-annotation in front of a random input parameter for a method. You will notice that the .api file is not generated. This will result in all of your methods returning 404-errors.
  • Make sure you always shut down the server before launching a new version on the dev-server. Or else your old code is still running and serving. See illustration:


Final words

I really like the idea of Google pushing their own REST framework for AppEngine. It eliminates one source of dependency problems which frameworks like Jersey and RESTlet gives you. What I don't like is the poor documentation and the inconsequent code convention. I guess that's what you get for working with beta software. Just be cautious pushing any code to production knowing this. I'm still going to use this as back-end for my Android software. Solely for the reason that this is the only REST-framwork I've gotten to run on AppEngine. I even went so far as to try Microsoft Azure for Java in desperation, but more on that in another post ;-)

10 comments:

  1. really congratulations for your tutorial !

    ..Google is often too simple into his documentation and tutorials offered .. if we see that most of GDG, GTUG, Hackathon and other are repetitive with exercises posed by Google, the level of detail of your development is really important..

    best regards
    @Mlaynes
    http://mlaynessanchez.blogspot.com

    ReplyDelete
  2. Thanks so much for this tutorial! It has really helped me a lot and hopefully lot of GAE beginners.

    I followed all your suggestions but somehow i still get 404 errors. Here is a sample of my code and .api file. Please help me if you can.

    @Api (name="Myapp", version="v1")
    public class TestEndpoint {
    private static PersistenceManager getPersistenceManager() {
    return PersistenceManagerUtil.get().getPersistenceManager();
    }

    public Test insert(@Named("username") String username, @Named("password") String password){
    Test test=null;
    test.setUsername(username);
    test.setPassword(password);

    PersistenceManager pm = getPersistenceManager();
    pm.makePersistent(test);
    pm.close();
    return test;
    }
    }

    .api file is as follows

    "Myapp.testEndpoint.insert" : {
    "path" : "test/{username}/{password}",
    "httpMethod" : "POST",
    "scopes" : [ ],
    "audiences" : [ ],
    "clientIds" : [ ],
    "rosyMethod" : ".TestEndpoint.insert",
    "request" : {
    "parameters" : {
    "username" : {
    "type" : "string",
    "required" : true
    },
    "password" : {
    "type" : "string",
    "required" : true
    }
    },
    "body" : "empty"
    },
    "response" : {
    "body" : "autoTemplate(backendResponse)"
    }
    }
    }

    ReplyDelete
    Replies
    1. Its working finally! :) As seen above, i had initialised test obj to null and so there was a NullPointerException which curl did not show but the APIs explorer showed.

      Suggest people to test the APIs on the APIs explorer instead of curl (which shows 404 for every error )

      Delete
  3. If you run "curl -i" you should get full header with correct response code. But which client to use is pretty much a personal prefefrence. Fiddler2 and "Invoke-RestMethod" in PowerShell are two other options

    ReplyDelete
  4. http://ido-green.appspot.com/CloudEndpoints/CloudEndpointsWebBlogPost.html

    Read this tutorial.

    ReplyDelete
  5. Really good blog:)I felt this very helpful,,,Even I faced same problem by trying Rest service in GAE but it didnt work ,then I choosed End points as last solution it worked for me Thank you.

    ReplyDelete
  6. I searched everywhere for a clean working example and failed to find it till now.
    Thank you sir

    ReplyDelete
  7. Thank you!!! I killed a lot of time on this. It is a first example that actually works!

    ReplyDelete
  8. Thanks... same here.. I've been trying to understand how to create REST service on GAE for almost 2 days. Read pages of documentation, tried different ways, tutorials...but nothing. This one finally works well, it's well exlpained and simple :D
    Just one thing. I had 404 thrown always, after a while I realized the problem is that my JDO/JPA version was v2... I changed it to v1 and finally worked.

    ReplyDelete
  9. Really? You had to use v2 of JDA/JPO? I had that problem with Jersey, but not with GCE. Own blog entry regarding Jersey:
    http://www.nilzorblog.com/2013/02/a-helloworld-example-of-jersey-rest.html

    v1 works well enough - I've actually got an app in production using that (www.chineseshowdown.com) . There's a couple of features I miss though, and you'll have a hard time getting support if you ever need it.

    ReplyDelete