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