package org.sigmah.client.security;
/*
* #%L
* Sigmah
* %%
* Copyright (C) 2010 - 2016 URD
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-3.0.html>.
* #L%
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.sigmah.client.dispatch.AbstractDispatchAsync;
import org.sigmah.client.dispatch.DispatchAsync;
import org.sigmah.client.dispatch.DispatchListener;
import org.sigmah.client.event.EventBus;
import org.sigmah.client.page.PageManager;
import org.sigmah.client.page.RequestParameter;
import org.sigmah.client.ui.widget.Loadable;
import org.sigmah.client.ui.zone.Zone;
import org.sigmah.client.util.ClientUtils;
import org.sigmah.client.util.ToStringBuilder;
import org.sigmah.offline.dispatch.LocalDispatchServiceAsync;
import org.sigmah.shared.command.Synchronize;
import org.sigmah.shared.command.base.Command;
import org.sigmah.shared.command.result.Result;
import com.allen_sauer.gwt.log.client.Log;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;
import org.sigmah.client.event.OfflineEvent;
import org.sigmah.client.event.handler.OfflineHandler;
import org.sigmah.offline.status.ApplicationState;
import org.sigmah.offline.status.ApplicationStateManager;
/**
* This class is the default implementation of {@link DispatchAsync}, which is essentially the client-side access to the
* {@link org.sigmah.server.dispatch.Dispatch} class on the server-side.
*
* @author Tom Miette (tmiette@ideia.fr)
* @author Denis Colliot (dcolliot@ideia.fr)
*/
public class SecureDispatchAsync extends AbstractDispatchAsync {
/**
*
* Command execution wrapper containing following properties:
* <ul>
* <li>Authentication token (used to retrieve corresponding user).</li>
* <li>Command to execute.</li>
* <li>Current page token.</li>
* </ul>
*
* <p>
* Implements {@link Command} to ensure {@code IsSerializable} implementation.
* </p>
* <p>
* Note: for consistency, this class is supposed to be located into {@code shared} package. However, in order to
* conserve <em>local</em> {@code private} constructor, it remains in client package.
* </p>
*
* @author Denis Colliot (dcolliot@ideia.fr)
* @param <C>
* The command type.
* @param <R>
* The command result type.
*/
public static final class CommandExecution<C extends Command<R>, R extends Result> implements Command<R> {
private String authToken;
private C command;
private String currentPageToken;
/**
* Serialization constructor.
*/
private CommandExecution() {
// Serialization.
}
/**
* Initialiazes a new {@code CommandExecution}.
*
* @param authToken
* The authentication token.
* @param command
* The command to execute.
* @param currentPageToken
* The current page token.
*/
private CommandExecution(final String authToken, final C command, final String currentPageToken) {
this.authToken = authToken;
this.command = command;
this.currentPageToken = currentPageToken;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);
builder.append("authToken", authToken);
builder.append("command", command);
builder.append("currentPageToken", currentPageToken);
return builder.toString();
}
public String getAuthenticationToken() {
return authToken;
}
public C getCommand() {
return command;
}
public String getCurrentPageToken() {
return currentPageToken;
}
}
/**
* The RPC dispatch <em>real</em> service implementation.
*/
private static final SecureDispatchServiceAsync realService = GWT.create(SecureDispatchService.class);
/**
* The authentication provider.
*/
private final AuthenticationProvider authenticationProvider;
/**
* The application event bus.
*/
private final EventBus eventBus;
/**
* The application page manager.
*/
private final PageManager pageManager;
/**
* Listeners related to commands. Called when a command finishes its execution successfully.
*/
private final Map<Class<?>, List<DispatchListener<?, ?>>> listeners;
/**
* Implementation of the RPC dispatch service used when the user is offline.
*/
private LocalDispatchServiceAsync offlineService;
/**
* Connection state. Used to decide which service to call when dispatching
* a command.
*/
private boolean online;
/**
* Initializes the {@code SecureDispatchAsync} with injected arguments.
*
* @param authenticationProvider
* The {@link AuthenticationProvider} instance.
* @param eventBus
* The {@link EventBus} implementation.
* @param pageManager
* The {@link PageManager} implementation.
* @param applicationStateManager
* he {@link ApplicationStateManager} implementation.
*/
@Inject
public SecureDispatchAsync(final AuthenticationProvider authenticationProvider, final EventBus eventBus, final PageManager pageManager, final ApplicationStateManager applicationStateManager) {
this.authenticationProvider = authenticationProvider;
this.eventBus = eventBus;
this.pageManager = pageManager;
this.listeners = new HashMap<Class<?>, List<DispatchListener<?, ?>>>();
registerEventHandlers();
}
/**
* {@inheritDoc}
*/
@Override
public <C extends Command<R>, R extends Result> void execute(final C command, final AsyncCallback<R> callback, final Loadable... loadables) {
execute(command, callback, loadables != null ? Arrays.asList(loadables) : null);
}
/**
* {@inheritDoc}
*/
@Override
public <C extends Command<R>, R extends Result> void execute(final C command, final AsyncCallback<R> callback, final Collection<Loadable> loadables) {
// Sets loadable elements in "loading" state.
final long startTime = new Date().getTime();
setLoadableElementsEnabled(startTime, loadables, true);
// Retrieving auth token.
final String authToken = authenticationProvider.get().getAuthenticationToken();
// Command execution.
getDispatchService(command).execute(new CommandExecution<C, R>(authToken, command, pageManager.getCurrentPageToken()), new AsyncCallback<Result>() {
@Override
@SuppressWarnings("unchecked")
public void onSuccess(Result result) {
try {
// Note: This cast is a dodgy hack to get around a GWT 1.6 async compiler issue
SecureDispatchAsync.this.onSuccess(command, (R) result, callback);
fireSuccess(command, (R) result);
} finally {
// Sets loadable elements in "normal" state.
setLoadableElementsEnabled(startTime, loadables, false);
}
}
@Override
public void onFailure(final Throwable caught) {
try {
SecureDispatchAsync.this.onFailure(command, caught, callback, loadables);
} finally {
// Sets loadable elements in "normal" state.
setLoadableElementsEnabled(startTime, loadables, false);
}
}
});
}
public <C extends Command<R>, R extends Result> void registerListener(Class<C> commandClass, DispatchListener<C, R> listener) {
List<DispatchListener<?, ?>> commandListeners = listeners.get(commandClass);
if (commandListeners == null) {
commandListeners = new ArrayList<DispatchListener<?, ?>>();
listeners.put(commandClass, commandListeners);
}
commandListeners.add(listener);
}
protected <C extends Command<R>, R extends Result> void fireSuccess(C command, R result) {
if (online) {
final List<DispatchListener<?, ?>> commandListeners = listeners.get(command.getClass());
if (commandListeners != null) {
for (final DispatchListener<?, ?> listener : commandListeners) {
@SuppressWarnings("unchecked")
final DispatchListener<C, R> cast = (DispatchListener<C, R>) listener;
cast.onSuccess(command, result, authenticationProvider.get());
}
}
}
}
public void setOfflineService(LocalDispatchServiceAsync offlineService) {
this.offlineService = offlineService;
}
// --------------------------------------------------------------------------------
//
// UTILITY METHODS.
//
// --------------------------------------------------------------------------------
/**
* Running commands start timestamp with their corresponding {@code Loadable} element(s).
*/
private static final Map<Long, Collection<Loadable>> commandsMap = new HashMap<Long, Collection<Loadable>>();
/**
* Running actions start timestamp with their corresponding {@code Loadable} element(s).
*/
private static final Map<Loadable, Integer> loadablesMap = new HashMap<Loadable, Integer>();
/**
* One loadable count.
*/
private static final Integer ONE = new Integer(1);
/**
* Command execution with no response timeout limit (in milliseconds).
* <em>Currently set to <b>5 minutes</b>.</em>
*/
private static final Long TIMEOUT = new Long(5 * 60 * 1000);
/**
* Sets the given {@code loadables} elements with the given {@code loading} state.
*
* @param loadables
* The {@link Loadable} elements.
* @param loading
* {@code true} to set elements in loading mode.
*/
private void setLoadableElementsEnabled(final Long startTime, final Collection<Loadable> loadables, final boolean loading) {
if (Log.isTraceEnabled()) {
Log.trace("Loading state: " + loading + " ; Running actions: " + commandsMap.size());
}
// Registering command and Updating loadables elements states (if any).
if (loading) {
commandsMap.put(startTime, loadables);
if (ClientUtils.isNotEmpty(loadables)) {
// Updating loadable elements state.
for (final Loadable loadable : loadables) {
if (loadable == null) {
continue;
}
final Integer count = loadablesMap.get(loadable);
loadablesMap.put(loadable, count == null ? ONE : count + 1);
loadable.setLoading(true);
}
}
} else {
disableLoadingState(loadables);
commandsMap.remove(startTime);
// Clearing commands that have exceeded timeout limit.
final Set<Long> commandsToRemove = new HashSet<Long>();
final Long now = new Date().getTime();
for (final Entry<Long, Collection<Loadable>> entry : commandsMap.entrySet()) {
if (TIMEOUT.compareTo(now - entry.getKey()) <= 0) {
commandsToRemove.add(entry.getKey());
}
}
for (final Long commandToRemove : commandsToRemove) {
disableLoadingState(commandsMap.get(commandToRemove));
commandsMap.remove(commandToRemove);
}
}
// Updating application loader state.
if (eventBus != null) {
eventBus.updateZoneRequest(Zone.APP_LOADER.requestWith(RequestParameter.CONTENT, ClientUtils.isNotEmpty(commandsMap)));
}
}
/**
* Disables the {@code loading} state of the given {@code loadables} elements.
*
* @param loadables
* The loadables elements (does nothing if {@code null} or empty).
*/
private static void disableLoadingState(final Collection<Loadable> loadables) {
if (ClientUtils.isEmpty(loadables)) {
return;
}
for (final Loadable loadable : loadables) {
if (loadable == null) {
continue;
}
final Integer count = loadablesMap.get(loadable);
if (count == null || ONE.equals(count)) {
// Disabling 'loading' state ONLY IF no more command references this loadable.
loadablesMap.remove(loadable);
loadable.setLoading(false);
} else {
loadablesMap.put(loadable, count - 1);
}
}
}
/**
* Begin to listen to events from the event bus.
*/
private void registerEventHandlers() {
eventBus.addHandler(OfflineEvent.getType(), new OfflineHandler() {
@Override
public void handleEvent(OfflineEvent event) {
online = event.getState() == ApplicationState.ONLINE;
}
});
}
private <C extends Command<R>, R extends Result> SecureDispatchServiceAsync getDispatchService(C command) {
if (online || command instanceof Synchronize) {
return realService;
} else {
return offlineService;
}
}
}