Version 1.7
Copyright © 2009 - 2010 Lars Vogel
02.02.2010
| Revision History | ||
|---|---|---|
| Revision 0.1 | 08.04.2009 | Lars Vogel |
| Created | ||
| Revision 0.2 - 1.5 | 25.04.2009- 23.04.2010 | Lars Vogel |
| bug fixes and enhancements | ||
| Revision 1.6 | 26.07.2010 | Lars Vogel |
| Update to Eclipse 3.6 | ||
| Revision 1.7 | 02.02.2010 | Lars Vogel |
| bug fixes and enhancements | ||
Table of Contents
Google offers a cloud computing infrastructure for creating and running web applications which is called Google App Engine (GAE) . GAE allows the dynamic allocation of system resources for an application based on the actual demand. Currently Google supports as Python and Java based applications. This includes Java Virtual Machine (JVM) based languages, e.g. Groovy. GAE for Java is often called GAE/J, the following article will use the term "AppEngine" synonymously.
The Google AppEngine allows to use standard Java API and AppEngine specific API's. If you want to be able to port your application from the AppEngine to a standard Java webcontainer, e.g. Tomcat or Jetty, you should only use Java standard API. applications are based on the servlet standard and allow you to use standard Java API.
AppEngine does not provide all Java classes, for example Swing and AWT are not supported and you cannot use Threads. Google provides a full list of supported classes on the Java Whitelist .
The Java AppEngine uses the Jetty servlet container to serve applications and supports the Java Servlet API in version 2.4. It provides access to databases via Java Data Objects (JDO) and the Java Persistence API (JPA) . In the background Appengine uses Google Bigtable as the distributed storage system for persisting application data.
Google provides Memcache as a caching mechanism. Developers who want to code against the standard Java API can use the JCache implementation (based on JSR 107).
Google offers free hosting for websites which are not highly frequented, e.g. 5 Millions page views. The price model for the websites that exceed thier daily quota is listed on the Google billing documentation pages . The usage quotas of the App Engine are constantly changing but, at the time of this writing, are around 5 millions pages views per month, which translates approx. in 6.5 CPU hours and 1 gigabyte of outbound traffic.
Currently a user can create a maximum of 10 applications on the Google App Engine. The user can delete existing application in the Admin Console under Application Settings.
The SDK for Eclipse is great but I believe a few things could be improved. Here are my favorite bugs. Please stare at them / vote for them to help them get solved.
Javadoc missing in SDK for some classesUnfortunately the Google Plugin does not allow to created a new Web Application Project without creating template files. The template are annoying if you want to start from scratch with a new application. Please stare / vote for bug Support new projects without creation of Servlet to get this solved.
Google offers a Eclipse plugin that provides both Google App Engine and GWT development capabilities. Install the all plugins from http://dl.google.com/eclipse/plugin/3.6 via the Eclipse update manager .
The installation will install the Google App Engine Server to your Eclipse plugin directory into "plugins/com.google.appengine.eclipse.sdkbundle_VERSION/". In this directory you find another directory "appengine-java-sdk_VERSION" which contains the server a in directory bin and several demos. To start the server with the guestbook demo simply type the following on the command line in the bin directory.
dev_appserver.cmd ..\demos\guestbook\war
To stop the server use "Cntr+C".
To deploy your application to the Google cloud you need a AppEngine account. To get such an account you need a Google email account. Open the URL http://appengine.google.com/ and login with your Google account information.
The first time you need then to verify your account. After providing your phone number, Google will text you a verification code via SMS. You will then type the verification code into the verification box online.
To create an application press the button "Create an application" and select an application name. You have to choose one which is still available.
The following will create a small "Todo" application using servlets and JSP's. For an introduction into servlet and JSP programming please see Servlet and JSP development - Tutorial

Create a new project "de.vogella.gae.java.todo" via File > New > Web Application Project. Package name is "de.vogella.gae.java.todo".

The created project can already run. Right click on your application, select run as-> Web Application

This should start Jetty on http://localhost:8888/ . Open the url in the browser and you should the possibility to select your servlet and to start it. If you select it you should see a "Hello, world" message.

