/* 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()); } }