/*
* Copyright 2008-2017 the original author or authors.
*
* 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.codehaus.griffon.runtime.core.controller;
import griffon.core.Configuration;
import griffon.core.Context;
import griffon.core.GriffonApplication;
import griffon.core.artifact.GriffonController;
import griffon.core.artifact.GriffonControllerClass;
import griffon.core.controller.AbortActionExecution;
import griffon.core.controller.Action;
import griffon.core.controller.ActionExecutionStatus;
import griffon.core.controller.ActionHandler;
import griffon.core.controller.ActionInterceptor;
import griffon.core.controller.ActionManager;
import griffon.core.i18n.MessageSource;
import griffon.core.i18n.NoSuchMessageException;
import griffon.core.mvc.MVCGroup;
import griffon.core.threading.UIThreadManager;
import griffon.exceptions.GriffonException;
import griffon.exceptions.InstanceMethodInvocationException;
import griffon.inject.Contextual;
import griffon.transform.Threading;
import griffon.util.AnnotationUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Named;
import java.lang.annotation.Annotation;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EventObject;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import static griffon.core.GriffonExceptionHandler.sanitize;
import static griffon.util.CollectionUtils.reverse;
import static griffon.util.GriffonClassUtils.EMPTY_ARGS;
import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
import static griffon.util.GriffonClassUtils.invokeInstanceMethod;
import static griffon.util.GriffonNameUtils.capitalize;
import static griffon.util.GriffonNameUtils.isBlank;
import static griffon.util.GriffonNameUtils.requireNonBlank;
import static griffon.util.GriffonNameUtils.uncapitalize;
import static griffon.util.TypeUtils.castToBoolean;
import static java.lang.reflect.Modifier.isPublic;
import static java.lang.reflect.Modifier.isStatic;
import static java.util.Objects.requireNonNull;
/**
* @author Andres Almiray
* @since 2.0.0
*/
public abstract class AbstractActionManager implements ActionManager {
private static final Logger LOG = LoggerFactory.getLogger(AbstractActionManager.class);
private static final String KEY_THREADING = "controller.threading";
private static final String KEY_THREADING_DEFAULT = "controller.threading.default";
private static final String KEY_DISABLE_THREADING_INJECTION = "griffon.disable.threading.injection";
private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
private static final String ERROR_ACTION_NAME_BLANK = "Argument 'actionName' must not be blank";
private static final String ERROR_ACTION_HANDLER_NULL = "Argument 'actionHandler' must not be null";
private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
private final ActionCache actionCache = new ActionCache();
private final Map<String, Threading.Policy> threadingPolicies = new ConcurrentHashMap<>();
private final List<ActionHandler> handlers = new CopyOnWriteArrayList<>();
private final GriffonApplication application;
@Inject
public AbstractActionManager(@Nonnull GriffonApplication application) {
this.application = requireNonNull(application, "Argument 'application' must not be null");
}
@Nullable
private static Method findActionAsMethod(@Nonnull GriffonController controller, @Nonnull String actionName) {
for (Method method : controller.getTypeClass().getMethods()) {
if (actionName.equals(method.getName()) &&
isPublic(method.getModifiers()) &&
!isStatic(method.getModifiers()) &&
method.getReturnType() == Void.TYPE) {
return method;
}
}
return null;
}
@Nonnull
protected Configuration getConfiguration() {
return application.getConfiguration();
}
@Nonnull
protected MessageSource getMessageSource() {
return application.getMessageSource();
}
@Nonnull
protected UIThreadManager getUiThreadManager() {
return application.getUIThreadManager();
}
@Nonnull
protected Map<String, Threading.Policy> getThreadingPolicies() {
return threadingPolicies;
}
@Nonnull
public Map<String, Action> actionsFor(@Nonnull GriffonController controller) {
requireNonNull(controller, ERROR_CONTROLLER_NULL);
Map<String, ActionWrapper> actions = actionCache.get(controller);
if (actions.isEmpty()) {
LOG.trace("No actions defined for controller {}", controller);
}
return Collections.<String, Action>unmodifiableMap(actions);
}
@Nullable
public Action actionFor(@Nonnull GriffonController controller, @Nonnull String actionName) {
requireNonNull(controller, ERROR_CONTROLLER_NULL);
requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
return actionCache.get(controller).get(normalizeName(actionName));
}
public void createActions(@Nonnull GriffonController controller) {
GriffonControllerClass griffonClass = (GriffonControllerClass) controller.getGriffonClass();
for (String actionName : griffonClass.getActionNames()) {
Method method = findActionAsMethod(controller, actionName);
if (method == null) {
throw new GriffonException(controller.getTypeClass().getCanonicalName() + " does not define an action named " + actionName);
}
ActionWrapper action = wrapAction(createAndConfigureAction(controller, actionName), method);
final String qualifiedActionName = action.getFullyQualifiedName();
for (ActionHandler handler : handlers) {
LOG.debug("Configuring action {} with {}", qualifiedActionName, handler);
handler.configure(action, method);
}
Map<String, ActionWrapper> actions = actionCache.get(controller);
if (actions.isEmpty()) {
actions = new TreeMap<>();
actionCache.set(controller, actions);
}
String actionKey = normalizeName(actionName);
LOG.trace("Action for {} stored as {}", qualifiedActionName, actionKey);
actions.put(actionKey, action);
}
}
@Nonnull
private ActionWrapper wrapAction(@Nonnull Action action, @Nonnull Method method) {
return new ActionWrapper(action, method);
}
@Override
public void updateActions() {
for (Action action : actionCache.allActions()) {
updateAction(action);
}
}
@Override
public void updateActions(@Nonnull GriffonController controller) {
for (Action action : actionsFor(controller).values()) {
updateAction(action);
}
}
@Override
public void updateAction(@Nonnull Action action) {
requireNonNull(action, ERROR_ACTION_NULL);
final String qualifiedActionName = action.getFullyQualifiedName();
for (ActionHandler handler : handlers) {
LOG.trace("Calling {}.update() on {}", handler, qualifiedActionName);
handler.update(action);
}
}
@Override
public void updateAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
requireNonNull(controller, ERROR_CONTROLLER_NULL);
requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
updateAction(actionFor(controller, actionName));
}
@Override
public void invokeAction(@Nonnull final Action action, @Nonnull final Object... args) {
requireNonNull(action, ERROR_ACTION_NULL);
final GriffonController controller = action.getController();
final String actionName = action.getActionName();
Runnable runnable = new Runnable() {
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public void run() {
Object[] updatedArgs = args;
List<ActionHandler> copy = new ArrayList<>(handlers);
List<ActionHandler> invokedHandlers = new ArrayList<>();
final String qualifiedActionName = action.getFullyQualifiedName();
ActionExecutionStatus status = ActionExecutionStatus.OK;
try {
LOG.trace("Resolving contextual arguments for " + qualifiedActionName);
updatedArgs = injectFromContext(action, updatedArgs);
} catch (IllegalStateException ise) {
LOG.debug("Execution of " + qualifiedActionName + " was aborted", ise);
throw ise;
}
if (LOG.isDebugEnabled()) {
int size = copy.size();
LOG.debug("Executing " + size + " handler" + (size != 1 ? "s" : "") + " for " + qualifiedActionName);
}
for (ActionHandler handler : copy) {
invokedHandlers.add(handler);
try {
LOG.trace("Calling {}.before() on {}", handler, qualifiedActionName);
updatedArgs = handler.before(action, updatedArgs);
} catch (AbortActionExecution aae) {
status = ActionExecutionStatus.ABORTED;
LOG.debug("Execution of {} was aborted by {}", qualifiedActionName, handler);
break;
}
}
LOG.trace("Status before execution of {} is {}", qualifiedActionName, status);
RuntimeException exception = null;
boolean exceptionWasHandled = false;
if (status == ActionExecutionStatus.OK) {
try {
doInvokeAction(controller, actionName, updatedArgs);
} catch (RuntimeException e) {
status = ActionExecutionStatus.EXCEPTION;
exception = (RuntimeException) sanitize(e);
LOG.warn("An exception occurred when executing {}", qualifiedActionName, exception);
}
LOG.trace("Status after execution of {} is {}", qualifiedActionName, status);
if (exception != null) {
for (ActionHandler handler : reverse(invokedHandlers)) {
LOG.trace("Calling {}.exception() on {}", handler, qualifiedActionName);
exceptionWasHandled = handler.exception(exception, action, updatedArgs);
}
}
}
for (ActionHandler handler : reverse(invokedHandlers)) {
LOG.trace("Calling {}.after() on {}", handler, qualifiedActionName);
handler.after(status, action, updatedArgs);
}
if (exception != null && !exceptionWasHandled) {
// throw it again
throw exception;
}
}
};
invokeAction(controller, actionName, runnable);
}
@Nonnull
private Object[] injectFromContext(@Nonnull Action action, @Nonnull Object[] args) {
ActionWrapper wrappedAction = null;
if (action instanceof ActionWrapper) {
wrappedAction = (ActionWrapper) action;
} else {
wrappedAction = wrapAction(action, findActionAsMethod(action.getController(), action.getActionName()));
}
MVCGroup group = action.getController().getMvcGroup();
if (group == null) {
// This case only occurs during testing, when an artifact is
// instantiated without a group
return args;
}
Context context = group.getContext();
if (wrappedAction.hasContextualArgs) {
Object[] newArgs = new Object[wrappedAction.argumentsInfo.size()];
for (int i = 0; i < newArgs.length; i++) {
ArgInfo argInfo = wrappedAction.argumentsInfo.get(i);
newArgs[i] = argInfo.contextual ? context.get(argInfo.name) : args[i];
if (argInfo.contextual && newArgs[i] != null) { context.put(argInfo.name, newArgs[i]); }
if (argInfo.contextual && !argInfo.nullable && newArgs[i] == null) {
throw new IllegalStateException("Could not find an instance of type " +
argInfo.type.getName() + " under key '" + argInfo.name +
"' in the context of MVCGroup[" + group.getMvcType() + ":" + group.getMvcId() +
"] to be injected as argument " + i +
" at " + action.getFullyQualifiedName() + "(). Argument does not accept null values.");
}
}
return newArgs;
}
return args;
}
public void invokeAction(@Nonnull final GriffonController controller, @Nonnull final String actionName, @Nonnull final Object... args) {
requireNonNull(controller, ERROR_CONTROLLER_NULL);
requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
invokeAction(actionFor(controller, actionName), args);
}
protected void doInvokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Object[] updatedArgs) {
try {
invokeInstanceMethod(controller, actionName, updatedArgs);
} catch (InstanceMethodInvocationException imie) {
if (imie.getCause() instanceof NoSuchMethodException) {
// try again but this time remove the 1st arg if it's
// descendant of java.util.EventObject
if (updatedArgs.length == 1 && updatedArgs[0] != null && EventObject.class.isAssignableFrom(updatedArgs[0].getClass())) {
invokeExactInstanceMethod(controller, actionName, EMPTY_ARGS);
} else {
throw imie;
}
} else {
throw imie;
}
}
}
private void invokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Runnable runnable) {
String fullQualifiedActionName = controller.getTypeClass().getName() + "." + actionName;
Threading.Policy policy = threadingPolicies.get(fullQualifiedActionName);
if (policy == null) {
if (isThreadingDisabled(fullQualifiedActionName)) {
policy = Threading.Policy.SKIP;
} else {
policy = resolveThreadingPolicy(controller, actionName);
}
threadingPolicies.put(fullQualifiedActionName, policy);
}
LOG.debug("Executing {} with policy {}", fullQualifiedActionName, policy);
switch (policy) {
case OUTSIDE_UITHREAD:
getUiThreadManager().runOutsideUI(runnable);
break;
case INSIDE_UITHREAD_SYNC:
getUiThreadManager().runInsideUISync(runnable);
break;
case INSIDE_UITHREAD_ASYNC:
getUiThreadManager().runInsideUIAsync(runnable);
break;
case SKIP:
default:
runnable.run();
}
}
@Nonnull
private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller, @Nonnull String actionName) {
Method method = findActionAsMethod(controller, actionName);
if (method != null) {
Threading annotation = method.getAnnotation(Threading.class);
return annotation == null ? resolveThreadingPolicy(controller) : annotation.value();
}
return Threading.Policy.OUTSIDE_UITHREAD;
}
@Nonnull
private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller) {
Threading annotation = AnnotationUtils.findAnnotation(controller.getTypeClass(), Threading.class);
return annotation == null ? resolveThreadingPolicy() : annotation.value();
}
@Nonnull
private Threading.Policy resolveThreadingPolicy() {
Object value = getConfiguration().get(KEY_THREADING_DEFAULT);
if (value == null) {
return Threading.Policy.OUTSIDE_UITHREAD;
}
if (value instanceof Threading.Policy) {
return (Threading.Policy) value;
}
String policy = String.valueOf(value).toLowerCase();
switch (policy) {
case "sync":
case "inside sync":
case "inside uithread sync":
case "inside_uithread_sync":
return Threading.Policy.INSIDE_UITHREAD_SYNC;
case "async":
case "inside async":
case "inside uithread async":
case "inside_uithread_async":
return Threading.Policy.INSIDE_UITHREAD_ASYNC;
case "outside":
case "outside uithread":
case "outside_uithread":
return Threading.Policy.OUTSIDE_UITHREAD;
case "skip":
return Threading.Policy.SKIP;
default:
throw new IllegalArgumentException("Value '" + policy + "' cannot be translated into " + Threading.Policy.class.getName());
}
}
private boolean isThreadingDisabled(@Nonnull String actionName) {
if (getConfiguration().getAsBoolean(KEY_DISABLE_THREADING_INJECTION, false)) {
return true;
}
Map<String, Object> settings = getConfiguration().asFlatMap();
String keyName = KEY_THREADING + "." + actionName;
while (!KEY_THREADING.equals(keyName)) {
Object value = settings.get(keyName);
keyName = keyName.substring(0, keyName.lastIndexOf("."));
if (value != null && !castToBoolean(value)) { return true; }
}
return false;
}
public void addActionHandler(@Nonnull ActionHandler actionHandler) {
requireNonNull(actionHandler, ERROR_ACTION_HANDLER_NULL);
if (handlers.contains(actionHandler)) {
return;
}
handlers.add(actionHandler);
}
public void addActionInterceptor(@Nonnull ActionInterceptor actionInterceptor) {
throw new UnsupportedOperationException(ActionInterceptor.class.getName() + " has been deprecated and is no longer supported");
}
@Nonnull
protected Action createAndConfigureAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
requireNonNull(controller, ERROR_CONTROLLER_NULL);
requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
Action action = createControllerAction(controller, actionName);
String normalizeNamed = capitalize(normalizeName(actionName));
String keyPrefix = controller.getTypeClass().getName() + ".action.";
doConfigureAction(action, controller, normalizeNamed, keyPrefix);
action.initialize();
return action;
}
protected abstract void doConfigureAction(@Nonnull Action action, @Nonnull GriffonController controller, @Nonnull String normalizeNamed, @Nonnull String keyPrefix);
@Nonnull
protected abstract Action createControllerAction(@Nonnull GriffonController controller, @Nonnull String actionName);
@Nonnull
public String normalizeName(@Nonnull String actionName) {
requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
if (actionName.endsWith(ACTION)) {
actionName = actionName.substring(0, actionName.length() - ACTION.length());
}
return uncapitalize(actionName);
}
@Nullable
protected String msg(@Nonnull String key, @Nonnull String actionName, @Nonnull String subkey, @Nullable String defaultValue) {
try {
return getMessageSource().getMessage(key + actionName + "." + subkey, application.getLocale());
} catch (NoSuchMessageException nsme) {
return getMessageSource().getMessage("application.action." + actionName + "." + subkey, application.getLocale(), defaultValue);
}
}
private static class ActionWrapper extends ActionDecorator {
private final List<ArgInfo> argumentsInfo = new ArrayList<>();
private boolean hasContextualArgs;
public ActionWrapper(@Nonnull Action delegate, @Nonnull Method method) {
super(delegate);
Class<?>[] parameterTypes = method.getParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
hasContextualArgs = method.getAnnotation(Contextual.class) != null;
for (int i = 0; i < parameterTypes.length; i++) {
ArgInfo argInfo = new ArgInfo();
argInfo.type = parameterTypes[i];
argInfo.name = argInfo.type.getCanonicalName();
Annotation[] annotations = parameterAnnotations[i];
if (annotations != null) {
for (Annotation annotation : annotations) {
if (Contextual.class.isAssignableFrom(annotation.annotationType())) {
hasContextualArgs = true;
argInfo.contextual = true;
}
if (Nonnull.class.isAssignableFrom(annotation.annotationType())) {
argInfo.nullable = false;
}
if (Named.class.isAssignableFrom(annotation.annotationType())) {
Named named = (Named) annotation;
if (!isBlank(named.value())) {
argInfo.name = named.value();
}
}
}
}
argumentsInfo.add(argInfo);
}
}
}
private static class ArgInfo {
private Class<?> type;
private String name;
private boolean nullable = true;
private boolean contextual = false;
}
private static class ActionCache {
private final Map<WeakReference<GriffonController>, Map<String, ActionWrapper>> cache = new ConcurrentHashMap<>();
@Nonnull
public Map<String, ActionWrapper> get(@Nonnull GriffonController controller) {
synchronized (cache) {
for (Map.Entry<WeakReference<GriffonController>, Map<String, ActionWrapper>> entry : cache.entrySet()) {
GriffonController test = entry.getKey().get();
if (test == controller) {
return entry.getValue();
}
}
}
return Collections.emptyMap();
}
public void set(@Nonnull GriffonController controller, @Nonnull Map<String, ActionWrapper> actions) {
WeakReference<GriffonController> existingController = null;
synchronized (cache) {
for (WeakReference<GriffonController> key : cache.keySet()) {
if (key.get() == controller) {
existingController = key;
break;
}
}
}
if (null != existingController) {
cache.remove(existingController);
}
cache.put(new WeakReference<>(controller), actions);
}
public Collection<Action> allActions() {
// create a copy to avoid CME
List<Action> actions = new ArrayList<>();
synchronized (cache) {
for (Map<String, ActionWrapper> map : cache.values()) {
actions.addAll(map.values());
}
}
return actions;
}
}
}