/*
* Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com]
* 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 de.ks.activity;
import de.ks.activity.context.ActivityContext;
import de.ks.activity.context.ActivityStore;
import de.ks.activity.executor.ActivityExecutor;
import de.ks.activity.executor.ActivityExecutorProducer;
import de.ks.activity.executor.ActivityJavaFXExecutor;
import de.ks.activity.executor.ActivityJavaFXExecutorProducer;
import de.ks.activity.initialization.ActivityCallback;
import de.ks.activity.initialization.ActivityInitialization;
import de.ks.activity.loading.ActivityLoadingExecutor;
import de.ks.application.Navigator;
import de.ks.datasource.DataSource;
import de.ks.eventsystem.bus.EventBus;
import de.ks.util.LockSupport;
import javafx.application.Platform;
import javafx.scene.Node;
import javafx.scene.control.Control;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.StackPane;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PreDestroy;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
/**
* used to control different activities and their interaction
*/
@Singleton
public class ActivityController {
private static final Logger log = LoggerFactory.getLogger(ActivityController.class);
protected final ActivityLoadingExecutor loadingExecutor = new ActivityLoadingExecutor();
@Inject
protected ActivityContext context;
@Inject
protected ActivityStore store;
@Inject
protected Instance<Navigator> navigator;
@Inject
protected ActivityInitialization initialization;
@Inject
protected EventBus eventBus;
@Inject
protected ActivityExecutor executor;
@Inject
protected ActivityJavaFXExecutor javaFXExecutor;
@Inject
protected Instance<Object> factory;
protected final Map<String, ActivityCfg> registeredActivities = new HashMap<>();
protected final ReentrantLock lock = new ReentrantLock(true);
public void stopCurrentStartNew(ActivityHint activityHint) {
loadInExecutor("could not start activityhint " + activityHint, () -> {
stopCurrent();
startOrResume(activityHint);
});
}
public void startOrResume(ActivityHint activityHint) {
String previousActivity = context.getCurrentActivity();
loadInExecutor("could not start activityhint " + activityHint, () -> {
try (LockSupport lockSupport = new LockSupport(lock)) {
log.debug("Begin with start/resume of {} ", activityHint.getDescription());
if (isCurrentActivity(activityHint)) {
log.debug("skip starting activity {} because it is already active");
reload();
store.waitForDataSource();
return;
}
showBusy();
Object dataSourceHint = null;
if (hasCurrentActivity() && context.hasCurrentActivity()) {
if (activityHint.getDataSourceHint() != null) {
dataSourceHint = activityHint.getDataSourceHint().get();
}
suspendCurrent();
}
String id = activityHint.getNextActivityId();
if (registeredActivities.containsKey(id)) {
resume(id, activityHint.isRefreshOnReturn(), dataSourceHint, null, activityHint);
} else {
context.startActivity(id);
ActivityCfg activityCfg = factory.select(activityHint.getNextActivity()).get();
registeredActivities.put(id, activityCfg);
activityCfg.setActivityHint(activityHint);
log.info("Starting activity {}", id);
DataSource dataSource = factory.select(activityCfg.getDataSource()).get();
dataSource.setLoadingHint(dataSourceHint);
store.setDatasource(dataSource);
initialization.loadActivity(activityCfg);
initialization.getControllers().forEach(eventBus::register);
select(activityCfg, activityCfg.getInitialController(), Navigator.MAIN_AREA);
initialization.getActivityCallbacks().forEach(ActivityCallback::onStart);
if (activityHint.isRefreshOnReturn()) {
reload();
store.waitForDataSource();
}
log.info("Started activity {}", id);
}
} catch (Exception e) {
log.error("Failed to start {} because of ", activityHint.getDescription(), e);
context.stopActivity(activityHint.getNextActivityId());
if (previousActivity != null) {
context.startActivity(previousActivity);
}
throw e;
} finally {
log.debug("Done with start/resume of {} ", activityHint.getDescription());
}
});
}
protected boolean isCurrentActivity(ActivityHint activityHint) {
String currentActivityId = getCurrentActivityId();
if (hasCurrentActivity() && context.hasCurrentActivity()) {
return currentActivityId.equals(activityHint.getNextActivityId());
} else {
return false;
}
}
protected void resume(String id, boolean reload, Object returnHint, Runnable returnToRunnable, ActivityHint activityHint) {
context.startActivity(id);
log.info("Resuming activity {}", id);
DataSource<?> datasource = store.getDatasource();
datasource.setLoadingHint(returnHint);
ActivityCfg activityCfg = registeredActivities.get(id);
if (activityHint != null) {
activityCfg.setActivityHint(activityHint);
}
initialization.getControllers().forEach(eventBus::register);
select(activityCfg, activityCfg.getInitialController(), Navigator.MAIN_AREA);
initialization.getActivityCallbacks().forEach(ActivityCallback::onResume);
if (reload) {
reload();
store.waitForDataSource();
}
if (returnToRunnable != null) {
try {
returnToRunnable.run();
} catch (Exception e) {
log.error("Could not execute return to runnable", e);
}
}
log.info("Resumed activity {}", id);
}
protected void suspendCurrent() {
log.debug("Suspending activity {}", getCurrentActivityId());
initialization.getActivityCallbacks().forEach(ActivityCallback::onSuspend);
store.waitForDataSource();
shutdownExecutors();
//I want to cleanup the executors themselves, but during registering, I sadly don't know that it is a producer
//the cdi api doesn't provide that information :(
context.cleanupSingleBean(ActivityExecutorProducer.class);
context.cleanupSingleBean(ActivityJavaFXExecutorProducer.class);
initialization.getControllers().forEach((controller) -> eventBus.unregister(controller));
}
private void shutdownExecutors() {
executor.shutdownNow();
javaFXExecutor.shutdownNow();
try {
executor.awaitTermination(5, TimeUnit.SECONDS);
javaFXExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//
}
}
public void stopCurrent() {
if (getCurrentActivity().getActivityHint().getReturnToActivity() != null) {
stop(getCurrentActivityId(), false);
} else {
reload();
}
}
public void stop(String id, boolean wait) {
if (!registeredActivities.containsKey(id)) {
return;
}
Future<?> future = loadInExecutor("could not stop activity " + id, () -> {
try (LockSupport lockSupport = new LockSupport(lock)) {
showBusy();
log.debug("Stopping activity {}", id);
Object returnHint = null;
String returnToActivity = null;
Runnable returnToRunnable = null;
boolean refresh = true;
ActivityCfg activityCfg = registeredActivities.get(id);
if (activityCfg != null) {
ActivityHint activityHint = activityCfg.getActivityHint();
if (activityHint.getReturnToDatasourceHint() != null) {
returnHint = activityHint.getReturnToDatasourceHint().get();
}
returnToActivity = activityHint.getReturnToActivity();
returnToRunnable = activityHint.getReturnToRunnable();
refresh = activityHint.isRefreshOnReturn();
}
context.startActivity(id);
store.stop();
initialization.getActivityCallbacks().forEach(ActivityCallback::onStop);
initialization.getControllers().forEach((controller) -> eventBus.unregister(controller));
store.waitForDataSource();
shutdownExecutors();
registeredActivities.remove(id);
context.stopActivity(id);
log.debug("Stopped activity {}", id);
if (returnToActivity != null && registeredActivities.containsKey(returnToActivity)) {
resume(returnToActivity, refresh, returnHint, returnToRunnable, null);
}
} catch (Exception e) {
log.error("Could not stop activity {}", id, e);
}
});
if (wait) {
try {
future.get();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
protected void showBusy() {
StackPane container = new StackPane();
ProgressBar progress = new ProgressBar(-1);
progress.setPrefSize(300, 25);
progress.setMaxSize(Control.USE_PREF_SIZE, Control.USE_PREF_SIZE);
container.getChildren().add(progress);
navigator.get().presentInMain(container);
}
public void stopAll() {
HashSet<String> ids = new HashSet<>(registeredActivities.keySet());
ids.forEach(id -> stop(id, true));
}
protected Future<?> loadInExecutor(String errorMsg, Runnable runnable) {
Future<?> submit = loadingExecutor.submit(runnable);
if (!Platform.isFxApplicationThread()) {
try {
submit.get();
} catch (InterruptedException e) {
//
} catch (ExecutionException e) {
log.error(errorMsg, e);
throw new RuntimeException(e);
}
}
return submit;
}
public void select(ActivityCfg activityCfg, Class<?> targetController, String presentationArea) {
Node view = initialization.getViewForController(targetController);
navigator.get().present(presentationArea, view);
activityCfg.setCurrentController(targetController);
}
public void waitForTasks() {
while (!loadingExecutor.isShutdown() && loadingExecutor.getActiveCount() > 0) {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
log.trace("Got interrupted while waiting for tasks.", e);
}
}
if (context.hasCurrentActivity()) {
waitForDataSource();
executor.waitForAllTasksDone();
javaFXExecutor.waitForAllTasksDone();
}
}
public void waitForDataSource() {
store.waitForDataSource();
}
public ActivityCfg getCurrentActivity() {
String id = getCurrentActivityId();
if (id == null) {
return null;
} else {
return registeredActivities.get(id);
}
}
public boolean hasCurrentActivity() {
return getCurrentActivity() != null;
}
public String getCurrentActivityId() {
return context.getCurrentActivity();
}
public ActivityExecutor getExecutorService() {
return executor;
}
public ActivityJavaFXExecutor getJavaFXExecutor() {
return javaFXExecutor;
}
@SuppressWarnings("unchecked")
public <T extends Node> T getCurrentNode() {
Class<?> currentController = getCurrentActivity().getCurrentController();
return getNodeForController(currentController);
}
@SuppressWarnings("unchecked")
public <T extends Node> T getNodeForController(Class<?> controller) {
return (T) initialization.getViewForController(controller);
}
@SuppressWarnings("unchecked")
public <T> T getCurrentController() {
return (T) getControllerInstance(getCurrentActivity().getCurrentController());
}
@SuppressWarnings("unchecked")
public <T> T getControllerInstance(Class<T> controller) {
return (T) initialization.getControllerInstance(controller);
}
@SuppressWarnings("unchecked")
public void save() {
store.save();
}
@SuppressWarnings("unchecked")
public void reload() {
store.reload();
}
@PreDestroy
private void shutdown() {
stopAll();
loadingExecutor.shutdown();
try {
loadingExecutor.awaitTermination(5, TimeUnit.SECONDS);
} catch (InterruptedException e) {
//i dont care
}
}
}