![JAR search and dependency download from the Maven repository](/logo.png)
org.geomajas.gwt.client.command.GwtCommandDispatcher Maven / Gradle / Ivy
/*
* This is part of Geomajas, a GIS framework, http://www.geomajas.org/.
*
* Copyright 2008-2014 Geosparc nv, http://www.geosparc.com/, Belgium.
*
* The program is available in open source according to the GNU Affero
* General Public License. All contributions in this program are covered
* by the Geomajas Contributors License Agreement. For full licensing
* details, see LICENSE.txt in the project root.
*/
package org.geomajas.gwt.client.command;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.i18n.client.LocaleInfo;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.rpc.ServiceDefTarget;
import org.geomajas.annotation.Api;
import org.geomajas.command.CommandResponse;
import org.geomajas.global.ExceptionCode;
import org.geomajas.global.ExceptionDto;
import org.geomajas.global.GeomajasConstant;
import org.geomajas.gwt.client.GeomajasService;
import org.geomajas.gwt.client.GeomajasServiceAsync;
import org.geomajas.gwt.client.command.event.DispatchStartedEvent;
import org.geomajas.gwt.client.command.event.DispatchStartedHandler;
import org.geomajas.gwt.client.command.event.DispatchStoppedEvent;
import org.geomajas.gwt.client.command.event.DispatchStoppedHandler;
import org.geomajas.gwt.client.command.event.HasDispatchHandlers;
import org.geomajas.gwt.client.command.event.TokenChangedEvent;
import org.geomajas.gwt.client.command.event.TokenChangedHandler;
import org.geomajas.gwt.client.util.Log;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* The central client side dispatcher for all commands. Use the {@link #execute(GwtCommand, CommandCallback...)}
* function to execute an asynchronous command on the server.
*
* Set a {@link TokenRequestHandler} to make sure the class automatically makes the user (re) login when needed.
*
* @author Pieter De Graef
* @author Oliver May
* @author Joachim Van der Auwera
* @since 2.0.0
*/
@Api(allMethods = true)
public final class GwtCommandDispatcher implements HasDispatchHandlers, CommandExceptionCallback,
CommunicationExceptionCallback {
private static final String SECURITY_EXCEPTION_CLASS_NAME = "org.geomajas.security.GeomajasSecurityException";
private static final String RANDOM_UNLIKELY_TOKEN = "%t@kén§#";
private static GwtCommandDispatcher instance = new GwtCommandDispatcher();
private final GeomajasServiceAsync service;
private final HandlerManager manager = new HandlerManager(this);
private final List deferreds;
private int nrOfDispatchedCommands;
private String locale;
private String userToken;
private UserDetail userDetail = new UserDetail();
private boolean useLazyLoading;
private int lazyFeatureIncludesDefault;
private int lazyFeatureIncludesSelect;
private int lazyFeatureIncludesAll;
private boolean showError;
private TokenRequestHandler tokenRequestHandler;
private CommandExceptionCallback commandExceptionCallback;
private CommunicationExceptionCallback communicationExceptionCallback;
// map is not synchronized as this class runs in JavaScript which only has one execution thread
private final Map> afterLoginCommands = new HashMap>();
private GwtCommandDispatcher() {
locale = LocaleInfo.getCurrentLocale().getLocaleName();
if ("default".equals(locale)) {
locale = null;
}
deferreds = new ArrayList();
service = (GeomajasServiceAsync) GWT.create(GeomajasService.class);
setServiceEndPointUrl(GWT.getModuleBaseURL() + "geomajasService");
setUseLazyLoading(true);
setShowError(true);
Window.addWindowClosingHandler(new ClosingHandler() {
public void onWindowClosing(ClosingEvent event) {
GwtCommandDispatcher.getInstance().setShowError(false);
// Cancel all outstanding requests:
for (Deferred deferred : deferreds) {
deferred.cancel();
}
}
});
}
// -------------------------------------------------------------------------
// Public methods:
// -------------------------------------------------------------------------
/**
* Get the only static instance of this class. This should be the object you work with.
*
* @return singleton instance
*/
public static GwtCommandDispatcher getInstance() {
return instance;
}
@Override
public HandlerRegistration addDispatchStartedHandler(DispatchStartedHandler handler) {
return manager.addHandler(DispatchStartedHandler.TYPE, handler);
}
@Override
public HandlerRegistration addDispatchStoppedHandler(DispatchStoppedHandler handler) {
return manager.addHandler(DispatchStoppedHandler.TYPE, handler);
}
/**
* The execution function. Executes a server side command.
*
* @param command
* The command to be executed. This command is a wrapper around the actual request object.
* @param callback
* A CommandCallback
function to be executed when the command successfully returns. The
* callbacks may implement CommunicationExceptionCallback or CommandExceptionCallback to allow error
* handling.
* @return deferred object which can be used to add extra callbacks
*/
public Deferred execute(final GwtCommand command, final CommandCallback... callback) {
final Deferred deferred = new Deferred();
for (CommandCallback successCallback : callback) {
try {
deferred.addCallback(successCallback);
} catch (Throwable t) {
Log.logError("Command failed on success callback", t);
}
}
return execute(command, deferred);
}
/**
* The execution function. Executes a server side command.
*
* @param command
* The command to be executed. This command is a wrapper around the actual request object.
* @param deferred
* list of callbacks for the command
* @return original deferred object as passed as parameter
*/
public Deferred execute(final GwtCommand command, final Deferred deferred) {
if (!deferreds.contains(deferred)) {
deferreds.add(deferred);
}
command.setLocale(locale);
command.setUserToken(userToken);
// shortcut, no need to invoke the server if we know the token has expired
if (null != userToken && userToken.length() > 0 && afterLoginCommands.containsKey(userToken)) {
afterLogin(command, deferred);
return deferred;
}
incrementDispatched();
service.execute(command, new AsyncCallback() {
public void onFailure(Throwable error) {
try {
boolean errorHandled = false;
for (CommandCallback> callback : deferred.getCallbacks()) {
if (callback instanceof CommunicationExceptionCallback) {
try {
((CommunicationExceptionCallback) callback).onCommunicationException(error);
} catch (Throwable t) {
Log.logError("Command failed on error callback", t);
}
errorHandled = true;
}
}
if (!errorHandled && deferred.isLogCommunicationExceptions()) {
onCommunicationException(error);
}
} catch (Throwable t) {
if (deferred.isLogCommunicationExceptions()) {
Log.logError("Command failed on error callback", t);
}
} finally {
decrementDispatched();
deferreds.remove(deferred);
}
}
public void onSuccess(CommandResponse response) {
try {
if (response.isError()) {
handleError(response);
} else {
if (!deferred.isCancelled()) {
for (CommandCallback callback : deferred.getCallbacks()) {
try {
callback.execute(response);
} catch (Throwable t) {
Log.logError("Command failed on success callback", t);
}
}
}
}
} catch (Throwable t) {
Log.logError("Command failed on success callback", t);
} finally {
decrementDispatched();
deferreds.remove(deferred);
}
}
private void handleError(CommandResponse response) {
// Security exceptions which indicate that the token is invalid cause a token request.
// Other security exceptions are normally reported, except when then undefined (null or empty)
// token is used. For restricted access for the anonymous user, this should also be assigned
// a proper token (can be a constant).
// This assumes that the undefined token is only used either in combination with the allow all
// security service or in a state between logout and login.
boolean authenticationFailed = false;
for (ExceptionDto exception : response.getExceptions()) {
authenticationFailed |= SECURITY_EXCEPTION_CLASS_NAME.equals(exception.getClassName())
&& (ExceptionCode.CREDENTIALS_MISSING_OR_INVALID == exception.getExceptionCode()
|| isUndefinedToken(command.getUserToken()));
}
if (authenticationFailed && null != tokenRequestHandler) {
handleLogin(command, deferred);
} else {
// normal error handling...
boolean errorHandled = false;
for (CommandCallback callback : deferred.getCallbacks()) {
if (callback instanceof CommandExceptionCallback) {
try {
((CommandExceptionCallback) callback).onCommandException(response);
errorHandled = true;
} catch (Throwable t) {
Log.logError("Command failed on error callback", t);
}
}
}
// fallback to the default behaviour
if (!errorHandled) {
try {
onCommandException(response);
} catch (Throwable t) {
Log.logError("Command failed on error callback", t);
}
}
}
}
});
return deferred;
}
/**
* Add a command and it's callbacks to the list of commands to retry after login.
*
* @param command
* command to retry
* @param deferred
* callbacks for the command
*/
private void afterLogin(GwtCommand command, Deferred deferred) {
String token = notNull(command.getUserToken());
if (!afterLoginCommands.containsKey(token)) {
afterLoginCommands.put(token, new ArrayList());
}
afterLoginCommands.get(token).add(new RetryCommand(command, deferred));
}
/**
* Method which forces retry of a command after login.
*
* This method assumes the single threaded nature of JavaScript execution for correctness.
*
* @param command
* command which needs to be retried
* @param deferred
* callbacks for the command
*/
private void handleLogin(GwtCommand command, Deferred deferred) {
final String oldToken = notNull(command.getUserToken());
if (!afterLoginCommands.containsKey(oldToken)) {
afterLoginCommands.put(oldToken, new ArrayList());
login(oldToken);
}
afterLogin(command, deferred);
}
/**
* Assure that a string is not null, convert to empty string if it is.
*
* @param string
* string to convert
* @return converted string
*/
private String notNull(String string) {
if (null == string) {
return "";
}
return string;
}
/**
* Check whether this is an "undefined" token.
*
* @param token
* token to test
* @return true when token is null or empty string
*/
private boolean isUndefinedToken(String token) {
return null == token || 0 == token.length();
}
/**
* Default behaviour for handling a communication exception. Shows a warning window to the user.
*
* @param error
* error to report
*/
public void onCommunicationException(Throwable error) {
if (isShowError()) {
communicationExceptionCallback.onCommunicationException(error);
}
}
/**
* Default behaviour for handling a command execution exception. Shows an exception report to the user.
*
* @param response
* command response with error
*/
public void onCommandException(CommandResponse response) {
if (isShowError()) {
commandExceptionCallback.onCommandException(response);
}
}
/**
* Is the dispatcher busy ?
*
* @return true if there are outstanding commands
*/
public boolean isBusy() {
return nrOfDispatchedCommands != 0;
}
/**
* Set the service end point URL to a different value. If pointing to a different context, make sure the
* GeomajasController of that context supports this.
*
* @see org.geomajas.gwt.server.mvc.GeomajasController
*
* @param url
* the new URL
*/
public void setServiceEndPointUrl(String url) {
ServiceDefTarget endpoint = (ServiceDefTarget) service;
endpoint.setServiceEntryPoint(url);
}
/**
* Force request a new user token. This is not used for extending the user token, that is handled automatically.
*/
public void login() {
logout(true);
login(RANDOM_UNLIKELY_TOKEN);
}
/**
* Force request a new login, the dangling commands for the previous token are retried when logged in.
*
* @param oldToken
* previous token
*/
private void login(final String oldToken) {
tokenRequestHandler.login(new TokenChangedHandler() {
public void onTokenChanged(TokenChangedEvent event) {
setToken(event.getToken(), event.getUserDetail(), false);
List retryCommands = afterLoginCommands.remove(oldToken);
if (null != retryCommands) {
for (RetryCommand retryCommand : retryCommands) {
execute(retryCommand.getCommand(), retryCommand.getDeferred());
}
}
}
});
}
/**
* Logout. Clear the user token.
*
* @since 2.0.0
*/
public void logout() {
logout(false);
}
/**
* Logout. Clear the user token. Can indicate that a login is about to follow.
*
* @param loginPending
* is a new login pending?
*/
private void logout(boolean loginPending) {
setToken(null, null, loginPending);
}
/**
* Set the user token, so it can be sent in every command. This is the internal version, used by the token changed
* handler.
*
* @param userToken
* user token
* @param userDetail
* user details
* @param loginPending
* true if this will be followed by a fresh token change
*/
private void setToken(String userToken, UserDetail userDetail, boolean loginPending) {
boolean changed = !isEqual(this.userToken, userToken);
this.userToken = userToken;
if (null == userDetail) {
userDetail = new UserDetail();
}
this.userDetail = userDetail;
if (changed) {
TokenChangedEvent event = new TokenChangedEvent(userToken, userDetail, loginPending);
manager.fireEvent(event);
}
}
/**
* Get currently active user authentication token.
*
* @return authentication token
*/
public String getUserToken() {
return userToken;
}
/**
* Get details for the current user.
*
* Object is always not-null, but the entries may be.
*
* @return user details object
*/
public UserDetail getUserDetail() {
return userDetail;
}
/**
* Add handler which is notified when the user token changes.
*
* @param handler
* token changed handler
* @return handler registration
*/
public HandlerRegistration addTokenChangedHandler(TokenChangedHandler handler) {
return manager.addHandler(TokenChangedHandler.TYPE, handler);
}
/**
* Set the login handler which should be used to request aan authentication token.
*
* @param tokenRequestHandler
* login handler
*/
public void setTokenRequestHandler(TokenRequestHandler tokenRequestHandler) {
this.tokenRequestHandler = tokenRequestHandler;
}
/**
* Get the current token request handler.
*
* @return token request handler
*/
public TokenRequestHandler getTokenRequestHandler() {
return tokenRequestHandler;
}
/**
* Set default command exception callback.
*
* @param commandExceptionCallback
* command exception callback
*/
public void setCommandExceptionCallback(CommandExceptionCallback commandExceptionCallback) {
this.commandExceptionCallback = commandExceptionCallback;
}
/**
* Set default communication exception callback.
*
* @param communicationExceptionCallback
* communication exception callback
*/
public void setCommunicationExceptionCallback(CommunicationExceptionCallback communicationExceptionCallback) {
this.communicationExceptionCallback = communicationExceptionCallback;
}
/**
* Is lazy feature loading enabled ?
*
* @return true when lazy feature loading is enabled
*/
public boolean isUseLazyLoading() {
return useLazyLoading;
}
/**
* Set lazy feature loading status.
*
* @param useLazyLoading
* lazy feature loading status
*/
public void setUseLazyLoading(boolean useLazyLoading) {
if (useLazyLoading != this.useLazyLoading) {
if (useLazyLoading) {
lazyFeatureIncludesDefault = GeomajasConstant.FEATURE_INCLUDE_STYLE
+ GeomajasConstant.FEATURE_INCLUDE_LABEL;
lazyFeatureIncludesSelect = GeomajasConstant.FEATURE_INCLUDE_ALL;
lazyFeatureIncludesAll = GeomajasConstant.FEATURE_INCLUDE_ALL;
} else {
lazyFeatureIncludesDefault = GeomajasConstant.FEATURE_INCLUDE_ALL;
lazyFeatureIncludesSelect = GeomajasConstant.FEATURE_INCLUDE_ALL;
lazyFeatureIncludesAll = GeomajasConstant.FEATURE_INCLUDE_ALL;
}
}
this.useLazyLoading = useLazyLoading;
}
/**
* Get default value for "featureIncludes" when getting features.
*
* @return default "featureIncludes" value
*/
public int getLazyFeatureIncludesDefault() {
return lazyFeatureIncludesDefault;
}
/**
* Set default value for "featureIncludes" when getting features.
*
* @param lazyFeatureIncludesDefault
* default for "featureIncludes"
*/
public void setLazyFeatureIncludesDefault(int lazyFeatureIncludesDefault) {
setUseLazyLoading(false);
this.lazyFeatureIncludesDefault = lazyFeatureIncludesDefault;
}
/**
* Get "featureIncludes" to use when selecting features.
*
* @return default "featureIncludes" for select commands
*/
public int getLazyFeatureIncludesSelect() {
return lazyFeatureIncludesSelect;
}
/**
* Set default "featureIncludes" for select commands.
*
* @param lazyFeatureIncludesSelect
* default "featureIncludes" for select commands
*/
public void setLazyFeatureIncludesSelect(int lazyFeatureIncludesSelect) {
setUseLazyLoading(false);
this.lazyFeatureIncludesSelect = lazyFeatureIncludesSelect;
}
/**
* Value to use for "featureIncludes" when all should be included.
*
* @return value for "featureIncludes" when all should be included
*/
public int getLazyFeatureIncludesAll() {
return lazyFeatureIncludesAll;
}
/**
* Set "featureIncludes" value when all should be included.
*
* @param lazyFeatureIncludesAll
* "featureIncludes" value when all should be included
*/
public void setLazyFeatureIncludesAll(int lazyFeatureIncludesAll) {
setUseLazyLoading(false);
this.lazyFeatureIncludesAll = lazyFeatureIncludesAll;
}
/**
* Should the dispatcher show error messages ?
*
* @return true if showing error messages, false otherwise
*/
public boolean isShowError() {
return showError;
}
/**
* Sets whether the dispatcher should show error messages.
*
* @param showError
* true if showing error messages, false otherwise
*/
public void setShowError(boolean showError) {
this.showError = showError;
}
// -------------------------------------------------------------------------
// Protected methods:
// -------------------------------------------------------------------------
protected void incrementDispatched() {
boolean started = nrOfDispatchedCommands == 0;
nrOfDispatchedCommands++;
if (started) {
manager.fireEvent(new DispatchStartedEvent());
}
}
protected void decrementDispatched() {
nrOfDispatchedCommands--;
if (nrOfDispatchedCommands == 0) {
manager.fireEvent(new DispatchStoppedEvent());
}
}
/**
* Checks whether 2 objects are equal. Null-safe, 2 null objects are considered equal.
*
* @param o1
* first object to compare
* @param o2
* second object to compare
* @return true if object are equal, false otherwise
*/
private boolean isEqual(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
/**
* Representation of a command which needs to be retried later.
*
* @author Joachim Van der Auwera
*/
private static class RetryCommand {
private final GwtCommand command;
private final Deferred deferred;
/**
* Create data to allow retying the command later.
*
* @param command
* command
* @param deferred
* callbacks
*/
public RetryCommand(GwtCommand command, Deferred deferred) {
this.command = command;
this.deferred = deferred;
}
/**
* Get the GwtCommand which needs to be retried.
*
* @return command
*/
public GwtCommand getCommand() {
return command;
}
/**
* Get the callbacks for handling the retied command.
*
* @return callbacks
*/
public Deferred getDeferred() {
return deferred;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy