package com.google.gwt.gwtpages.client;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Stack;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.gwtpages.client.event.PageIllegalAccessEvent;
import com.google.gwt.gwtpages.client.event.PageNotFoundEvent;
import com.google.gwt.gwtpages.client.event.PageShownEvent;
import com.google.gwt.gwtpages.client.message.DefaultMessageHandler;
import com.google.gwt.gwtpages.client.page.ApplicationPresenter;
import com.google.gwt.gwtpages.client.page.AsyncPageCallback;
import com.google.gwt.gwtpages.client.page.DefaultPageLoadingHandler;
import com.google.gwt.gwtpages.client.page.LoadedPageContainer;
import com.google.gwt.gwtpages.client.page.Page;
import com.google.gwt.gwtpages.client.page.cache.PageCache;
import com.google.gwt.gwtpages.client.page.cache.SimplePageCache;
import com.google.gwt.gwtpages.client.page.event.PageErrorEventHandler;
import com.google.gwt.gwtpages.client.page.event.PageEventHandler;
import com.google.gwt.gwtpages.client.page.event.PageRequestEventHandler;
import com.google.gwt.gwtpages.client.page.loader.PageLoadCallback;
import com.google.gwt.gwtpages.client.page.loader.PageLoader;
import com.google.gwt.gwtpages.client.page.parameters.PageParameters;
import com.google.gwt.gwtpages.client.page.parameters.SimplePageParameters;
import com.google.gwt.gwtpages.client.page.parameters.SimpleTokenizer;
import com.google.gwt.gwtpages.client.page.parameters.Tokenizer;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.History;
/**
* Controller class used to move from 1 page to another within a GWT Pages
* application. When using this class, the
* {@link GWTPagesSettings#init(PagePresenter, PageLoader, com.google.gwt.event.shared.HandlerManager)}
* method must be called first. There are several different gotoPage methods
* which can be used depending on the context.
*
* When the application initially loads, you also usually want to call
* {@link Pages#showStartPage(boolean)}
*
* @author Joe Hudson
*/
public class Pages implements ValueChangeHandler<String> {
private static Pages instance;
private Settings settings = new Settings();
protected HandlerManager eventBus;
private boolean keepGoing = true;
private GotoPageCommand lastCommand;
private LoadedPageContainer currentPage;
// local cache
private Stack<GotoPageCommand> currentCommandStack = new Stack<GotoPageCommand>();
private Stack<String> currentPageTokenStack = new Stack<String>();
private static final int MAX_STACK_SIZE = 8;
public static Pages get() {
if (null == instance)
instance = new Pages();
return instance;
}
public static Pages init(PageLoader pageLoader,
ApplicationPresenter applicationPresenter, HandlerManager bus,
boolean monitorHistoryEvents) {
Pages pages = new Pages(pageLoader, applicationPresenter, bus,
monitorHistoryEvents);
return pages;
}
public Pages(PageLoader pageLoader,
ApplicationPresenter applicationPresenter, HandlerManager bus,
boolean monitorHistoryEvents) {
settings.pagePresenter = applicationPresenter;
settings.pageLoader = pageLoader;
eventBus = bus;
init();
if (monitorHistoryEvents)
History.addValueChangeHandler(this);
}
protected void init() {
if (updateStaticInstance()) {
instance = this;
}
settings.pageLoader.init(this);
settings.pagePresenter.init(this);
}
protected boolean updateStaticInstance () {
return true;
}
public Pages() {
if (null != instance) {
this.settings = instance.settings;
this.eventBus = instance.eventBus;
}
instance = this;
}
/**
* Return a nested pages instance that can be used within this pages instance
* @param presenter the nested application presenter
* @param bus the event bus
*/
public Pages nested(ApplicationPresenter presenter, HandlerManager bus) {
return nested(settings.getPageLoader(), presenter, bus);
}
/**
* Return a nested pages instance that can be used within this pages instance
* @param presenter the nested application presenter
*/
public Pages nested(ApplicationPresenter presenter) {
return nested(settings.getPageLoader(), presenter, new HandlerManager(null));
}
/**
* Return a nested pages instance that can be used within this pages instance
* @param pageLoader the page loader
* @param presenter the nested application presenter
* @param bus the event bus
*/
public Pages nested(PageLoader pageLoader, ApplicationPresenter presenter, HandlerManager bus) {
Pages pages = new Pages(pageLoader, presenter, bus, false) {
@Override
protected boolean updateStaticInstance() {
return false;
}
};
return pages;
}
/**
* Go to the start page.
*
* @param useHistoryToken
* true if the history token should be evaluated to obtain the
* page token and false to use the standard default page
*/
public void showStartPage(boolean useHistoryToken) {
String token = PageLoader.PAGE_DEFAULT;
if (useHistoryToken)
token = History.getToken();
if (null == token)
token = PageLoader.PAGE_DEFAULT;
onNewPage(token, true);
}
/**
* Return the {@link GotoPageCommand} used to navigate to the page
* represented by the page token
*
* @param pageToken
* the page token
*/
public GotoPageCommand goTo(String pageToken) {
return new GotoPageCommand(pageToken, this);
}
/**
* Return the {@link GotoPageCommand} used to navigate to the page
* represented by the page token
*
* @param pageToken
* the page token
* @param convienance
* method for using
* {@link GotoPageCommand#addParameter(Serializable)}
*/
public GotoPageCommand goTo(String pageToken, Serializable... params) {
GotoPageCommand cmd = new GotoPageCommand(pageToken, this);
for (Serializable param : params)
cmd.addParameter(param);
return cmd;
}
/**
* Return the {@link GotoPageCommand} used to navigate to the page
* represented by the page token
*
* @param pageToken
* the page token
* @param session
* a custom page request session
*/
public GotoPageCommand goTo(String pageToken, PageRequestSession session) {
return new GotoPageCommand(pageToken, session, this);
}
/**
* Return the {@link GotoPageCommand} used to navigate to the page
* represented by the page token
*
* @param pageToken
* the page token
* @param session
* a custom page request session
* @param convienance
* method for using
* {@link GotoPageCommand#addParameter(Serializable)}
*/
public GotoPageCommand goTo(String pageToken,
PageRequestSession session, Serializable... params) {
GotoPageCommand cmd = new GotoPageCommand(pageToken, session, this);
for (Serializable param : params)
cmd.addParameter(param);
return cmd;
}
/**
* Use {@link GotoPageCommand#execute()}
*/
void goTo(GotoPageCommand command) {
currentCommandStack.add(command);
sanityCheck();
keepGoing = true;
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageRequest(command.getPageToken(), null,
command.getSession());
}
String pageToken = command.getPageToken();
LoadedPageContainer pageData = settings.getPageCache().borrowPage(pageToken);
String historyToken = null;
PageParameters parameters = null;
if (null != command.getParameterList(false)) {
Serializable[] params = command.getParameterList(false).toArray(
new Serializable[command.getParameterList(false).size()]);
historyToken = createHistoryToken(pageToken, params);
parameters = new SimplePageParameters(pageToken, historyToken,
params, null);
} else if (null != command.getParameterMap(false)) {
historyToken = createHistoryToken(pageToken,
command.getParameterMap(false));
parameters = new SimplePageParameters(pageToken, historyToken,
null, command.getParameterMap(false));
} else {
historyToken = createHistoryToken(pageToken);
parameters = new SimplePageParameters(pageToken, historyToken,
null, null);
}
if (command.shouldAddHistoryToken())
History.newItem(historyToken, false);
if (null != pageData) {
goTo(pageData, parameters, command);
} else {
settings.getPageLoader().getPage(pageToken,
new _PageLoadCallback(parameters, command));
}
}
private void onNewPage(String historyToken, boolean addHistoryToken) {
currentPageTokenStack.add(historyToken);
// FIXME add sanity check here
keepGoing = true;
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageRequest(null, historyToken, null);
}
if (!keepGoing())
return;
String pageToken = null;
if (null == historyToken
|| historyToken.equals(PageLoader.PAGE_DEFAULT)) {
pageToken = PageLoader.PAGE_DEFAULT;
} else {
Iterator<String> possibleTokens = settings.getTokenizer()
.getPossiblePageTokens(historyToken);
while (possibleTokens.hasNext()) {
String token = possibleTokens.next();
if (settings.getPageLoader().isValidPageToken(token)) {
pageToken = token;
break;
}
}
}
if (null == pageToken) {
onPageNotFound(null, historyToken);
return;
}
PageParameters parameters = settings.getTokenizer().getPageParameters(
historyToken, pageToken);
LoadedPageContainer pageLoadResult = settings.getPageCache().borrowPage(pageToken);
if (null != pageLoadResult) {
// the page was cached
goTo(pageLoadResult, parameters, new GotoPageCommand(pageToken,
this).addHistoryToken(addHistoryToken));
} else {
// the page couldn't be found
settings.getPageLoader().getPage(
pageToken,
new _PageLoadCallback(parameters, new GotoPageCommand(
pageToken, this).addHistoryToken(addHistoryToken)));
}
}
private void sanityCheck() {
if (currentCommandStack.size() > MAX_STACK_SIZE || currentPageTokenStack.size() > MAX_STACK_SIZE) {
_forwardToPage(new UnhandledStackOverflowErrorPage());
stopRequest();
}
}
/**
* Internal check - should we continue with the page request?
*/
private boolean keepGoing() {
if (keepGoing)
return true;
else
return false;
}
/** Async page loading callbacks **/
private void onPageFound(LoadedPageContainer pageLoadResult,
PageParameters pageParameters, GotoPageCommand command) {
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageLoaded(pageLoadResult);
}
if (!keepGoing())
return;
settings.getPageCache().registerPage(pageLoadResult.getPageToken(), pageLoadResult);
goTo(pageLoadResult, pageParameters, command);
}
private void onPageNotFound(String pageToken, String historyToken) {
ArrayList<Command> commands = new ArrayList<Command>();
if (null == historyToken) historyToken = pageToken;
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
Command cmd = handler.onPageNotFound(historyToken);
if (null != cmd && !commands.contains(cmd))
commands.add(cmd);
}
eventBus.fireEvent(new PageNotFoundEvent(historyToken));
if (commands.size() == 0) {
if (settings.getPageLoader().isValidPageToken(PageLoader.PAGE_NOT_FOUND)) {
goTo(PageLoader.PAGE_NOT_FOUND);
}
else {
_forwardToPage(new UnhandledPageNotFoundPage(historyToken));
}
} else {
for (Command cmd : commands)
cmd.execute();
}
currentCommandStack.clear();
currentPageTokenStack.clear();
}
private void onPageLoadFailure(String pageToken, String historyToken,
Throwable cause) {
ArrayList<Command> commands = new ArrayList<Command>();
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
Command cmd = handler.onPageLoadFailure(historyToken, cause);
if (null != cmd && !commands.contains(cmd))
commands.add(cmd);
}
if (commands.size() == 0) {
_forwardToPage(new UnhandledPageLoadErrorPage(pageToken, cause));
} else {
for (Command cmd : commands)
cmd.execute();
}
currentCommandStack.clear();
currentPageTokenStack.clear();
}
private void goTo(LoadedPageContainer page, PageParameters parameters,
GotoPageCommand command) {
// do a sanity check here so we don't get caught in a loop...
try {
page.getPage().init(this);
}
catch (Throwable t) {
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
handler.onPageEnterFailure(page, parameters, command);
}
return;
}
command.getSession().setPageAttributes(page.getAttributes());
page = page.copy();
lastCommand = command;
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onBeforePageEnter(page, parameters, command);
}
if (!keepGoing())
return;
LoadedPageContainer previousPage = null;
try {
_AsyncPageCallback callback = new _AsyncPageCallback(page,
previousPage, parameters, command);
page.getPage().onEnterPage(parameters, command.getSession(),
callback);
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onAfterPageEnter(page, parameters, command);
}
if (callback.waitForAsync || callback.endState) {
if (!callback.waitForAsync) {
try {
settings.getPageCache().returnPage(page, false);
}
catch (Throwable t) {}
}
// don't do anything - it will happen in the callback onSuccess
// method
// or we are at some end state
} else {
// complete the process
callback.successComplete = true;
callback.onSuccess(true);
currentCommandStack.clear();
currentPageTokenStack.clear();
try {
settings.getPageCache().returnPage(page, false);
}
catch (Throwable t) {}
}
} catch (Throwable t) {
try {
settings.getPageCache().returnPage(page, true);
}
catch (Throwable t2) {}
ArrayList<Command> commands = new ArrayList<Command>();
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
Command cmd = handler.onPageEnterFailure(page, parameters,
command);
if (null != cmd && !commands.contains(cmd))
commands.add(cmd);
}
if (commands.size() == 0) {
if (settings.getPageLoader().isValidPageToken(PageLoader.PAGE_ERROR)) {
goTo(PageLoader.PAGE_ERROR);
}
else {
_forwardToPage(new UnhandledPageEnterErrorPage(
page.getPageToken(), t));
}
} else {
for (Command cmd : commands)
cmd.execute();
}
currentCommandStack.clear();
currentPageTokenStack.clear();
}
}
/**
* History token change listener
*/
public void onValueChange(ValueChangeEvent<String> event) {
onNewPage(event.getValue(), true);
}
/**
* Create and return the history token represented by the page token and
* additional parameters
*
* @param pageToken
* the page token
* @param parameters
* input parameters
*/
public String createHistoryToken(String pageToken,
Serializable... parameters) {
return settings.getTokenizer()
.createHistoryToken(pageToken, parameters);
}
/**
* Create and return the history token represented by the page token and
* additional parameters
*
* @param pageToken
* the page token
* @param parameters
* input parameters
*/
public String createHistoryToken(String pageToken,
HashMap<String, Serializable> parameters) {
return settings.getTokenizer()
.createHistoryToken(pageToken, parameters);
}
/**
* Stop the current request processing
*/
public void stopRequest() {
keepGoing = false;
currentCommandStack.clear();
currentPageTokenStack.clear();
}
public GotoPageCommand getLastCommand() {
return lastCommand;
}
/**
* Add page lifecycle event handler
*
* @param pageEventHandlers
* the handler(s)
* @return the Settings
*/
public Pages add(PageEventHandler... pageEventHandlers) {
for (PageEventHandler pageEventHandler : pageEventHandlers) {
pageEventHandler.init(this);
settings.pageEventHandlers.add(pageEventHandler);
if (pageEventHandler instanceof PageErrorEventHandler)
settings.pageErrorEventHandlers.add((PageErrorEventHandler) pageEventHandler);
if (pageEventHandler instanceof PageRequestEventHandler)
settings.pageRequestEventHandlers.add((PageRequestEventHandler) pageEventHandler);
}
return this;
}
/**
* Set the {@link Tokenizer}. If not set, {@link SimpleTokenizer} will be
* used.
*
* @param tokenizer
* the page tokenizer
* @return the Settings
*/
public Pages setPageTokenizer(Tokenizer tokenizer) {
settings.tokenizer = tokenizer;
tokenizer.init(this);
return this;
}
/**
* Set the page cacheing mechanism
* @param pageCache the page cacheing mechanism
*/
public void setPageCache(PageCache pageCache) {
settings.pageCache = pageCache;
}
/**
* Add default event handlers for handling page request messages as well as
* notifying the user when pages are being loaded. The handlers that are
* added are
* <ul>
* <ol>
* <li>{@link DefaultPageLoadingHandler}</li>
* <li>{@link DefaultMessageHandler}</li>
* </ul>
*/
public Pages addDefaultEventHandlers() {
add(new DefaultPageLoadingHandler(), new DefaultMessageHandler());
return this;
}
/**
* Return the settings for this pages instance
*/
public Settings getSettings() {
return settings;
}
/**
* Return the common event bus
*/
public HandlerManager getEventBus() {
return eventBus;
}
/**
* Return the {@link}
*
* @return
*/
public LoadedPageContainer getCurrentPage() {
return currentPage;
}
protected void _forwardToPage(Page page) {
settings.getApplicationPresenter().showPage(
new LoadedPageContainer(page), new SimplePageParameters(this),
new PageRequestSession());
}
/** Async callback classes */
private class _AsyncPageCallback implements AsyncPageCallback {
private LoadedPageContainer previousPage;
private LoadedPageContainer page;
private PageParameters pageParameters;
private GotoPageCommand command;
private boolean successComplete = false;
private boolean waitForAsync = false;
private boolean endState = false;
private _AsyncPageCallback(LoadedPageContainer page,
LoadedPageContainer previousPage,
PageParameters pageParameters, GotoPageCommand command) {
this.page = page;
this.previousPage = previousPage;
this.pageParameters = pageParameters;
this.command = command;
}
public void onSuccess() {
onSuccess(false);
}
private void onSuccess(boolean force) {
if (!force && successComplete) {
System.err
.println("Error with page '"
+ page.getPageToken()
+ "' onSuccess() was called and you must call waitForAsync() first.");
return;
}
try {
settings.getApplicationPresenter().showPage(page, pageParameters,
command.getSession());
currentPage = page;
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageEnterSuccess(page, pageParameters, command);
}
getEventBus().fireEvent(
new PageShownEvent(page, pageParameters, command,
previousPage));
successComplete = true;
currentCommandStack.clear();
currentPageTokenStack.clear();
try {
settings.getPageCache().returnPage(page, false);
}
catch (Throwable t) {}
}
catch (Throwable t) {
try {
settings.getPageCache().returnPage(page, true);
}
catch (Throwable t2) {}
}
}
public void onFailure(Throwable cause) {
if (successComplete) {
System.err
.println("Error with page '"
+ page.getPageToken()
+ "' onFailure() was called but the request was already completed successfully.");
return;
}
try {
settings.getPageCache().returnPage(page, true);
}
catch (Throwable t) {}
ArrayList<Command> commands = new ArrayList<Command>();
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
Command cmd = handler.onPageEnterFailure(page, pageParameters,
command);
if (null != cmd && !commands.contains(cmd))
commands.add(cmd);
}
if (commands.size() == 0) {
if (settings.getPageLoader().isValidPageToken(PageLoader.PAGE_ERROR)) {
goTo(PageLoader.PAGE_ERROR);
}
else {
_forwardToPage(new UnhandledPageEnterErrorPage(
page.getPageToken(), cause));
}
} else {
for (Command cmd : commands)
cmd.execute();
}
}
public void onIllegalAccess(Serializable... parameters) {
try {
// we don't define this as an error with the page - just user access issue hence the false
settings.getPageCache().returnPage(page, false);
}
catch (Throwable t) {}
if (successComplete) {
System.err
.println("Error with page '"
+ page.getPageToken()
+ "' onIllegalAccess() was called but the request was already completed successfully.");
return;
}
ArrayList<Command> commands = new ArrayList<Command>();
for (PageErrorEventHandler handler : settings.pageErrorEventHandlers) {
Command cmd = handler.onIllegalPageAccess(page, pageParameters,
command, parameters);
if (null != cmd && !commands.contains(cmd))
commands.add(cmd);
}
eventBus.fireEvent(new PageIllegalAccessEvent(page, pageParameters,
command, parameters));
if (commands.size() == 0) {
if (settings.getPageLoader().isValidPageToken(PageLoader.PAGE_ILLEGAL_ACCESS)) {
goTo(PageLoader.PAGE_ILLEGAL_ACCESS);
}
else {
_forwardToPage(new UnhandledIllegalAccessPage(page.getPageToken()));
}
} else {
for (Command cmd : commands)
cmd.execute();
}
endState = true;
}
public void waitForAsync() {
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageWaitForAsync(page, pageParameters, command);
}
waitForAsync = true;
}
public void forward(GotoPageCommand command) {
try {
settings.getPageCache().returnPage(page, false);
}
catch (Throwable t) {}
for (PageRequestEventHandler handler : settings.pageRequestEventHandlers) {
handler.onPageForward(page, pageParameters, this.command,
command);
}
endState = true;
command.addHistoryToken(false);
goTo(command);
}
}
private class _PageLoadCallback implements PageLoadCallback {
private PageParameters parameters;
private GotoPageCommand command;
public _PageLoadCallback(PageParameters parameters,
GotoPageCommand command) {
this.parameters = parameters;
this.command = command;
}
public void onPageFound(LoadedPageContainer pageLoadResult) {
Pages.this.onPageFound(pageLoadResult, parameters, command);
}
public void onPageNotFound(String pageToken) {
Pages.this.onPageNotFound(pageToken, parameters.getHistoryToken());
}
public void onPageLoadFailure(String pageToken, Throwable cause) {
Pages.this.onPageLoadFailure(pageToken,
parameters.getHistoryToken(), cause);
}
}
public class Settings {
protected ApplicationPresenter pagePresenter;
protected PageCache pageCache;
protected Tokenizer tokenizer;
protected PageLoader pageLoader;
protected ArrayList<PageEventHandler> pageEventHandlers = new ArrayList<PageEventHandler>();
protected ArrayList<PageErrorEventHandler> pageErrorEventHandlers = new ArrayList<PageErrorEventHandler>();
protected ArrayList<PageRequestEventHandler> pageRequestEventHandlers = new ArrayList<PageRequestEventHandler>();
/**
* Return the {@link Tokenizer}. If not set, {@link SimpleTokenizer}
* will be used.
*/
public Tokenizer getTokenizer() {
if (null == tokenizer) {
tokenizer = new SimpleTokenizer();
tokenizer.init(Pages.this);
}
return tokenizer;
}
/**
* Return a list of all defined page lifecycle event handlers (or empty
* list if none defined)
*/
public ArrayList<PageEventHandler> getEventHandlers() {
return pageEventHandlers;
}
/**
* Return the {@link PageLoader}.
*/
public PageLoader getPageLoader() {
return pageLoader;
}
/**
* Return the {@link ApplicationPresenter}
*/
public ApplicationPresenter getApplicationPresenter() {
return pagePresenter;
}
public PageCache getPageCache() {
if (null == pageCache) pageCache = new SimplePageCache();
return pageCache;
}
}
}