/* 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.client.model; import java.util.ArrayList; import java.util.Date; import java.util.List; import com.google.appengine.demos.sticky.client.model.Service.CreateObjectResult; import com.google.appengine.demos.sticky.client.model.Service.UserInfoResult; import com.google.gwt.core.client.GWT; import com.google.gwt.user.client.rpc.AsyncCallback; /** * Encapsulates the entire application data controller for the application. The * model controls all RPC to the server and is responsible for keeping client * side copies of data synchronized with the server. * * @author knorton@google.com (Kelly Norton) */ public class Model { /** * An observer interface to deliver all data change events. */ public interface DataObserver { /** * Called when a new {@link Note} is created. * * @param note * the note that was created */ void onNoteCreated(Note note); /** * Called when a new {@link Surface} is created. * * @param surface * the surface that was created */ void onSurfaceCreated(Surface surface); /** * Called when an initial list of {@link Note}s is returned from the server. * * @param notes * all the {@link Note}s on the currently selected {@link Surface}. */ void onSurfaceNotesReceived(Note[] notes); /** * Called when the selected {@link Surface} changes. * * @param nowSelected * the surface that is now selected * @param wasSelected * the surface that was previously selected */ void onSurfaceSelected(Surface nowSelected, Surface wasSelected); /** * Called when the initial list of {@link Surface}s is returned from the * server. * * @param surfaces * all the surfaces where the current author is a member */ void onSurfacesReceived(Surface[] surfaces); } /** * An observer interface used to get callbacks during the initial load of the * {@link Model}. */ public interface LoadObserver { /** * Invoked when the {@link Model} loads successfully. * * @param model * the newly loaded model */ void onModelLoaded(Model model); /** * Invoked when the model fails to load. */ void onModelLoadFailed(); } /** * An observer interface that provides callbacks useful for giving the user * feedback about calls to the server. */ public interface StatusObserver { /** * Invoked when RPC calls begin to succeed again after a failure was * reported. */ void onServerCameBack(); /** * Invoked when RPC calls to the server are failing. */ void onServerWentAway(); /** * Invoked when current task has finished. This is often used to stop * displaying status Ui that was made visible in the * {@link StatusObserver#onTaskStarted(String)} callback. */ void onTaskFinished(); /** * Invoked when a task that requires user feedback starts. * * @param description * a description of the task that is starting */ void onTaskStarted(String description); } /** * A simple callback for reporting success to the caller asynchronously. This * is used for call sites where the caller needs to know the result of an RPC. */ public static interface SuccessCallback { void onResponse(boolean success); } /** * A task that manages the call to the server to add an author to a surface. */ private class AddAuthorToSurfaceTask extends Task implements AsyncCallback<Service.AddAuthorToSurfaceResult> { private final Surface surface; private final String email; private final SuccessCallback callback; public AddAuthorToSurfaceTask(Surface surface, String email, SuccessCallback callback) { this.surface = surface; this.email = email; this.callback = callback; } public void onFailure(Throwable caught) { getQueue().taskFailed(this, caught instanceof Service.AccessDeniedException); } public void onSuccess(Service.AddAuthorToSurfaceResult result) { final boolean success = result != null; callback.onResponse(success); if (success) { surface.update(Model.this, result.getAuthorName(), result .getUpdatedAt()); } getQueue().taskSucceeded(this); } @Override void execute() { api.addAuthorToSurface(surface.getKey(), email, this); } } /** * A task that manages the call to the server to create a new note. */ private class CreateNoteTask extends Task implements AsyncCallback<CreateObjectResult> { private final Note note; private final Surface surface; public CreateNoteTask(Surface surface, Note note) { this.note = note; this.surface = surface; } @Override public void execute() { api.createNote(surface.getKey(), note.getX(), note.getY(), note .getWidth(), note.getHeight(), this); } public void onFailure(Throwable caught) { getQueue().taskFailed(this, caught instanceof Service.AccessDeniedException); } public void onSuccess(CreateObjectResult result) { if (surface == selectedSurface) { noteLoader.cacheNote(result.getKey(), note); } note.update(result.getKey(), result.getUpdateTime()); getQueue().taskSucceeded(this); } } /** * A task to manages the call to the server to create a new surface. */ private class CreateSurfaceTask extends Task implements AsyncCallback<Service.CreateObjectResult> { private final Surface surface; public CreateSurfaceTask(Surface surface) { this.surface = surface; } public void execute() { api.createSurface(surface.getTitle(), this); } public void onFailure(Throwable caught) { getQueue().taskFailed(this, caught instanceof Service.AccessDeniedException); } public void onSuccess(Service.CreateObjectResult result) { surfaceLoader.cacheSurface(result.getKey(), surface); surface.update(result.getKey(), result.getUpdateTime()); getQueue().taskSucceeded(this); } } /** * Encapsulates a linked list node that is used by {@link TaskQueue} to keep * an ordered list of pending {@link Task}s. */ private static class Node { private final Task task; private Node next; Node(Task task) { this.task = task; } void execute(TaskQueue queue) { task.execute(queue); } } /** * Encapsulates a task for writing data to the server. The tasks are managed * by the {@link TaskQueue} and are auto-retried on failure. */ private abstract static class Task { private TaskQueue queue; abstract void execute(); void execute(TaskQueue queue) { this.queue = queue; execute(); } TaskQueue getQueue() { return queue; } } /** * Provides a mechanism to perform write tasks sequentially and retry tasks * that fail. */ private class TaskQueue extends RetryTimer { private Node head, tail; public void post(Task task) { final Node node = new Node(task); if (isIdle()) { head = tail = node; executeHead(); } else { enqueueTail(node); } } private void enqueueTail(Node node) { assert head != null && tail != null; assert node != null; tail = tail.next = node; } private void executeHead() { head.execute(this); } private void executeNext() { head = head.next; if (head != null) { executeHead(); } else { tail = null; } } private boolean isIdle() { return head == null; } private void taskFailed(Task task, boolean fatal) { assert task == head.task; // Report a failure to the Model. onServerFailed(fatal); // Schedule a retry. retryLater(); } private void taskSucceeded(Task task) { assert task == head.task; // Report a success to the Model. onServerSucceeded(); // Reset the retry counter. resetRetryCount(); // Move on to the next task. executeNext(); } @Override protected void retry() { // Retry running the head task. executeHead(); } } /** * A {@link Task} that manages the call to the server to update the contents * of a {@link Note}. */ private class UpdateNoteContentTask extends Task implements AsyncCallback<Date> { private final String content; private final Note note; public UpdateNoteContentTask(Note note, String content) { this.note = note; this.content = content; } public void execute() { note.setContent(content); api.changeNoteContent(note.getKey(), content, this); } public void onFailure(Throwable caught) { getQueue().taskFailed(this, caught instanceof Service.AccessDeniedException); } public void onSuccess(Date lastUpdatedAt) { note.update(lastUpdatedAt); getQueue().taskSucceeded(this); } } /** * A {@link Task} that manages the call to the server to update the position * of a {@link Note}. */ private class UpdateNotePositionTask extends Task implements AsyncCallback<Date> { private final Note note; private final int x, y, width, height; public UpdateNotePositionTask(Note note, int x, int y, int w, int h) { this.note = note; this.x = x; this.y = y; this.width = w; this.height = h; } public void execute() { note.setX(x); note.setY(y); note.setWidth(width); note.setHeight(height); api.changeNotePosition(note.getKey(), x, y, width, height, this); } public void onFailure(Throwable caught) { getQueue().taskFailed(this, caught instanceof Service.AccessDeniedException); } public void onSuccess(Date result) { note.update(result); getQueue().taskSucceeded(this); } } /** * The period to use, in millisconds, for polling for updates to notes on the * currently selected surface. */ private static final int GET_NOTES_POLLING_INTERVAL = 10000 /* ms. */; /** * The period to use, in milliseconds, for polling for updates to the list of * surfaces that the author is participating in. */ private static final int GET_SURFACES_POLLING_INTERVAL = 20000 /* ms. */; /** * Provides an asynchronous factory for loading a {@link Model}. * * @param loadObserver * a callback to receive load events * @param statusObserver * a callback to receive status events */ public static void load(final LoadObserver loadObserver, final StatusObserver statusObserver) { final ServiceAsync api = GWT.create(Service.class); api.getUserInfo(new AsyncCallback<Service.UserInfoResult>() { public void onFailure(Throwable caught) { loadObserver.onModelLoadFailed(); } public void onSuccess(UserInfoResult result) { loadObserver.onModelLoaded(new Model(result.getAuthor(), result .getSurface(), result.getLogoutUrl(), api, statusObserver)); } }); } native static void forceApplicationReload() /*-{ $wnd.location.reload(); }-*/; /** * An rpc proxy for making calls to the server. */ private final ServiceAsync api; /** * The currently selected surface. This should never be null. */ private Surface selectedSurface; /** * The currently logged in author. */ private final Author author; /** * The list of the observers monitoring the model for data related events. */ private final List<DataObserver> dataObservers = new ArrayList<DataObserver>(); /** * The observer that is receiving status events. */ private final StatusObserver statusObserver; /** * A url that can be used to log the current user out. */ private final String logoutUrl; /** * A task queue to manage all writes to the server. */ private final TaskQueue taskQueue = new TaskQueue(); /** * Manages the initial loading of notes associated with the selected surface * and polls repeatedly for changes. */ private final NoteLoader noteLoader = new NoteLoader(this, GET_NOTES_POLLING_INTERVAL); /** * Manages the initial loading of the list of surfaces for an author and * continues polling repeatedly for updates. */ private final SurfaceLoader surfaceLoader = new SurfaceLoader(this, GET_SURFACES_POLLING_INTERVAL); /** * Indicates whether the RPC end point is currently responding. */ private boolean offline; private Model(Author author, Surface selectedSurface, String logoutUrl, ServiceAsync api, StatusObserver statusObserver) { this.author = author; this.api = api; this.logoutUrl = logoutUrl; this.statusObserver = statusObserver; selectedSurface.initialize(this); selectSurface(selectedSurface); surfaceLoader.start(); } /** * Add an {@link Author} as a member of a particular {@link Surface} and * persist that change to the server. * * @param surface * the surface to which the author will be added * @param email * the email address of the person to add * @param callback * a callback to report success/failure to the caller */ public void addAuthorToSurface(Surface surface, String email, SuccessCallback callback) { taskQueue.post(new AddAuthorToSurfaceTask(surface, email, callback)); } /** * Subsscribes a {@link DataObserver} to receive data related events from this * {@link Model}. * * @param observer */ public void addDataObserver(DataObserver observer) { dataObservers.add(observer); } /** * Creates a note with no content at a particular location on the * {@link Surface} and persists that change to the server. * * @param x * @param y * @param width * @param height */ public void createNote(int x, int y, int width, int height) { final Note note = new Note(this, x, y, width, height); notifyNoteCreated(note); taskQueue.post(new CreateNoteTask(getSelectedSurface(), note)); } /** * Creates a {@link Surface} with the specified title and persists that change * to the server. * * @param title */ public void createSurface(String title) { final Surface surface = new Surface(this, title); notifySurfaceCreated(surface); taskQueue.post(new CreateSurfaceTask(surface)); selectSurface(surface); } /** * Gets the currently logged in author. * * @return */ public Author getCurrentAuthor() { return author; } /** * Gets a url that can be used to log out the current user. * * @return */ public String getLogoutUrl() { return logoutUrl; } /** * Gets the currently selected surface. * * @return */ public Surface getSelectedSurface() { return selectedSurface; } /** * Selects the specified {@link Surface}. The newly selected surface should be * made visible in the Ui. * * @param surface * the surface to display */ public void selectSurface(Surface surface) { if (surface == selectedSurface) { return; } final Surface wasSelected = selectedSurface; selectedSurface = surface; noteLoader.reset(); notifySurfaceSelected(surface, wasSelected); } /** * Updates the contents of a {@link Note} and persists the change to the * server. * * @param note * @param content */ public void updateNoteContent(final Note note, String content) { taskQueue.post(new UpdateNoteContentTask(note, content)); } /** * Updates the position of a {@link Note} and persists the change to the * server. * * @param note * @param x * @param y * @param width * @param height */ public void updateNotePosition(final Note note, int x, int y, int width, int height) { taskQueue.post(new UpdateNotePositionTask(note, x, y, width, height)); } private void notifySurfaceSelected(Surface nowSelected, Surface wasSelected) { for (int i = 0, n = dataObservers.size(); i < n; ++i) { dataObservers.get(i).onSurfaceSelected(nowSelected, wasSelected); } } ServiceAsync getService() { return api; } StatusObserver getStatusObserver() { return statusObserver; } void notifyNoteCreated(Note note) { for (int i = 0, n = dataObservers.size(); i < n; ++i) { dataObservers.get(i).onNoteCreated(note); } } void notifySurfaceCreated(Surface surface) { for (int i = 0, n = dataObservers.size(); i < n; ++i) { dataObservers.get(i).onSurfaceCreated(surface); } } void notifySurfaceNotesReceived(Note[] notes) { for (int i = 0, n = dataObservers.size(); i < n; ++i) { dataObservers.get(i).onSurfaceNotesReceived(notes); } } void notifySurfacesReceived(Surface[] surfaces) { for (int i = 0, n = dataObservers.size(); i < n; ++i) { dataObservers.get(i).onSurfacesReceived(surfaces); } } /** * Invoked by tasks and loaders when RPC invocations begin to fail. */ void onServerFailed(boolean fatal) { if (fatal) { forceApplicationReload(); return; } if (!offline) { statusObserver.onServerWentAway(); offline = true; } } /** * Invoked by tasks and loaders when RPC invocations succeed. */ void onServerSucceeded() { if (offline) { statusObserver.onServerCameBack(); offline = false; } } }