com.google.gwt.gears.sample.gwtnote.client.AppController.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.gears.sample.gwtnote.client.AppController.java

Source

/*
 * Copyright 2008 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.gwt.gears.sample.gwtnote.client;

import com.google.gwt.core.client.GWT;
import com.google.gwt.gears.sample.gwtnote.client.local.GearsHelper;
import com.google.gwt.gears.sample.gwtnote.client.rpc.Note;
import com.google.gwt.gears.sample.gwtnote.client.rpc.NoteService;
import com.google.gwt.gears.sample.gwtnote.client.rpc.NoteServiceAsync;
import com.google.gwt.gears.sample.gwtnote.client.ui.RichTextWidget;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import com.google.gwt.user.client.ui.ChangeListener;
import com.google.gwt.user.client.ui.Widget;

import java.util.HashMap;
import java.util.Iterator;

/**
 * A controller class that manages UI and data-synchronization events. This is
 * the core component of the GWT GearsNote application.
 */
public class AppController {

    protected static final String REPLACE_CONF_TEXT = "Someone has changed the note you are viewing. Keep your copy?";

    private static final int TICKS_PER_RPC = 100;

    protected boolean ready = false;

    protected boolean localDirty = false;

    private RichTextWidget rtw = null;

    private GearsHelper gears = null;

    private NoteServiceAsync rpc = null;

    private HashMap<String, Note> noteData = new HashMap<String, Note>();

    private int rpcCntdown = TICKS_PER_RPC;

    private Object lastData = "";

    /**
     * Default constructor.
     */
    public AppController() {
        // set up RPC handles
        rpc = GWT.create(NoteService.class);
        String rpcUrl = GWT.getModuleBaseURL() + "rpc";
        ((ServiceDefTarget) rpc).setServiceEntryPoint(rpcUrl);

        // create a database helper
        gears = new GearsHelper();
    }

    /**
     * Tells the controller which widget instance to use to monitor and update for
     * UI changes.
     * 
     * @param mainPanel
     *          the widget to use
     */
    public void setWidget(RichTextWidget mainPanel) {
        this.rtw = mainPanel;
        rtw.addNameChangeListener(new ChangeListener() {
            public void onChange(Widget sender) {
                String newName = rtw.getName();
                if (noteData.containsKey(newName)) {
                    Note n = noteData.get(newName);
                    if (!rtw.getHTML().equals(n.getText())) {
                        rtw.setHTML(n.getText());
                    }
                }
            }
        });
    }

    /**
     * Kicks off the main processing loop. That loop is a timer that fires every
     * 10 milliseconds, forever. It calls {@link #update()} which implements the
     * sync logic, and may or may not actually do anything for a give timer tick.
     * (For instance, it may be stalled waiting on an asynchronous RPC call to
     * return.)
     */
    public void startMainLoop() {
        init();

        Timer t = new Timer() {
            @Override
            public void run() {
                if (!ready) {
                    return;
                }
                update();
            }
        };
        t.scheduleRepeating(10);
    }

    /**
     * General initialization.
     */
    protected void init() {
        ready = false;
        noteData.clear(); // ditto
        syncFromGears(true);
        syncToServer(true);

        // init the dirty-testing code
        localDirty = false;
        Note def = noteData.get("default");
        if (def != null) {
            lastData = def.getText();
        }
    }

    /**
     * Fallback-mode initialization. This method is called when the attempt to
     * fetch data from the RPC server on startup fails; this fallback method
     * fetches similar data from the local database.
     * 
     * @param isInit
     *          instructs this method that the attempt to init over RPC previously
     *          failed, which causes this method to take additional actions to
     *          prevent data loss when the server comes back online
     */
    protected void syncFromGears(boolean isInit) {
        if (!gears.gearsEnabled()) {
            return;
        }

        Note[] notes = gears.getNotes();
        Note n = null;
        if (notes != null && notes.length > 0) {
            for (int i = 0; i < notes.length; ++i) {
                n = notes[i];
                if (!noteData.containsKey(n.getName())) {
                    n = notes[i];
                    noteData.put(n.getName(), n);
                }
            }
        }
        if (isInit) {
            n = noteData.get("default");
            if (n != null) {
                rtw.setHTML(n.getText());
            }
        }
    }

