/*
* Copyright (C) 2014 Red Hat, Inc. and/or its affiliates.
*
* 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 org.jboss.errai.jpa.sync.client.local;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.persistence.NamedQuery;
import org.jboss.errai.common.client.api.Assert;
import org.jboss.errai.common.client.api.ErrorCallback;
import org.jboss.errai.common.client.api.RemoteCallback;
import org.jboss.errai.ioc.client.container.IOC;
import org.jboss.errai.ioc.client.lifecycle.api.LifecycleEvent;
import org.jboss.errai.ioc.client.lifecycle.api.LifecycleListener;
import org.jboss.errai.ioc.client.lifecycle.api.StateChange;
import org.jboss.errai.jpa.sync.client.shared.SyncResponse;
import org.jboss.errai.jpa.sync.client.shared.SyncResponses;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gwt.user.client.Timer;
/**
* Handles the job of keeping one JPA {@link NamedQuery} in sync between the client and server. A
* ClientSyncWorker has three states:
* <ol>
* <li>Not yet started - no sync happens
* <li>Running - data sync operations happen automatically
* <li>Stopped - no sync happens
* </ol>
*
* New instances are in the "not yet started" state. You start them with a call to {@link #start()},
* and stop them with a call to {@link #stop()}. Once started, a sync worker instance can be stopped
* but not restarted. Once stopped, a sync worker cannot be restarted.
*
* @author Jonathan Fuerth <jfuerth@redhat.com>
* @author Christian Sadilek <csadilek@redhat.com>
*
* @param <E>
* The entity type this worker's named query returns.
*/
public class ClientSyncWorker<E> {
private static final int SYNC_PERIOD_MILLIS = 5000;
private static final Logger logger = LoggerFactory.getLogger(ClientSyncWorker.class);
private final List<DataSyncCallback<E>> callbacks = new ArrayList<DataSyncCallback<E>>();
private LifecycleListener<Object> beanlifecycleListener;
private Object managedBeanInstance;
private final Timer timer;
private boolean started;
private boolean stopped;
private Map<String, Object> queryParams;
/**
* The callback that gets notified by ClientSyncManager when a sync operation has completed.
* Notifies this worker's callbacks.
*/
private final RemoteCallback<List<SyncResponse<E>>> onCompletion = new RemoteCallback<List<SyncResponse<E>>>() {
@Override
public void callback(List<SyncResponse<E>> response) {
SyncResponses<E> responses = new SyncResponses<E>(response);
for (DataSyncCallback<E> callback : callbacks) {
try {
callback.onSync(responses);
}
catch (Throwable t) {
logger.error("Ignoring Exception from DataSyncCallback:", t);
}
}
}
};
private final RemoteCallback<List<SyncResponse<E>>> timerSchedulingRemoteCallback = new RemoteCallback<List<SyncResponse<E>>>() {
@Override
public void callback(final List<SyncResponse<E>> responses) {
try {
ClientSyncWorker.this.onCompletion.callback(responses);
}
finally {
scheduleTimerIfNotStopped(SYNC_PERIOD_MILLIS);
}
}
};
@SuppressWarnings("rawtypes")
private final ErrorCallback<?> timerSchedulingErrorCallback = new ErrorCallback() {
@Override
@SuppressWarnings("unchecked")
public boolean error(Object message, Throwable throwable) {
boolean retVal = true;
try {
retVal = ClientSyncWorker.this.onError.error(message, throwable);
}
finally {
scheduleTimerIfNotStopped(SYNC_PERIOD_MILLIS);
}
return retVal;
}
};
private final String queryName;
private final Class<E> queryResultType;
@SuppressWarnings("rawtypes")
private final ErrorCallback onError;
/**
* A callback that provides parameters for the query used by a {@link ClientSyncWorker}.
*/
public static interface QueryParamInitCallback {
/**
* Returns a map from query parameter name to its value. Never null. In case a query has no
* parameters, an empty map is returned.
*/
public Map<String, Object> getQueryParams();
}
/**
* Creates a new ClientSyncWorker which takes responsibility for syncing the results of the named
* JPA query.
*
* @param <E>
* The entity type the named query returns.
* @param queryName
* The name of a JPA named query. Must be visible to client-side code, and if it has
* parameters, they must be named (not positional) parameters.
* @param queryResultType
* The result type returned by the named query.
* @param onError
* Error callback that should be invoked if any sync request encounters a data
* transmission error on the bus. If null, transmission errors are logged to the slf4j
* logger for the {@link ClientSyncWorker} class.
* @return a new ClientSyncWorker instance in the "not yet started" state.
*/
public static <E> ClientSyncWorker<E> create(final String queryName, final Class<E> queryResultType,
final ErrorCallback<?> onError) {
return new ClientSyncWorker<E>(ClientSyncManager.getInstance(), queryName, queryResultType, onError);
}
/**
* Creates a new ClientSyncWorker which takes responsibility for syncing the results of the named
* JPA query.
* <p>
* This constructor is primarily intended for testing. Consider using
* {@link #create(String, Class, Map, ErrorCallback)} instead, which obtains an instance of
* ClientSyncManager from the IOC Bean Manager.
*
* @param manager
* The instance of ClientSyncManager that should be used for all data sync operations.
* @param queryName
* The name of a JPA named query. Must be visible to client-side code, and if it has
* parameters, they must be named (not positional) parameters.
* @param queryResultType
* The result type returned by the named query.
* @param onError
* Error callback that should be invoked if any sync request encounters a data
* transmission error on the bus. If null, transmission errors are logged to the slf4j
* logger for the {@link ClientSyncWorker} class.
* @return a new ClientSyncWorker instance in the "not yet started" state.
*/
public ClientSyncWorker(final ClientSyncManager manager, final String queryName, final Class<E> queryResultType,
final ErrorCallback<?> onError) {
Assert.notNull(manager);
this.queryName = Assert.notNull(queryName);
this.queryResultType = Assert.notNull(queryResultType);
this.onError = onError;
timer = new Timer() {
@Override
public void run() {
try {
manager.coldSync(ClientSyncWorker.this.queryName, ClientSyncWorker.this.queryResultType, queryParams,
timerSchedulingRemoteCallback, timerSchedulingErrorCallback);
}
catch (Throwable t) {
if (!manager.isSyncInProgress()) {
scheduleTimerIfNotStopped(SYNC_PERIOD_MILLIS);
}
}
}
};
}
/**
* Registers the given callback to receive notifications each time a sync operation has been
* performed.
*
* @param onCompletion
* the callback to notify of completed sync operations. Must not be null.
*/
public void addSyncCallback(final DataSyncCallback<E> onCompletion) {
Assert.notNull(onCompletion);
callbacks.add(onCompletion);
}
/**
* Starts this sync worker if it has not already been started or stopped.
*
* @param queryParams
* Name-value pairs for all named parameters in the named query. Never null.
*
* @throws IllegalStateException
* if this sync worker has been stopped.
*/
public void start(Map<String, Object> queryParams) {
if (stopped)
throw new IllegalStateException("This worker was already stopped");
this.queryParams = Assert.notNull(queryParams);
started = true;
// let's sync immediately so we don't have to wait 5 seconds before the first sync
timer.run();
}
/**
* Starts this sync worker if it has not already been started or stopped.
*
* @param beanInstance
* The managed bean instance the observes the sync results and defines the query
* parameters.
*
* @param queryParamCallback
* A {@link QueryParamInitCallback} that provides the query parameters for this
* {@link ClientSyncWorker}'s query.
*
* @throws IllegalStateException
* if this sync worker has been stopped.
*/
public void start(final Object beanInstance, final QueryParamInitCallback queryParamCallback) {
if (stopped)
throw new IllegalStateException("This worker was already stopped");
started = true;
this.managedBeanInstance = beanInstance;
this.queryParams = queryParamCallback.getQueryParams();
// Register a lifecycle listener for the managed bean so we can update the query params
// when the bean's state changes (i.e. when errai navigation updates the @PageState fields.)
beanlifecycleListener = new LifecycleListener<Object>() {
@Override
public void observeEvent(LifecycleEvent<Object> event) {
ClientSyncWorker.this.queryParams = queryParamCallback.getQueryParams();
}
@Override
public boolean isObserveableEventType(Class<? extends LifecycleEvent<Object>> eventType) {
return eventType.equals(StateChange.class);
}
};
IOC.registerLifecycleListener(beanInstance, beanlifecycleListener);
// Let's give control back so that other parts of the framework have a chance to update fields
// of the managed bean before we start the first synchronization
new Timer() {
@Override
public void run() {
queryParams = queryParamCallback.getQueryParams();
timer.run();
}
}.schedule(500);
}
/**
* Stops this sync worker if it is running.
*
* @throws IllegalStateException
* if this sync worker has not yet been started.
*/
public void stop() {
if (!started)
throw new IllegalStateException("This worker was never started");
stopped = true;
callbacks.clear();
if (beanlifecycleListener != null && managedBeanInstance != null) {
IOC.unregisterLifecycleListener(managedBeanInstance, beanlifecycleListener);
}
timer.cancel();
}
private void scheduleTimerIfNotStopped(final int delayMillis) {
if (!started)
throw new IllegalStateException("This worker was never started");
if (!stopped)
timer.schedule(delayMillis);
}
}