/* Copyright (c) 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.appengine.demos.sticky.server;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import javax.jdo.Transaction;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.memcache.MemcacheServiceFactory;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.demos.sticky.client.model.Author;
import com.google.appengine.demos.sticky.client.model.Note;
import com.google.appengine.demos.sticky.client.model.Service;
import com.google.appengine.demos.sticky.client.model.Surface;
import com.google.gwt.user.server.rpc.RemoteServiceServlet;
/**
* The server-side RPC endpoint for {@link Service}.
*
* @author knorton@google.com (Kelly Norton)
*
*/
@SuppressWarnings("serial")
public class ServiceImpl extends RemoteServiceServlet implements Service {
private static final int TIMESTAMP_PADDING = 1000 * 60;
private static Date convertTimestampToDate(String timetamp) {
// To ensure we don't miss any events due to clock differences, we will
// expand the time window by 1 minute.
return new Date(Long.parseLong(timetamp, 16) - TIMESTAMP_PADDING);
}
private static String createTimestamp() {
// The client should never need to do math on the timestamp and returning a
// long value into GWT just complicates the client side code since GWT will
// load code to emulate longs. To simplify things, we return a hex encoded
// string.
return Long.toString(System.currentTimeMillis(), 16);
}
private static Note[] getNotesSinceTimestamp(Note[] notes, String timestamp) {
if (timestamp == null) {
return notes;
} else {
// Get the actual date + padding represented by the timestamp.
final Date since = convertTimestampToDate(timestamp);
final List<Note> newNotes = new ArrayList<Note>(notes.length);
// Return only those notes that were updated after since.
for (Note note : notes) {
if (note.getLastUpdatedAt().after(since)) {
newNotes.add(note);
}
}
return newNotes.toArray(new Note[newNotes.size()]);
}
}
private static String getSurfaceKey(Store.Note note) {
// Use the fact that surface and note have parent/child keys to very quickly
// determine the surface key for a particular note.
return KeyFactory.keyToString(note.getKey().getParent());
}
private static Note[] toClientNotes(Collection<Store.Note> notes) {
final Note[] clients = new Note[notes.size()];
int i = 0;
for (Store.Note n : notes) {
clients[i++] = new Note(KeyFactory.keyToString(n.getKey()), n.getX(), n
.getY(), n.getWidth(), n.getHeight(), n.getContent(), n
.getLastUpdatedAt(), n.getAuthorName(), n.getAuthorEmail());
}
return clients;
}
private static Surface toClientSurface(Store.Surface surface) {
final List<String> names = surface.getAuthorNames();
return new Surface(KeyFactory.keyToString(surface.getKey()), surface
.getTitle(), names.toArray(new String[names.size()]), surface
.getNotes().size(), surface.getLastUpdatedAt());
}
/**
* A convenient way to get the current user and throw an exception if the user
* isn't logged in.
*
* @param userService
* the user service to use
* @return the current user
* @throws AccessDeniedException
*/
private static User tryGetCurrentUser(UserService userService)
throws AccessDeniedException {
if (!userService.isUserLoggedIn()) {
throw new Service.AccessDeniedException();
}
return userService.getCurrentUser();
}
/**
* A reference to the data store.
*/
private final Store store = new Store("transactions-optional");
/**
* A reference to a cache service.
*/
private final Cache cache = new Cache(MemcacheServiceFactory
.getMemcacheService());
public AddAuthorToSurfaceResult addAuthorToSurface(final String surfaceKey,
final String email) throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
final Key key = KeyFactory.stringToKey(surfaceKey);
final Store.Author me = api.getOrCreateNewAuthor(user);
// Find an author with the given email address. If we can't find it, we'll
// return null to the client to indicate that the author does not exist
final Store.Author author = api.tryGetAuthor(email);
if (author == null) {
return null;
}
// Verify that author has access to the surface that is being changed.
if (!me.hasSurface(key)) {
throw new Service.AccessDeniedException();
}
final Store.Surface surface = api.getSurface(key);
// If the author already belongs to the surface, we return success without
// making any changes to the store.
if (!author.hasSurface(key)) {
cache.deleteSurfaceKeys(author.getEmail());
cache.deleteSurface(surface.getKey());
// Add the surface key to the author object. Since we'll be updating an
// object, carry out the operation in a transaction.
final Transaction txA = api.begin();
author.addSurface(surface);
api.saveAuthor(author);
txA.commit();
// Add the author name to the surface.
final Transaction txB = api.begin();
surface.addAuthorName(author.getName());
api.saveSurface(surface);
txB.commit();
}
return new AddAuthorToSurfaceResult(author.getName(), surface
.getLastUpdatedAt());
} finally {
api.close();
}
}
public Date changeNoteContent(final String noteKey, final String content)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
// Convert the string version of the key to an actual key.
final Key key = KeyFactory.stringToKey(noteKey);
final Store.Author me = api.getOrCreateNewAuthor(user);
// Start a transaction for the Note we're updating.
final Transaction tx = api.begin();
final Store.Note note = api.getNote(key);
// Verify that the author owns the Note.
if (!note.isOwnedBy(me)) {
throw new Service.AccessDeniedException();
}
note.setContent(content);
final Date result = api.saveNote(note).getLastUpdatedAt();
tx.commit();
// Invalidate the notes cache for the surface that owns this Note.
cache.deleteNotes(getSurfaceKey(note));
return result;
} finally {
api.close();
}
}
public Date changeNotePosition(final String noteKey, final int x,
final int y, final int width, final int height)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
// Convert the string version of the key into an actual key.
final Key key = KeyFactory.stringToKey(noteKey);
final Store.Author me = api.getOrCreateNewAuthor(user);
// Start a transaction for the Note we're updating.
final Transaction tx = api.begin();
final Store.Note note = api.getNote(key);
// Verify that the author owns the Note.
if (!note.isOwnedBy(me)) {
throw new Service.AccessDeniedException();
}
note.setX(x);
note.setY(y);
note.setWidth(width);
note.setHeight(height);
final Date result = api.saveNote(note).getLastUpdatedAt();
tx.commit();
// Invalidate the notes cache for the surface that owns this Note.
cache.deleteNotes(getSurfaceKey(note));
return result;
} finally {
api.close();
}
}
public CreateObjectResult createNote(final String surfaceKey, final int x,
final int y, final int width, final int height)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
// Convert the string version of the key to the actual Key.
final Key key = KeyFactory.stringToKey(surfaceKey);
final Store.Author me = api.getOrCreateNewAuthor(user);
// Verify that the author is actually a member of the surface.
if (!me.hasSurface(key)) {
throw new Service.AccessDeniedException();
}
// Start a transaction for the surface update.
final Transaction tx = api.begin();
final Store.Surface surface = api.getSurface(key);
final Store.Note note = new Store.Note(me, x, y, width, height);
surface.getNotes().add(note);
api.saveSurface(surface);
final CreateObjectResult result = new CreateObjectResult(KeyFactory
.keyToString(note.getKey()), note.getLastUpdatedAt());
tx.commit();
// Invalidate the cache for the surface.
cache.deleteNotes(surfaceKey);
return result;
} finally {
api.close();
}
}
public CreateObjectResult createSurface(final String title)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
final Store.Author me = api.getOrCreateNewAuthor(user);
final Store.Surface surface = new Store.Surface(title);
surface.addAuthorName(me.getName());
api.saveSurface(surface);
final Transaction tx = api.begin();
me.addSurface(surface);
api.saveAuthor(me);
tx.commit();
// Invalidate the cached surface keys for this author.
cache.deleteSurfaceKeys(me.getEmail());
return new CreateObjectResult(KeyFactory.keyToString(surface.getKey()),
surface.getLastUpdatedAt());
} finally {
api.close();
}
}
public GetNotesResult getNotes(String surfaceKey, String since)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
return new Service.GetNotesResult(createTimestamp(), getNotes(user,
surfaceKey, since));
}
public GetSurfacesResult getSurfaces(String timestamp)
throws AccessDeniedException {
final User user = tryGetCurrentUser(UserServiceFactory.getUserService());
final Store.Api api = store.getApi();
try {
// getSurfaceKeys will return a cached entry if possible.
final List<Key> keys = getSurfaceKeys(api, user);
final Surface[] surfaces = new Surface[keys.size()];
for (int i = 0, n = keys.size(); i < n; ++i) {
// getSurface will return a cached entry if possible.
surfaces[i] = getSurface(api, keys.get(i));
}
return new GetSurfacesResult(null, surfaces);
} finally {
api.close();
}
}
public UserInfoResult getUserInfo() throws AccessDeniedException {
final UserService userService = UserServiceFactory.getUserService();
final User user = tryGetCurrentUser(userService);
final Store.Api api = store.getApi();
try {
final Key surfaceKey = getSurfaceKeys(api, user).get(0);
final UserInfoResult result = new Service.UserInfoResult(new Author(user
.getEmail(), user.getNickname()), getSurface(api, surfaceKey),
userService.createLogoutURL(userService.createLoginURL("/")));
return result;
} finally {
api.close();
}
}
private Note[] getNotes(User user, String surfaceKey, String since)
throws AccessDeniedException {
final Store.Api api = store.getApi();
try {
// Attempt to load from cache.
final Note[] fromCache = cache.getNotes(user, surfaceKey);
if (fromCache != null) {
return getNotesSinceTimestamp(fromCache, since);
}
// Cache lookup failed, query the data store.
final Key key = KeyFactory.stringToKey(surfaceKey);
final Store.Author me = api.getOrCreateNewAuthor(user);
if (!me.hasSurface(key)) {
throw new Service.AccessDeniedException();
}
final Store.Surface surface = api.getSurface(key);
final Note[] notes = cache.putNotes(user, surfaceKey,
toClientNotes(surface.getNotes()));
return getNotesSinceTimestamp(notes, since);
} finally {
api.close();
}
}
private Surface getSurface(Store.Api api, Key key) {
// Attempt to load from cache.
final Surface fromCache = cache.getSurface(key);
if (fromCache != null) {
return fromCache;
}
// Cache lookup failed, query the data store.
return cache.putSurface(key, toClientSurface(api.getSurface(key)));
}
private List<Key> getSurfaceKeys(Store.Api api, User user) {
final String email = user.getEmail();
// Attempt to load from cache.
final List<Key> fromCache = cache.getSurfaceKeys(email);
if (fromCache != null) {
return fromCache;
}
// Cache lookup failed, query the data store.
final Store.Author author = api.getOrCreateNewAuthor(user);
return cache.putSurfaceKeys(email, author.getSurfaceKeys());
}
}