Create the following class which will be our model.
package de.vogella.gae.java.todo.model;
import java.util.Calendar;
/**
* Model class which will store the Todo Items
*
* @author Lars Vogel
*
*/
public class Todo {
private final long id;
private String author;
private String shortDescription;
private String longDescription;
private String url;
private Calendar dueDate;
boolean finished;
public Todo(long id, String author, String shortDescription, String longDescription, String url, Calendar dueDate) {
this.id= id;
this.author = author;
this.shortDescription = shortDescription;
this.longDescription = longDescription;
this.url = url;
this.dueDate = dueDate;
finished = false;
}
public long getId() {
return id;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getLongDescription() {
return longDescription;
}
public void setLongDescription(String longDescription) {
this.longDescription = longDescription;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public Calendar getDueDate() {
return dueDate;
}
public void setDueDate(Calendar dueDate) {
this.dueDate = dueDate;
}
public boolean isFinished() {
return finished;
}
public void setFinished(boolean finished) {
this.finished = finished;
}
}
Create the following class which will serve as a content provider. This class is a Singleton which stores the data in memory.
package de.vogella.gae.java.todo.dao;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import org.datanucleus.sco.simple.GregorianCalendar;
import de.vogella.gae.java.todo.model.Todo;
public enum Dao {
INSTANCE;
private static long number=1;
private final List<Todo> todos = new ArrayList<Todo>();
private Dao(){
// Create a test Todo
// Image that I read the data from bigtables
Todo todo = new Todo(1, "vogella", "Issue number 1", "Detailed Description of everything", "", GregorianCalendar.getInstance());
todos.add(todo);
}
public void add(String author, String summery, String description, String url, Calendar dueDate){
synchronized (this) {
number ++;
todos.add(new Todo(number, author, summery, description, url, dueDate));
}
}
public List<Todo> getTodos(@SuppressWarnings("unused") String user){
// For testing we will always return the same data no matter what the user is
return todos;
}
public void remove(long id) {
Todo remove = null;
for (Todo todo: todos){
if (todo.getId() == id){
remove = todo;
}
}
if (remove!=null){
todos.remove(remove);
}
}
}
Create the following two servlets. The first will be called if a new Todo is created the second one if a Todo is finished.
package de.vogella.gae.java.todo;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.datanucleus.sco.simple.GregorianCalendar;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import de.vogella.gae.java.todo.dao.Dao;
@SuppressWarnings("serial")
public class ServletCreateTodo extends HttpServlet {
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
String summery = checkNull(req.getParameter("summary"));
String longDescription = checkNull(req.getParameter("description"));
String url = checkNull(req.getParameter("url"));
// Not yet used
String dueDate = req.getParameter("dueDate");
Dao.INSTANCE.add(user.getNickname(), summery, longDescription, url,
GregorianCalendar.getInstance());
resp.sendRedirect("/TodoApplication.jsp");
}
private String checkNull(String s) {
if (s == null) {
return "";
}
return s;
}
}
package de.vogella.gae.java.todo;
import java.io.IOException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import de.vogella.gae.java.todo.dao.Dao;
public class ServletRemoveTodo extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
String id = req.getParameter("id");
Dao.INSTANCE.remove(Long.parseLong(id));
resp.sendRedirect("/TodoApplication.jsp");
}
}
In the folder "war" create the following JSP "TodoApplication.jsp".
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="javax.jdo.PersistenceManager" %>
<%@ page import="javax.jdo.Query" %>
<%@ page import="com.google.appengine.api.users.User" %>
<%@ page import="com.google.appengine.api.users.UserService" %>
<%@ page import="com.google.appengine.api.users.UserServiceFactory" %>
<%@ page import="de.vogella.gae.java.todo.model.Todo" %>
<%@ page import="de.vogella.gae.java.todo.dao.Dao" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<%@page import="java.util.ArrayList"%>
<html>
<head>
<title>Todos</title>
<link rel="stylesheet" type="text/css" href="css/main.css"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"></meta>
</head>
<body>
<%
Dao dao = Dao.INSTANCE;
UserService userService = UserServiceFactory.getUserService();
User user = userService.getCurrentUser();
String url = userService.createLoginURL(request.getRequestURI());
String urlLinktext = "Login";
List<Todo> todos = new ArrayList<Todo>();
if (user != null){
url = userService.createLogoutURL(request.getRequestURI());
urlLinktext = "Logout";
todos = dao.getTodos(user.getNickname());
}
%>
<div style="width: 100%;">
<div class="line"></div>
<div class="topLine">
<div style="float: left;"><img src="images/todo.png" /></div>
<div style="float: left;" class="headline">Todos</div>
<div style="float: right;"><a href="<%=url%>"><%=urlLinktext%></a> <%=(user==null? "" : user.getNickname())%></div>
</div>
</div>
<div style="clear: both;"/>
You have a total number of <%= todos.size() %> Todos.
<table>
<tr>
<th>Short description </th>
<th>Due Date</th>
<th>Long Description</th>
<th>URL</th>
<th>Done</th>
</tr>
<% for (Todo todo : todos) {%>
<tr>
<td>
<%=todo.getShortDescription()%>
</td>
<td>
2009.11.10
</td>
<td>
<%=todo.getLongDescription()%>
</td>
<td>
<%=todo.getUrl()%>
</td>
<td>
<a class="done" href="/done?id=<%=todo.getId()%>" >Done</a>
</td>
</tr>
<%}
%>
</table>
<hr />
<div class="main">
<div class="headline">New todo</div>
<% if (user != null){ %>
<form action="/new" method="post" accept-charset="utf-8">
<table>
<tr>
<td><label for="summery">Summary</label></td>
<td><input type="text" name="summery" id="summery" size="65"/></td>
</tr>
<tr>
<td><label for="dueDate">Due Date</label></td>
<td><input type="text" name="dueDate" id="dueDate"/></td>
</tr>
<tr>
<td valign="description"><label for="description">Description</label></td>
<td><textarea rows="4" cols="50" name="description" id="description"></textarea></td>
</tr>
<tr>
<td valign="top"><label for="url">URL</label></td>
<td><input type="text" name="url" id="url" size="65" /></td>
</tr>
<tr>
<td colspan="2" align="right"><input type="submit" value="Create"/></td>
</tr>
</table>
</form>
<% }else{ %>
Please login with your Google account
<% } %>
</div>
</body>
</html>
The JSP refers to a css file. Create a folder "war/css" and create the following file "main.css".
body {
margin: 5px;
font-family: Arial, Helvetica, sans-serif;
}
hr {
border: 0;
background-color: #DDDDDD;
height: 1px;
margin: 5px;
}
table th {
background:#EFEFEF none repeat scroll 0 0;
border-top:1px solid #CCCCCC;
font-size:small;
padding-left:5px;
padding-right:4px;
padding-top:4px;
vertical-align:top;
text-align:left;
}
table tr {
background-color: #e5ecf9;
font-size:small;
}
.topLine {
height: 1.25em;
background-color: #e5ecf9;
padding-left: 4px;
padding-right: 4px;
padding-bottom: 3px;
padding-top: 2px;
}
.headline {
font-weight: bold;
color: #3366cc;
}
.done {
font-size: x-small;
vertical-align: top;
}
.email {
font-size: x-small;
vertical-align: top;
}
form td {
font-size: smaller;
}
Change "web.xml" in folder /war/WEB-INF" to the following. It will create the right servlet mapping and will set the JSP as start page.
<?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"> <servlet> <servlet-name>CreateNewTodo</servlet-name> <servlet-class>de.vogella.gae.java.todo.ServletCreateTodo</servlet-class> </servlet> <servlet> <servlet-name>RemoveTodo</servlet-name> <servlet-class>de.vogella.gae.java.todo.ServletRemoveTodo</servlet-class> </servlet> <servlet-mapping> <servlet-name>RemoveTodo</servlet-name> <url-pattern>/done</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>CreateNewTodo</servlet-name> <url-pattern>/new</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>/TodoApplication.jsp</welcome-file> </welcome-file-list> </web-app>
You should be able to run your application locally, login and create and delete "Todos".

