I have been inching to try out google app engine and its capabilities with "app engine + java + BlazeDS + Flex", it works well on my personal website. But "remote object data accessing" is not suitable for client platform like Iphone and Android. I happend find a great topic talking about "app engine + java + spring + REST + JSON", it does help me with my Planned Iphone preject.
The end product will be spring mvc app running in google app engine handling GET, POST, PUT and DELETE http requests with JSON objects in and out utilizing JDO and seesions. And you can access it from you Iphone App.
So here you have it. Step by step.
Part One: Once you are finished with this article, you will be able to implement REST services in Google Apps Engine using JAVA, Spring 3.0 and JSON.
- Eclipse with google App engine. I am using Eclipse, which is my favorite IDE. There is a lot of article about how to install Google App Engine on eclipse, and google also have great help doc here: http://code.google.com/eclipse/docs/download.html. I believe you can get it done easily and finally you will got something like:
- Create a new google Web Application Project: File -> new -> Web Application Project (google), and named it like "gae-json-iphone"
- Add Spring Support.
Go Download the latest version of Spring 3 from http://www.springsource.org/download. For this tutorial we have downloaded spring-framework-3.0.0.RELEASE-with-docs.zip, which contains the documentation also. unzip it into a folder where you keep your java libs. Copy the following Jars into war\WEB-INF\llib directory:
1. org.springframework.asm-3.0.0.RELEASE.jar
2. org.springframework.beans-3.0.0.RELEASE.jar
3. org.springframework.context-3.0.0.RELEASE.jar
4. org.springframework.core-3.0.0.RELEASE.jar
5. org.springframework.expression-3.0.0.RELEASE.jar
6. org.springframework.oxm-3.0.0.RELEASE.jar
7. org.springframework.web-3.0.0.RELEASE.jar
8. org.springframework.web.servlet-3.0.0.RELEASE.jar :
Also grab your favorite version of log4j.jar and commons-logging-1.1.1.jar. Remember to rename commons-logging-1.1.1.jar to something like commons-logging-1.1.jar, google app engine replaces this jar with its own version with crippled packages, by providing different name we keep both versions and make spring happy. see more why . Once you copied the jars, open project preferences and add those jars to the build library path: - configuring spring.
The final version should look like this:<?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">
<context-param>
<param-name>log4jConfigLocation</param-name>
<param-value>/WEB-INF/log4j.properties</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
</listener>
<servlet>
<servlet-name>rest-json</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>rest-json</servlet-name>
<url-pattern>/api/*</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
- Add JSON support. -- Jackson JSON.Grab the library from here and put jackson-core-1.4.jar and jackson-mapper-1.4.jar file into projects WEB-INF/lib directory
- Configure Spring
To configure Spring servlet we need to create servletname-servlet.xml file for spring bean configuration. Our file has to be named rest-json-servlet.xml.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<!--
tell String to scan “com.zpylxapp.iphone” package for any bean
annotations
-->
<context:component-scan base-package="com.zpylxapp.iphone" />
<!--
define message converter to convert messages sent to the server into
beans using MappingJacksonHttpMessageConverter
-->
<bean
class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="jsonHttpMessageConverter" />
</list>
</property>
</bean>
<bean id="jsonHttpMessageConverter"
class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter" />
<!--
tells spring to render messages sent from the server to the client
using JsonView
-->
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="json" value="application/json" />
</map>
</property>
<property name="defaultContentType" value="application/json" />
<property name="defaultViews">
<list>
<bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
</list>
</property>
</bean>
</beans>
Part Two: Using JPA with App Engine. GAE support both JDO and JPA. I use JPA here. see detail of GAE JPA here.
- Creating the persistence.xml File. The JPA interface needs a configuration file named
persistence.xml
. You can create this file in /src/META-INF/persistence.xml
:
<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">
<persistence-unit name="transactions-optional">
<provider>org.datanucleus.store.appengine.jpa.DatastorePersistenceProvider</provider>
<properties>
<property name="datanucleus.NontransactionalRead" value="true"/>
<property name="datanucleus.NontransactionalWrite" value="true"/>
<property name="datanucleus.ConnectionURL" value="appengine"/>
</properties>
</persistence-unit>
</persistence>
- Getting an EntityManager Instance.
An app interacts with JPA using an instance of the EntityManager class. You get this instance by instantiating and calling a method on an instance of the EntityManagerFactory class. The factory uses the JPA configuration (identified by the name
"transactions-optional"
) to create EntityManager instances.
Because an EntityManagerFactory instance takes time to initialize, it's a good idea to reuse a single instance as much as possible. An easy way to do this is to create a singleton wrapper class with a static instance, as follows:
EMF.java
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
public final class EMF {
private static final EntityManagerFactory emfInstance =
Persistence.createEntityManagerFactory("transactions-optional");
private EMF() {}
public static EntityManagerFactory get() {
return emfInstance;
}
}
- User bean to support permissions.
package com.zpylxapp.iphone;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String name;
private String password;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setPassword(String password) {
this.password = password;
}
public String getPassword() {
return password;
}
}
- Implement Login Controller. (for POST request “/login”)
package com.zpylxapp.iphone;
import java.io.IOException;
import java.util.List;
import javax.persistence.EntityManager;
import javax.servlet.http.HttpServletRequest;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import com.zpylxapp.jpa.*;
@Controller
@RequestMapping("/login")
public class LoginController {
private static Logger logger = Logger.getLogger( LoginController.class );
@SuppressWarnings("unchecked")
@ModelAttribute("user")
@RequestMapping(value = "/", method = RequestMethod.POST)
public User login( @RequestBody User user, HttpServletRequest request ) throws IOException {
logger.debug(user);
// Hardcoded for demo website
if( user.getName().equalsIgnoreCase("user1") && user.getPassword().equals("password1") ) {
user.setId(1001);
user.setEmail("fake@email.com");
//set user session to check if user login or not.Even it is RESTful, we still need "Session"
request.getSession(true).setAttribute( Constants.SESSION_USER , user);
return user;
}
EntityManager em = EMF.get().createEntityManager();
List<User> users;
try {
javax.persistence.Query query = em.createQuery("select p from User p ");
users = (List<User>)query.getResultList();
if(users.size()>0)
return users.get(0);
} finally {
em.close();
}
return null;
}
}
- Add a preHandle interceptor, to check only if the user is present in the session. If no user is present and user is not trying to log in, send back an error. lets code our interceptor. I named it SessionInerceptor:
package com.zpylxapp.iphone;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
public class SessionInterceptor extends HandlerInterceptorAdapter {
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
User user = (User) request.getSession().getAttribute(
Constants.SESSION_USER);
String uri = request.getRequestURI();
String base = request.getContextPath();
uri = uri.substring(base.length());
if (user == null && !uri.startsWith("/api/login")) {
request.getSession().invalidate();
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"SESSION-EXPIRED\"}");
return false;
}
return true;
}
}
Now lets add this interceptor to the Spring MVC stack by adding these 5 lines to our rest-json-flex-servlet.xml file.
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<bean class="com.lureto.rjf.SessionInterceptor"/>
</property>
</bean>
One more thing – we need to enable session in google app engine. Add this line to your appengine-web.xml.
<sessions-enabled>true</sessions-enabled>
sessions are backed up by the datastore, you should be able to see the session data using the Data Viewer in the Admin Console.
work on java part is already done. Let us move to Iphone client.
Part Three: Using JPA with App Engine.
- To use JSON in Iphone, you need third part library like json-framework. Download JSON_2.2.2.dmg and open it, then drag the JSON directory into your project.
- Create a view based iphone project, add button and textfield as following:
- Handler to the “login” button is as below. which will get user/password from user input and encapsulate them in a JSON post request. If the user is invalid, give an alert window. If it is valid, the response is a NSDictionary type. It will be display in another TableView based page.
-(IBAction)sendLogin:(id)sender{
//prepare json data
NSString *username = tfUsername.text;
NSString *password = tfPassword.text;
NSDictionary *data = [[NSDictionary alloc] initWithObjectsAndKeys:
username, USERNAMEKEY,
password, PASSWORDKEY,
nil];
SBJsonWriter *json = [SBJsonWriter alloc];
NSString *jsonString = [json stringWithObject:data];
NSLog(jsonString);
[data release];
[json release];
//create/send http post request
NSData *postData = [jsonString dataUsingEncoding:NSASCIIStringEncoding];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:[NSURL URLWithString:@"http://zpylxrealtors.appspot.com/api/login/"]];
[request setHTTPMethod:@"POST"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setHTTPBody:postData];
NSURLResponse *urlResponse;
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:nil];
[request release];
NSString *jsonData = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
NSLog(jsonData);
//parse response json data
// Create a dictionary from the JSON string
@try {
NSDictionary *response = [jsonData JSONValue];
id user = [response objectForKey:@"user"];
if(user != nil && [user isKindOfClass:[NSDictionary class]]){
//go to next view
UserListViewController *nextview = [[UserListViewController alloc] initWithStyle:UITableViewStyleGrouped];
nextview.user = user;
[self presentModalViewController:nextview animated:YES];
[nextview release];
}
else{
//alert
UIAlertView *myAlert = [[UIAlertView alloc] initWithTitle:@"Error"
message:@"invalid user/password!" delegate:nil
cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
[myAlert show];
[myAlert release];
}
}
@catch (NSException * e) {
//nothing to do here, just provent to crash when google location search does not work
NSLog(@"NSException: ....");
}
@finally {
[jsonData release];
}
}
- delegation code for the table view:
@interface UserListViewController : UITableViewController {
NSDictionary *user;
}
@property(nonatomic, retain) NSDictionary *user;
……………
#pragma mark Table view methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
// Customize the number of rows in the table view.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 3;
}
// Customize the appearance of table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:CellIdentifier] autorelease];
}
// Set up the cell...
if(indexPath.row == 0){
cell.textLabel.text = @"name";
cell.detailTextLabel.text = [user objectForKey:@"name"];
}
else if(indexPath.row == 1){
cell.textLabel.text = @"id";
cell.detailTextLabel.text = [NSString stringWithFormat:@"%d", [user objectForKey:@"id"]];
}
else{
cell.textLabel.text = @"email";
cell.detailTextLabel.text = [user objectForKey:@"email"];
}
return cell;
}
- result page: