AbstractActionManager.java
001 /*
002  * Copyright 2008-2017 the original author or authors.
003  *
004  * Licensed under the Apache License, Version 2.0 (the "License");
005  * you may not use this file except in compliance with the License.
006  * You may obtain a copy of the License at
007  *
008  *     http://www.apache.org/licenses/LICENSE-2.0
009  *
010  * Unless required by applicable law or agreed to in writing, software
011  * distributed under the License is distributed on an "AS IS" BASIS,
012  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013  * See the License for the specific language governing permissions and
014  * limitations under the License.
015  */
016 package org.codehaus.griffon.runtime.core.controller;
017 
018 import griffon.core.Configuration;
019 import griffon.core.Context;
020 import griffon.core.GriffonApplication;
021 import griffon.core.artifact.GriffonController;
022 import griffon.core.artifact.GriffonControllerClass;
023 import griffon.core.controller.AbortActionExecution;
024 import griffon.core.controller.Action;
025 import griffon.core.controller.ActionExecutionStatus;
026 import griffon.core.controller.ActionFactory;
027 import griffon.core.controller.ActionHandler;
028 import griffon.core.controller.ActionInterceptor;
029 import griffon.core.controller.ActionManager;
030 import griffon.core.controller.ActionMetadata;
031 import griffon.core.controller.ActionMetadataFactory;
032 import griffon.core.controller.ActionParameter;
033 import griffon.core.controller.ControllerAction;
034 import griffon.core.i18n.MessageSource;
035 import griffon.core.i18n.NoSuchMessageException;
036 import griffon.core.mvc.MVCGroup;
037 import griffon.core.threading.UIThreadManager;
038 import griffon.exceptions.GriffonException;
039 import griffon.exceptions.InstanceMethodInvocationException;
040 import griffon.transform.Threading;
041 import org.slf4j.Logger;
042 import org.slf4j.LoggerFactory;
043 
044 import javax.annotation.Nonnull;
045 import javax.annotation.Nullable;
046 import javax.inject.Inject;
047 import java.lang.ref.WeakReference;
048 import java.lang.reflect.Method;
049 import java.util.ArrayList;
050 import java.util.Collection;
051 import java.util.Collections;
052 import java.util.EventObject;
053 import java.util.List;
054 import java.util.Map;
055 import java.util.TreeMap;
056 import java.util.concurrent.ConcurrentHashMap;
057 import java.util.concurrent.CopyOnWriteArrayList;
058 
059 import static griffon.core.GriffonExceptionHandler.sanitize;
060 import static griffon.util.AnnotationUtils.findAnnotation;
061 import static griffon.util.AnnotationUtils.isAnnotatedWith;
062 import static griffon.util.CollectionUtils.reverse;
063 import static griffon.util.GriffonClassUtils.EMPTY_ARGS;
064 import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
065 import static griffon.util.GriffonClassUtils.invokeInstanceMethod;
066 import static griffon.util.GriffonNameUtils.capitalize;
067 import static griffon.util.GriffonNameUtils.requireNonBlank;
068 import static griffon.util.GriffonNameUtils.uncapitalize;
069 import static griffon.util.TypeUtils.castToBoolean;
070 import static java.lang.reflect.Modifier.isPublic;
071 import static java.lang.reflect.Modifier.isStatic;
072 import static java.util.Objects.requireNonNull;
073 
074 /**
075  @author Andres Almiray
076  @since 2.0.0
077  */
078 public abstract class AbstractActionManager implements ActionManager {
079     private static final Logger LOG = LoggerFactory.getLogger(AbstractActionManager.class);
080 
081     private static final String KEY_THREADING = "controller.threading";
082     private static final String KEY_THREADING_DEFAULT = "controller.threading.default";
083     private static final String KEY_DISABLE_THREADING_INJECTION = "griffon.disable.threading.injection";
084     private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
085     private static final String ERROR_ACTION_NAME_BLANK = "Argument 'actionName' must not be blank";
086     private static final String ERROR_ACTION_HANDLER_NULL = "Argument 'actionHandler' must not be null";
087     private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
088     private static final String ERROR_METHOD_NULL = "Argument 'method' must not be null";
089 
090     private final ActionCache actionCache = new ActionCache();
091     private final Map<String, Threading.Policy> threadingPolicies = new ConcurrentHashMap<>();
092     private final List<ActionHandler> handlers = new CopyOnWriteArrayList<>();
093 
094     private final GriffonApplication application;
095     private final ActionFactory actionFactory;
096     private final ActionMetadataFactory actionMetadataFactory;
097 
098     @Inject
099     public AbstractActionManager(@Nonnull GriffonApplication application, @Nonnull ActionFactory actionFactory, @Nonnull ActionMetadataFactory actionMetadataFactory) {
100         this.application = requireNonNull(application, "Argument 'application' must not be null");
101         this.actionFactory = requireNonNull(actionFactory, "Argument 'actionFactory' must not be null");
102         this.actionMetadataFactory = requireNonNull(actionMetadataFactory, "Argument 'actionMetadataFactory' must not be null");
103     }
104 
105     @Nullable
106     private static Method findActionAsMethod(@Nonnull GriffonController controller, @Nonnull String actionName) {
107         for (Method method : controller.getTypeClass().getMethods()) {
108             if (actionName.equals(method.getName()) &&
109                 isPublic(method.getModifiers()) &&
110                 !isStatic(method.getModifiers()) &&
111                 (isAnnotatedWith(method, ControllerAction.class, true|| method.getReturnType() == Void.TYPE)) {
112                 return method;
113             }
114         }
115         return null;
116     }
117 
118     @Nonnull
119     protected Configuration getConfiguration() {
120         return application.getConfiguration();
121     }
122 
123     @Nonnull
124     protected MessageSource getMessageSource() {
125         return application.getMessageSource();
126     }
127 
128     @Nonnull
129     protected UIThreadManager getUiThreadManager() {
130         return application.getUIThreadManager();
131     }
132 
133     @Nonnull
134     protected Map<String, Threading.Policy> getThreadingPolicies() {
135         return threadingPolicies;
136     }
137 
138     @Nonnull
139     public Map<String, Action> actionsFor(@Nonnull GriffonController controller) {
140         requireNonNull(controller, ERROR_CONTROLLER_NULL);
141         Map<String, Action> actions = actionCache.get(controller);
142         if (actions.isEmpty()) {
143             LOG.trace("No actions defined for controller {}", controller);
144         }
145         return Collections.<String, Action>unmodifiableMap(actions);
146     }
147 
148     @Nullable
149     public Action actionFor(@Nonnull GriffonController controller, @Nonnull String actionName) {
150         requireNonNull(controller, ERROR_CONTROLLER_NULL);
151         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
152         return actionCache.get(controller).get(normalizeName(actionName));
153     }
154 
155     public void createActions(@Nonnull GriffonController controller) {
156         GriffonControllerClass griffonClass = (GriffonControllerClasscontroller.getGriffonClass();
157         for (String actionName : griffonClass.getActionNames()) {
158             Method method = findActionAsMethod(controller, actionName);
159             if (method == null) {
160                 throw new GriffonException(controller.getTypeClass().getCanonicalName() " does not define an action named " + actionName);
161             }
162 
163             Action action = createAndConfigureAction(controller, actionName, method);
164 
165             final String qualifiedActionName = action.getFullyQualifiedName();
166             for (ActionHandler handler : handlers) {
167                 LOG.debug("Configuring action {} with {}", qualifiedActionName, handler);
168                 handler.configure(action, method);
169             }
170 
171             Map<String, Action> actions = actionCache.get(controller);
172             if (actions.isEmpty()) {
173                 actions = new TreeMap<>();
174                 actionCache.set(controller, actions);
175             }
176             String actionKey = normalizeName(actionName);
177             LOG.trace("Action for {} stored as {}", qualifiedActionName, actionKey);
178             actions.put(actionKey, action);
179         }
180     }
181 
182     @Override
183     public void updateActions() {
184         for (Action action : actionCache.allActions()) {
185             updateAction(action);
186         }
187     }
188 
189     @Override
190     public void updateActions(@Nonnull GriffonController controller) {
191         for (Action action : actionsFor(controller).values()) {
192             updateAction(action);
193         }
194     }
195 
196     @Override
197     public void updateAction(@Nonnull Action action) {
198         requireNonNull(action, ERROR_ACTION_NULL);
199 
200         final String qualifiedActionName = action.getFullyQualifiedName();
201         for (ActionHandler handler : handlers) {
202             LOG.trace("Calling {}.update() on {}", handler, qualifiedActionName);
203             handler.update(action);
204         }
205     }
206 
207     @Override
208     public void updateAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
209         requireNonNull(controller, ERROR_CONTROLLER_NULL);
210         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
211         updateAction(actionFor(controller, actionName));
212     }
213 
214     @Override
215     public void invokeAction(@Nonnull final Action action, @Nonnull final Object... args) {
216         requireNonNull(action, ERROR_ACTION_NULL);
217         final GriffonController controller = action.getController();
218         final String actionName = action.getActionName();
219         Runnable runnable = new Runnable() {
220             @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
221             public void run() {
222                 Object result = null;
223                 Object[] updatedArgs = args;
224                 List<ActionHandler> copy = new ArrayList<>(handlers);
225                 List<ActionHandler> invokedHandlers = new ArrayList<>();
226 
227                 final String qualifiedActionName = action.getFullyQualifiedName();
228                 ActionExecutionStatus status = ActionExecutionStatus.OK;
229 
230                 try {
231                     LOG.trace("Resolving contextual arguments for " + qualifiedActionName);
232                     updatedArgs = injectFromContext(action, updatedArgs);
233                 catch (IllegalStateException ise) {
234                     LOG.debug("Execution of " + qualifiedActionName + " was aborted", ise);
235                     throw ise;
236                 }
237 
238                 if (LOG.isDebugEnabled()) {
239                     int size = copy.size();
240                     LOG.debug("Executing " + size + " handler" (size != "s" """ for " + qualifiedActionName);
241                 }
242 
243                 for (ActionHandler handler : copy) {
244                     invokedHandlers.add(handler);
245                     try {
246                         LOG.trace("Calling {}.before() on {}", handler, qualifiedActionName);
247                         updatedArgs = handler.before(action, updatedArgs);
248                     catch (AbortActionExecution aae) {
249                         status = ActionExecutionStatus.ABORTED;
250                         LOG.debug("Execution of {} was aborted by {}", qualifiedActionName, handler);
251                         break;
252                     }
253                 }
254 
255                 LOG.trace("Status before execution of {} is {}", qualifiedActionName, status);
256                 RuntimeException exception = null;
257                 boolean exceptionWasHandled = false;
258                 if (status == ActionExecutionStatus.OK) {
259                     try {
260                         result = doInvokeAction(controller, actionName, updatedArgs);
261                     catch (RuntimeException e) {
262                         status = ActionExecutionStatus.EXCEPTION;
263                         exception = (RuntimeExceptionsanitize(e);
264                         LOG.warn("An exception occurred when executing {}", qualifiedActionName, exception);
265                     }
266                     LOG.trace("Status after execution of {} is {}", qualifiedActionName, status);
267 
268                     if (exception != null) {
269                         for (ActionHandler handler : reverse(invokedHandlers)) {
270                             LOG.trace("Calling {}.exception() on {}", handler, qualifiedActionName);
271                             exceptionWasHandled = handler.exception(exception, action, updatedArgs);
272                         }
273                     }
274                 }
275 
276                 for (ActionHandler handler : reverse(invokedHandlers)) {
277                     LOG.trace("Calling {}.after() on {}", handler, qualifiedActionName);
278                     result = handler.after(status, action, updatedArgs, result);
279                 }
280 
281                 if (exception != null && !exceptionWasHandled) {
282                     // throw it again
283                     throw exception;
284                 }
285             }
286         };
287         invokeAction(controller, actionName, runnable);
288     }
289 
290     @Nonnull
291     private Object[] injectFromContext(@Nonnull Action action, @Nonnull Object[] args) {
292         MVCGroup group = action.getController().getMvcGroup();
293         if (group == null) {
294             // This case only occurs during testing, when an artifact is
295             // instantiated without a group
296             return args;
297         }
298 
299         Context context = group.getContext();
300         ActionMetadata actionMetadata = action.getActionMetadata();
301         if (actionMetadata.hasContextualArgs()) {
302             Object[] newArgs = new Object[actionMetadata.getParameters().length];
303             for (int i = 0; i < newArgs.length; i++) {
304                 ActionParameter param = actionMetadata.getParameters()[i];
305                 newArgs[i= param.isContextual() ? context.get(param.getName()) : args[i];
306                 if (param.isContextual() && newArgs[i!= null) { context.put(param.getName(), newArgs[i])}
307                 if (param.isContextual() && !param.isNullable() && newArgs[i== null) {
308                     throw new IllegalStateException("Could not find an instance of type " +
309                         param.getType().getName() " under key '" + param.getName() +
310                         "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
311                         "] to be injected as argument " + i +
312                         " at " + action.getFullyQualifiedName() "(). Argument does not accept null values.");
313                 }
314             }
315             return newArgs;
316         }
317 
318         return args;
319     }
320 
321     public void invokeAction(@Nonnull final GriffonController controller, @Nonnull final String actionName, @Nonnull final Object... args) {
322         requireNonNull(controller, ERROR_CONTROLLER_NULL);
323         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
324         invokeAction(actionFor(controller, actionName), args);
325     }
326 
327     @Nullable
328     protected Object doInvokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Object[] updatedArgs) {
329         try {
330             return invokeInstanceMethod(controller, actionName, updatedArgs);
331         catch (InstanceMethodInvocationException imie) {
332             if (imie.getCause() instanceof NoSuchMethodException) {
333                 // try again but this time remove the 1st arg if it's
334                 // descendant of java.util.EventObject
335                 if (updatedArgs.length == && updatedArgs[0!= null && EventObject.class.isAssignableFrom(updatedArgs[0].getClass())) {
336                     return invokeExactInstanceMethod(controller, actionName, EMPTY_ARGS);
337                 else {
338                     throw imie;
339                 }
340             else {
341                 throw imie;
342             }
343         }
344     }
345 
346     private void invokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Runnable runnable) {
347         String fullQualifiedActionName = controller.getTypeClass().getName() "." + actionName;
348         Threading.Policy policy = threadingPolicies.get(fullQualifiedActionName);
349         if (policy == null) {
350             if (isThreadingDisabled(fullQualifiedActionName)) {
351                 policy = Threading.Policy.SKIP;
352             else {
353                 policy = resolveThreadingPolicy(controller, actionName);
354             }
355             threadingPolicies.put(fullQualifiedActionName, policy);
356         }
357 
358         LOG.debug("Executing {} with policy {}", fullQualifiedActionName, policy);
359 
360         switch (policy) {
361             case OUTSIDE_UITHREAD:
362                 getUiThreadManager().runOutsideUI(runnable);
363                 break;
364             case OUTSIDE_UITHREAD_ASYNC:
365                 getUiThreadManager().runOutsideUIAsync(runnable);
366                 break;
367             case INSIDE_UITHREAD_SYNC:
368                 getUiThreadManager().runInsideUISync(runnable);
369                 break;
370             case INSIDE_UITHREAD_ASYNC:
371                 getUiThreadManager().runInsideUIAsync(runnable);
372                 break;
373             case SKIP:
374             default:
375                 runnable.run();
376         }
377     }
378 
379     @Nonnull
380     protected Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller, @Nonnull String actionName) {
381         Method method = findActionAsMethod(controller, actionName);
382         if (method != null) {
383             Threading annotation = findAnnotation(method, Threading.class, true);
384             return annotation == null ? resolveThreadingPolicy(controller: annotation.value();
385         }
386 
387         return Threading.Policy.OUTSIDE_UITHREAD;
388     }
389 
390     @Nonnull
391     protected Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller) {
392         Threading annotation = findAnnotation(controller.getTypeClass(), Threading.class, true);
393         return annotation == null ? resolveThreadingPolicy() : annotation.value();
394     }
395 
396     @Nonnull
397     protected Threading.Policy resolveThreadingPolicy() {
398         Object value = getConfiguration().get(KEY_THREADING_DEFAULT);
399         if (value == null) {
400             return Threading.Policy.OUTSIDE_UITHREAD;
401         }
402 
403         if (value instanceof Threading.Policy) {
404             return (Threading.Policyvalue;
405         }
406 
407         String policy = String.valueOf(value).toLowerCase();
408         switch (policy) {
409             case "sync":
410             case "inside sync":
411             case "inside uithread sync":
412             case "inside_uithread_sync":
413                 return Threading.Policy.INSIDE_UITHREAD_SYNC;
414             case "async":
415             case "inside async":
416             case "inside uithread async":
417             case "inside_uithread_async":
418                 return Threading.Policy.INSIDE_UITHREAD_ASYNC;
419             case "outside":
420             case "outside uithread":
421             case "outside_uithread":
422                 return Threading.Policy.OUTSIDE_UITHREAD;
423             case "background":
424             case "outside async":
425             case "outside uithread async":
426             case "outside_uithread_async":
427                 return Threading.Policy.OUTSIDE_UITHREAD_ASYNC;
428             case "skip":
429                 return Threading.Policy.SKIP;
430             default:
431                 throw new IllegalArgumentException("Value '" + policy + "' cannot be translated into " + Threading.Policy.class.getName());
432         }
433     }
434 
435     protected boolean isThreadingDisabled(@Nonnull String actionName) {
436         if (getConfiguration().getAsBoolean(KEY_DISABLE_THREADING_INJECTION, false)) {
437             return true;
438         }
439 
440         Map<String, Object> settings = getConfiguration().asFlatMap();
441 
442         String keyName = KEY_THREADING + "." + actionName;
443         while (!KEY_THREADING.equals(keyName)) {
444             Object value = settings.get(keyName);
445             keyName = keyName.substring(0, keyName.lastIndexOf('.'));
446             if (value != null && !castToBoolean(value)) { return true}
447         }
448 
449         return false;
450     }
451 
452     public void addActionHandler(@Nonnull ActionHandler actionHandler) {
453         requireNonNull(actionHandler, ERROR_ACTION_HANDLER_NULL);
454         if (handlers.contains(actionHandler)) {
455             return;
456         }
457         handlers.add(actionHandler);
458     }
459 
460     public void addActionInterceptor(@Nonnull ActionInterceptor actionInterceptor) {
461         throw new UnsupportedOperationException(ActionInterceptor.class.getName() " has been deprecated and is no longer supported");
462     }
463 
464     @Nonnull
465     protected Action createAndConfigureAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Method method) {
466         requireNonNull(controller, ERROR_CONTROLLER_NULL);
467         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
468         requireNonNull(method, ERROR_METHOD_NULL);
469 
470         Action action = createControllerAction(controller, actionName, method);
471 
472         String normalizeNamed = capitalize(normalizeName(actionName));
473         String keyPrefix = controller.getTypeClass().getName() ".action.";
474 
475         doConfigureAction(action, controller, normalizeNamed, keyPrefix);
476 
477         action.initialize();
478 
479         return action;
480     }
481 
482     protected abstract void doConfigureAction(@Nonnull Action action, @Nonnull GriffonController controller, @Nonnull String normalizeNamed, @Nonnull String keyPrefix);
483 
484     @Nonnull
485     protected Action createControllerAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Method method) {
486         ActionMetadata actionMetadata = actionMetadataFactory.create(controller, actionName, method);
487         return actionFactory.create(controller, actionMetadata);
488     }
489 
490     @Nonnull
491     public String normalizeName(@Nonnull String actionName) {
492         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
493         if (actionName.endsWith(ACTION)) {
494             actionName = actionName.substring(0, actionName.length() - ACTION.length());
495         }
496         return uncapitalize(actionName);
497     }
498 
499     @Nullable
500     protected String msg(@Nonnull String key, @Nonnull String actionName, @Nonnull String subkey, @Nullable String defaultValue) {
501         try {
502             return getMessageSource().getMessage(key + actionName + "." + subkey, application.getLocale());
503         catch (NoSuchMessageException nsme) {
504             return getMessageSource().getMessage("application.action." + actionName + "." + subkey, application.getLocale(), defaultValue);
505         }
506     }
507 
508     private static class ActionCache {
509         private final Map<WeakReference<GriffonController>, Map<String, Action>> cache = new ConcurrentHashMap<>();
510 
511         @Nonnull
512         public Map<String, Action> get(@Nonnull GriffonController controller) {
513             synchronized (cache) {
514                 for (Map.Entry<WeakReference<GriffonController>, Map<String, Action>> entry : cache.entrySet()) {
515                     GriffonController test = entry.getKey().get();
516                     if (test == controller) {
517                         return entry.getValue();
518                     }
519                 }
520             }
521             return Collections.emptyMap();
522         }
523 
524         public void set(@Nonnull GriffonController controller, @Nonnull Map<String, Action> actions) {
525             WeakReference<GriffonController> existingController = null;
526             synchronized (cache) {
527                 for (WeakReference<GriffonController> key : cache.keySet()) {
528                     if (key.get() == controller) {
529                         existingController = key;
530                         break;
531                     }
532                 }
533             }
534 
535             if (null != existingController) {
536                 cache.remove(existingController);
537             }
538 
539             cache.put(new WeakReference<>(controller), actions);
540         }
541 
542         public Collection<Action> allActions() {
543             // create a copy to avoid CME
544             List<Action> actions = new ArrayList<>();
545 
546             synchronized (cache) {
547                 for (Map<String, Action> map : cache.values()) {
548                     actions.addAll(map.values());
549                 }
550             }
551 
552             return actions;
553         }
554     }
555 }