To deploy your application to the GAE you need to maintain your application ID in the file "web/WEB-INF/appengine-web.xml". In the tag "application" maintain the application you have created in GAE/J Registration . In my example I use the application "vogellajava".
<?xml version="1.0" encoding="utf-8"?> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <application>vogellajava</application> <version>1</version> <!-- Configure java.util.logging --> <system-properties> <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/> </system-properties> </appengine-web-app>
Now deploy your application to the Google cloud. The following assumes that you have already created your account.
Right-click on your project and select Google > App Engine Settings... from the context menu. Check that the application ID is as you maintained it in "appengine-web.xml".

Right-click on your project, Select Google -> Deploy to App Engine. You need to login with your Google account information.

After the upload completes you will find your application online under http://application-id.appspot.com, e.g. in my case http://vogellajava.appspot.com.
AppEngine allows your application to send and receive emails. We will use the receive email feature to be able to receive new Toods via email.
To receive emails in your application you have to declare that your application is allowed to use the mail service. Add the service "mail" to your "appengine-web.xml".
<?xml version="1.0" encoding="utf-8"?> <appengine-web-app xmlns="http://appengine.google.com/ns/1.0"> <application>vogellatodo</application> <version>1</version> <!-- Configure java.util.logging --> <system-properties> <property name="java.util.logging.config.file" value="WEB-INF/logging.properties"/> </system-properties> <inbound-services> <service>mail</service> </inbound-services> </appengine-web-app>

