/* 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.HashMap; import java.util.Map; import com.google.appengine.demos.sticky.client.model.Model.StatusObserver; import com.google.appengine.demos.sticky.client.model.Service.AccessDeniedException; import com.google.appengine.demos.sticky.client.model.Service.GetNotesResult; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; /** * Controls all aspects of loading the set of {@link Note}s associated with the * selected {@link Surface}. This class takes care of performing (and possibly * retrying) a query for the initial set of Notes and then continues polling the * server for updates. * * @author knorton@google.com (Kelly Norton) */ class NoteLoader { /** * Controls the initial load of notes from the server and will retry on * failure. */ private class InitialLoader extends RetryTimer implements AsyncCallback<Service.GetNotesResult> { private final int id; public InitialLoader() { id = activeId; start(); } public void onFailure(Throwable caught) { model.onServerFailed(caught instanceof Service.AccessDeniedException); if (isActiveSession(id)) { retryLater(); } } public void onSuccess(GetNotesResult result) { model.onServerSucceeded(); if (!isActiveSession(id)) { return; } model.getStatusObserver().onTaskFinished(); timestamp = result.getTimestamp(); final Note[] notes = result.getNotes(); for (int i = 0, n = notes.length; i < n; ++i) { final Note note = notes[i]; note.initialize(model); notesCache.put(note.getKey(), note); } model.notifySurfaceNotesReceived(notes); startPolling(); } private void start() { model.getService().getNotes(model.getSelectedSurface().getKey(), null, this); } @Override protected void retry() { if (isActiveSession(id)) { start(); } } } /** * Controls the polling calls to the server. */ private class Poller extends Timer implements AsyncCallback<Service.GetNotesResult> { private final int id; public Poller() { this.id = activeId; scheduleRepeating(interval); } public void onFailure(Throwable caught) { model.onServerFailed(caught instanceof AccessDeniedException); if (!isActiveSession(id)) { cancel(); } } public void onSuccess(GetNotesResult result) { model.onServerSucceeded(); if (!isActiveSession(id)) { cancel(); return; } timestamp = result.getTimestamp(); final Note[] notes = result.getNotes(); for (int i = 0, n = notes.length; i < n; ++i) { final Note note = notes[i]; final Note existing = notesCache.get(note.getKey()); // If note does not exist, send a noted created notification. // Otherwise, update the note and allow it to notify its observer that // it changed. if (existing == null) { note.initialize(model); notesCache.put(note.getKey(), note); model.notifyNoteCreated(note); } else { existing.update(note); } } } @Override public void run() { if (!isActiveSession(id)) { cancel(); return; } final Surface surface = model.getSelectedSurface(); if (surface.hasKey()) { model.getService().getNotes(model.getSelectedSurface().getKey(), timestamp, this); } } } private final Model model; private final int interval; private String timestamp; private int activeId; private Map<String, Note> notesCache = new HashMap<String, Note>(); /** * Creates a new loader that is bound to the given model. * * @param model the model to which this loader is bound * @param interval the time to wait between polls to the server */ public NoteLoader(Model model, int interval) { this.model = model; this.interval = interval; } /** * Add a note to the loading cache so the {@link Model} can properly manage * duplicate objects. * * @param key * the notes key * @param note * the note */ public void cacheNote(String key, Note note) { assert key != null; notesCache.put(key, note); } /** * Forces the loader to cancel the active poller, reload the initial load and * start restart polling. This is called by the {@link Model} when a new * surface is selected. */ public void reset() { activeId++; notesCache.clear(); timestamp = null; final StatusObserver statusObserver = model.getStatusObserver(); statusObserver.onTaskStarted("Loading '" + model.getSelectedSurface().getTitle() + "' ..."); // If the surface has not been saved, we know there are no notes // associated with it, so it is safe to just synthesize an event on the // client. if (model.getSelectedSurface().hasKey()) { startInitialLoad(); } else { statusObserver.onTaskFinished(); model.notifySurfaceNotesReceived(new Note[0]); startPolling(); } } private boolean isActiveSession(int id) { return id == activeId; } private void startInitialLoad() { new InitialLoader(); } private void startPolling() { new Poller(); } }