    /**
     * Synchronizes the local application's state with the server's data. The
     * <code>gearsFallback</code> parameter should generally only be set to true
     * during first-time initialization.
     * 
     * @param isInit
     *          if true, indicates that a failure to contact server should result
     *          in an attempt to sync from the Gears database
     */
    protected void syncFromServer(final boolean isInit) {
        rpc.getNotes(new AsyncCallback<Note[]>() {
            public void onFailure(Throwable caught) {
                ready = true;
            }

            public void onSuccess(Note[] notes) {
                if (notes == null) {
                    ready = true;
                    return;
                }

                // process the results and figure out if we need to update our state
                Note n = null;
                for (int i = 0; i < notes.length; ++i) {
                    n = notes[i];

                    // server sent us a totally new record -- just store it
                    if (!noteData.containsKey(n.getName())) {
                        noteData.put(n.getName(), n);
                        gears.updateNote(n);
                        continue;
                    }

                    // record exists -- check if server version is more recent & handle it
                    Note current = noteData.get(n.getName());
                    if (!current.getVersion().equals(n.getVersion())) {
                        current.setVersion(n.getVersion());
                        if (current.getText().equals(n.getText())) {
                            // versions don't match but text is same anyway
                            gears.updateNote(current); // to update version...
                            localDirty = false;
                            lastData = current.getText();
                        } else if (current.getName().equals(rtw.getName()) && localDirty
                                && Window.confirm(REPLACE_CONF_TEXT)) {
                            // if versions don't match, ask user for permission to override
                            gears.updateNote(current); // to update version...
                            // we are proceeding w/ local data, so don't touch UI
                        } else {
                            // user rejected override, or else was not current note
                            current.setText(n.getText());
                            gears.updateNote(current);

                            localDirty = false;
                            lastData = current.getText();

                            // don't forget to update UI state...
                            if (rtw.getName().equals(current.getName())) {
                                rtw.setHTML(current.getText());
                            }
                        }
                        continue;
                    }
                }

                // in the special case of startup, check for default value
                if (isInit) {
                    Note def = noteData.get("default");
                    if (def != null) {
                        rtw.setHTML(def.getText());
                    }
                }
                ready = true;
            }
        });
    }

    /**
     * Uploads the current set of dirty (modified) notes to the server, and upon
     * acknowledgment of that, fetches the server's set of data.
     */
    protected void syncToServer(final boolean isInit) {
        // temporarily stall the main timer loop until we're done
        ready = false;

        // convert our Map of notes into an array
        Note[] notes = new Note[noteData.size()];
        Iterator<Note> it = noteData.values().iterator();
        for (int i = 0; it.hasNext(); ++i) {
            notes[i] = it.next();
        }

        // upload our current data
        rpc.setNotes(notes, new AsyncCallback<Void>() {
            public void onFailure(Throwable caught) {
                // next call is also likely to fail, so don't bother to try
                if (isInit) {
                    Note def = noteData.get("default");
                    if (def != null) {
                        rtw.setHeight(def.getText());
                    }
                }
                ready = true;
            }

            public void onSuccess(Void result) {
                // it worked: now request server's data
                syncFromServer(isInit); // releases 'ready' later
            }
        });
    }

    /**
     * Core execution routine. Called once per timer tick.
     */
    protected void update() {
        syncFromGears(false);
        if (rpcCntdown == 0) {
            rpcCntdown = TICKS_PER_RPC;
            syncToServer(false);
        }
        rpcCntdown -= 1;
        updateUIState();
    }

    /**
     * Synchronizes the user interface state with the in-memory data model. Note
     * that this goes both ways: it updates the UI if new data is present, and
     * also updates the data if the user has entered or changed data.
     */
    protected void updateUIState() {
        // extract data from the UI
        String curName = rtw.getName();
        String curData = rtw.getHTML();
        curData = (curData == null) ? "" : curData;
        curName = (curName == null) ? "" : curName;

        if (!curData.equals(lastData)) {
            localDirty = true;
            lastData = curData;
        }

        if (noteData.containsKey(curName)) {
            // fetch the latest data for the note the user is trying to look at
            Note n = noteData.get(curName);
            if (!n.getText().equals(curData)) {
                // if the UI doesn't currently show latest data, update it
                n.setText(curData);
                gears.updateNote(n);
            }
        } else {
            // if the user has created a new record (unknown name) just store it
            Note n = new Note(curName, "1", curData);
            noteData.put(curName, n);
            gears.updateNote(n);
        }

        // add all the notes to the options list
        Iterator<String> it = noteData.keySet().iterator();
        String[] names = new String[noteData.size()];
        for (int i = 0; it.hasNext(); ++i) {
            names[i] = it.next();
        }
        rtw.setNameOptions(names);
    }
}