After defining that your application can receive email you need to define the email address for your application. This address follows the pattern: "user@domain". The domainis based on the applicationid, e.g. "applicationid.appspotmail.com". Please note that appspotmail.com is different from appspot.com , which we use in the domain name of our application. You can add use any user you want as long as you provide a mapping to a servlet in "web.xml".
Via the "web.xml" you define which servlet answers to which user. The url pattern must start with "/_ah/mail/" to tell AppEngine that this is a mapping for receiving emails. The url pattern can include wildcards.
We will define that one servlet receives the email for all users. Add the following servlet mappings to your "web.xml", make sure you replace the string "applicationid" with your real id. "
<?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"> <servlet> <servlet-name>CreateNewTodo</servlet-name> <servlet-class>de.vogella.gae.java.todo.ServletCreateTodo</servlet-class> </servlet> <servlet> <servlet-name>RemoveTodo</servlet-name> <servlet-class>de.vogella.gae.java.todo.ServletRemoveTodo</servlet-class> </servlet> <servlet-mapping> <servlet-name>RemoveTodo</servlet-name> <url-pattern>/done</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>CreateNewTodo</servlet-name> <url-pattern>/new</url-pattern> </servlet-mapping> <servlet> <servlet-name>Email</servlet-name> <servlet-class>de.vogella.gae.java.todo.EmailServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>Email</servlet-name> <url-pattern>/_ah/mail/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>TodoApplication.jsp</welcome-file> </welcome-file-list> </web-app>
Create the following servlet which will process your email.
package de.vogella.gae.java.todo;
import java.io.IOException;
import java.util.Properties;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.Session;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.datanucleus.sco.simple.GregorianCalendar;
import com.google.appengine.api.users.User;
import de.vogella.gae.java.todo.dao.Dao;
public class EmailServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
Properties props = new Properties();
Session email = Session.getDefaultInstance(props, null);
try {
MimeMessage message = new MimeMessage(email, req.getInputStream());
String summary = message.getSubject();
String description = getText(message);
Address[] addresses = message.getFrom();
User user = new User(addresses[0].toString(), "gmail.com");
Dao.INSTANCE.add(user.getNickname(), summary, description, "",
GregorianCalendar.getInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
private boolean textIsHtml = false;
/**
* Return the primary text content of the message.
*/
private String getText(Part p) throws
MessagingException, IOException {
if (p.isMimeType("text/*")) {
String s = (String)p.getContent();
textIsHtml = p.isMimeType("text/html");
return s;
}
if (p.isMimeType("multipart/alternative")) {
// prefer html text over plain text
Multipart mp = (Multipart)p.getContent();
String text = null;
for (int i = 0; i < mp.getCount(); i++) {
Part bp = mp.getBodyPart(i);
if (bp.isMimeType("text/plain")) {
if (text == null)
text = getText(bp);
continue;
} else if (bp.isMimeType("text/html")) {
String s = getText(bp);
if (s != null)
return s;
} else {
return getText(bp);
}
}
return text;
} else if (p.isMimeType("multipart/*")) {
Multipart mp = (Multipart)p.getContent();
for (int i = 0; i < mp.getCount(); i++) {
String s = getText(mp.getBodyPart(i));
if (s != null)
return s;
}
}
return null;
}
}
To test your email functionality start your application and open the admin console on "http://localhost:8888/_ah/admin/". Select Inbound Mail and maintain some test data. Make sure the To email address match the servlet mapping for the email address.
Deploy your application. Send an email to "whatever@applicationid.appspotmail.com". Login to your application. You should get a new Todo with the summary of the email header and the description of the email body.
You can define cron jobs in your application. Via a cron job on th AppEngine you can access a URL according to a time pattern which is defined similar to the cron jobs on Unix. You define your cron jobs in a file "cron.xml" in the "WEB-INF" folder of you application.
<?xml version="1.0" encoding="UTF-8"?>
<cronentries>
<cron>
<url>/count</url>
<description>Calls the webpage /count every minute</description>
<schedule>every 1 minutes</schedule>
</cron>
</cronentries>
This cron job will call the local website /count every minute.
You can check you cron jobs in the admin console.

In case of problems the GAE provides access to the log files.

You can also use standard Java Logging for creating custom logs on the GAE/J. See Java Logging Tutorial and Logging on the GAE/J .
You can also extend the admin console with own pages. Create the following entry in "appengine-web.xml".
<admin-console> <page name="Static List" url="/static.html" /> </admin-console>
We will just use a static HTML for the example. Create the following file in the "WEB-INF" folder.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>Integrated into the Admin Console</title>
</head>
<body>
<h1>Hello Admin Console!</h1>
<table>
<tr>
<td colspan="2" style="font-weight:bold;">This could be a dynamic site:</td>
</tr>
<tr>
<td>But it isn't.</td>
</tr>
</table>
</body>
</html>
If you redeploy your application you have another entry in your Admin console.

Thank you for practicing with this tutorial.
I maintain this tutorial in my private time. If you like the information please help me by using flattr or donating or by
|
Before posting questions, please see the vogella FAQ . If you have questions or find an error in this article please use the www.vogella.de Google Group . I have created a short list how to create good questions which might also help you. .
http://www.vogella.de/articles/GoogleAppEngine/article.html Google App Engine Development with Python