AbstractActionManager.java
001 /*
002  * Copyright 2008-2016 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.ActionHandler;
027 import griffon.core.controller.ActionInterceptor;
028 import griffon.core.controller.ActionManager;
029 import griffon.core.i18n.MessageSource;
030 import griffon.core.i18n.NoSuchMessageException;
031 import griffon.core.mvc.MVCGroup;
032 import griffon.core.threading.UIThreadManager;
033 import griffon.exceptions.GriffonException;
034 import griffon.exceptions.InstanceMethodInvocationException;
035 import griffon.inject.Contextual;
036 import griffon.transform.Threading;
037 import griffon.util.AnnotationUtils;
038 import org.slf4j.Logger;
039 import org.slf4j.LoggerFactory;
040 
041 import javax.annotation.Nonnull;
042 import javax.annotation.Nullable;
043 import javax.inject.Inject;
044 import javax.inject.Named;
045 import java.lang.annotation.Annotation;
046 import java.lang.ref.WeakReference;
047 import java.lang.reflect.Method;
048 import java.util.ArrayList;
049 import java.util.Collection;
050 import java.util.Collections;
051 import java.util.EventObject;
052 import java.util.List;
053 import java.util.Map;
054 import java.util.TreeMap;
055 import java.util.concurrent.ConcurrentHashMap;
056 import java.util.concurrent.CopyOnWriteArrayList;
057 
058 import static griffon.core.GriffonExceptionHandler.sanitize;
059 import static griffon.util.CollectionUtils.reverse;
060 import static griffon.util.GriffonClassUtils.EMPTY_ARGS;
061 import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
062 import static griffon.util.GriffonClassUtils.invokeInstanceMethod;
063 import static griffon.util.GriffonNameUtils.capitalize;
064 import static griffon.util.GriffonNameUtils.getNaturalName;
065 import static griffon.util.GriffonNameUtils.isBlank;
066 import static griffon.util.GriffonNameUtils.requireNonBlank;
067 import static griffon.util.GriffonNameUtils.uncapitalize;
068 import static griffon.util.TypeUtils.castToBoolean;
069 import static java.lang.reflect.Modifier.isPublic;
070 import static java.lang.reflect.Modifier.isStatic;
071 import static java.util.Objects.requireNonNull;
072 
073 /**
074  @author Andres Almiray
075  @since 2.0.0
076  */
077 public abstract class AbstractActionManager implements ActionManager {
078     private static final Logger LOG = LoggerFactory.getLogger(AbstractActionManager.class);
079 
080     private static final String KEY_THREADING = "controller.threading";
081     private static final String KEY_THREADING_DEFAULT = "controller.threading.default";
082     private static final String KEY_DISABLE_THREADING_INJECTION = "griffon.disable.threading.injection";
083     private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
084     private static final String ERROR_ACTION_NAME_BLANK = "Argument 'actionName' must not be blank";
085     private static final String ERROR_ACTION_HANDLER_NULL = "Argument 'actionHandler' must not be null";
086     private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
087     private final ActionCache actionCache = new ActionCache();
088     private final Map<String, Threading.Policy> threadingPolicies = new ConcurrentHashMap<>();
089     private final List<ActionHandler> handlers = new CopyOnWriteArrayList<>();
090 
091     private final GriffonApplication application;
092 
093     @Inject
094     public AbstractActionManager(@Nonnull GriffonApplication application) {
095         this.application = requireNonNull(application, "Argument 'application' must not be null");
096     }
097 
098     @Nullable
099     private static Method findActionAsMethod(@Nonnull GriffonController controller, @Nonnull String actionName) {
100         for (Method method : controller.getClass().getMethods()) {
101             if (actionName.equals(method.getName()) &&
102                 isPublic(method.getModifiers()) &&
103                 !isStatic(method.getModifiers()) &&
104                 method.getReturnType() == Void.TYPE) {
105                 return method;
106             }
107         }
108         return null;
109     }
110 
111     @Nonnull
112     protected Configuration getConfiguration() {
113         return application.getConfiguration();
114     }
115 
116     @Nonnull
117     protected MessageSource getMessageSource() {
118         return application.getMessageSource();
119     }
120 
121     @Nonnull
122     protected UIThreadManager getUiThreadManager() {
123         return application.getUIThreadManager();
124     }
125 
126     @Nonnull
127     protected Map<String, Threading.Policy> getThreadingPolicies() {
128         return threadingPolicies;
129     }
130 
131     @Nonnull
132     public Map<String, Action> actionsFor(@Nonnull GriffonController controller) {
133         requireNonNull(controller, ERROR_CONTROLLER_NULL);
134         Map<String, ActionWrapper> actions = actionCache.get(controller);
135         if (actions.isEmpty()) {
136             LOG.trace("No actions defined for controller {}", controller);
137         }
138         return Collections.<String, Action>unmodifiableMap(actions);
139     }
140 
141     @Nullable
142     public Action actionFor(@Nonnull GriffonController controller, @Nonnull String actionName) {
143         requireNonNull(controller, ERROR_CONTROLLER_NULL);
144         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
145         return actionCache.get(controller).get(normalizeName(actionName));
146     }
147 
148     public void createActions(@Nonnull GriffonController controller) {
149         GriffonControllerClass griffonClass = (GriffonControllerClasscontroller.getGriffonClass();
150         for (String actionName : griffonClass.getActionNames()) {
151             Method method = findActionAsMethod(controller, actionName);
152             if (method == null) {
153                 throw new GriffonException(controller.getClass().getCanonicalName() " does not define an action named " + actionName);
154             }
155 
156             ActionWrapper action = wrapAction(createAndConfigureAction(controller, actionName), method);
157 
158             final String qualifiedActionName = action.getFullyQualifiedName();
159             for (ActionHandler handler : handlers) {
160                 LOG.debug("Configuring action {} with {}", qualifiedActionName, handler);
161                 handler.configure(action, method);
162             }
163 
164             Map<String, ActionWrapper> actions = actionCache.get(controller);
165             if (actions.isEmpty()) {
166                 actions = new TreeMap<>();
167                 actionCache.set(controller, actions);
168             }
169             String actionKey = normalizeName(actionName);
170             LOG.trace("Action for {} stored as {}", qualifiedActionName, actionKey);
171             actions.put(actionKey, action);
172         }
173     }
174 
175     @Nonnull
176     private ActionWrapper wrapAction(@Nonnull Action action, @Nonnull Method method) {
177         return new ActionWrapper(action, method);
178     }
179 
180     @Override
181     public void updateActions() {
182         for (Action action : actionCache.allActions()) {
183             updateAction(action);
184         }
185     }
186 
187     @Override
188     public void updateActions(@Nonnull GriffonController controller) {
189         for (Action action : actionsFor(controller).values()) {
190             updateAction(action);
191         }
192     }
193 
194     @Override
195     public void updateAction(@Nonnull Action action) {
196         requireNonNull(action, ERROR_ACTION_NULL);
197 
198         final String qualifiedActionName = action.getFullyQualifiedName();
199         for (ActionHandler handler : handlers) {
200             LOG.trace("Calling {}.update() on {}", handler, qualifiedActionName);
201             handler.update(action);
202         }
203     }
204 
205     @Override
206     public void updateAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
207         requireNonNull(controller, ERROR_CONTROLLER_NULL);
208         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
209         updateAction(actionFor(controller, actionName));
210     }
211 
212     @Override
213     public void invokeAction(@Nonnull final Action action, @Nonnull final Object... args) {
214         requireNonNull(action, ERROR_ACTION_NULL);
215         final GriffonController controller = action.getController();
216         final String actionName = action.getActionName();
217         Runnable runnable = new Runnable() {
218             @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
219             public void run() {
220                 Object[] updatedArgs = args;
221                 List<ActionHandler> copy = new ArrayList<>(handlers);
222                 List<ActionHandler> invokedHandlers = new ArrayList<>();
223 
224                 final String qualifiedActionName = action.getFullyQualifiedName();
225                 ActionExecutionStatus status = ActionExecutionStatus.OK;
226 
227                 try {
228                     LOG.trace("Resolving contextual arguments for " + qualifiedActionName);
229                     updatedArgs = injectFromContext(action, updatedArgs);
230                 catch (IllegalStateException ise) {
231                     LOG.debug("Execution of " + qualifiedActionName + " was aborted", ise);
232                     throw ise;
233                 }
234 
235                 if (LOG.isDebugEnabled()) {
236                     int size = copy.size();
237                     LOG.debug("Executing " + size + " handler" (size != "s" """ for " + qualifiedActionName);
238                 }
239 
240                 for (ActionHandler handler : copy) {
241                     invokedHandlers.add(handler);
242                     try {
243                         LOG.trace("Calling {}.before() on {}", handler, qualifiedActionName);
244                         updatedArgs = handler.before(action, updatedArgs);
245                     catch (AbortActionExecution aae) {
246                         status = ActionExecutionStatus.ABORTED;
247                         LOG.debug("Execution of {} was aborted by {}", qualifiedActionName, handler);
248                         break;
249                     }
250                 }
251 
252                 LOG.trace("Status before execution of {} is {}", qualifiedActionName, status);
253                 RuntimeException exception = null;
254                 boolean exceptionWasHandled = false;
255                 if (status == ActionExecutionStatus.OK) {
256                     try {
257                         doInvokeAction(controller, actionName, updatedArgs);
258                     catch (RuntimeException e) {
259                         status = ActionExecutionStatus.EXCEPTION;
260                         exception = (RuntimeExceptionsanitize(e);
261                         LOG.warn("An exception occurred when executing {}", qualifiedActionName, exception);
262                     }
263                     LOG.trace("Status after execution of {} is {}", qualifiedActionName, status);
264 
265                     if (exception != null) {
266                         for (ActionHandler handler : reverse(invokedHandlers)) {
267                             LOG.trace("Calling {}.exception() on {}", handler, qualifiedActionName);
268                             exceptionWasHandled = handler.exception(exception, action, updatedArgs);
269                         }
270                     }
271                 }
272 
273                 for (ActionHandler handler : reverse(invokedHandlers)) {
274                     LOG.trace("Calling {}.after() on {}", handler, qualifiedActionName);
275                     handler.after(status, action, updatedArgs);
276                 }
277 
278                 if (exception != null && !exceptionWasHandled) {
279                     // throw it again
280                     throw exception;
281                 }
282             }
283         };
284         invokeAction(controller, actionName, runnable);
285     }
286 
287     @Nonnull
288     private Object[] injectFromContext(@Nonnull Action action, @Nonnull Object[] args) {
289         ActionWrapper wrappedAction = null;
290         if (action instanceof ActionWrapper) {
291             wrappedAction = (ActionWrapperaction;
292         else {
293             wrappedAction = wrapAction(action, findActionAsMethod(action.getController(), action.getActionName()));
294         }
295 
296         MVCGroup group = action.getController().getMvcGroup();
297         if (group == null) {
298             // This case only occurs during testing, when an artifact is
299             // instantiated without a group
300             return args;
301         }
302 
303         Context context = group.getContext();
304         if (wrappedAction.hasContextualArgs) {
305             Object[] newArgs = new Object[wrappedAction.argumentsInfo.size()];
306             for (int i = 0; i < newArgs.length; i++) {
307                 ArgInfo argInfo = wrappedAction.argumentsInfo.get(i);
308                 newArgs[i= argInfo.contextual ? context.get(argInfo.name: args[i];
309                 if (argInfo.contextual && newArgs[i!= nullcontext.put(argInfo.name, newArgs[i]);
310                 if (argInfo.contextual && !argInfo.nullable && newArgs[i== null) {
311                     throw new IllegalStateException("Could not find an instance of type " +
312                         argInfo.type.getName() " under key '" + argInfo.name +
313                         "' in the context of MVCGroup[" + group.getMvcType() ":" + group.getMvcId() +
314                         "] to be injected as argument " + i +
315                         " at " + action.getFullyQualifiedName() "(). Argument does not accept null values.");
316                 }
317             }
318             return newArgs;
319         }
320 
321         return args;
322     }
323 
324     public void invokeAction(@Nonnull final GriffonController controller, @Nonnull final String actionName, @Nonnull final Object... args) {
325         requireNonNull(controller, ERROR_CONTROLLER_NULL);
326         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
327         invokeAction(actionFor(controller, actionName), args);
328     }
329 
330     protected void doInvokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Object[] updatedArgs) {
331         try {
332             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                     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.getClass().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 INSIDE_UITHREAD_SYNC:
367                 getUiThreadManager().runInsideUISync(runnable);
368                 break;
369             case INSIDE_UITHREAD_ASYNC:
370                 getUiThreadManager().runInsideUIAsync(runnable);
371                 break;
372             case SKIP:
373             default:
374                 runnable.run();
375         }
376     }
377 
378     @Nonnull
379     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller, @Nonnull String actionName) {
380         Method method = findActionAsMethod(controller, actionName);
381         if (method != null) {
382             Threading annotation = method.getAnnotation(Threading.class);
383             return annotation == null ? resolveThreadingPolicy(controller: annotation.value();
384         }
385 
386         return Threading.Policy.OUTSIDE_UITHREAD;
387     }
388 
389     @Nonnull
390     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller) {
391         Threading annotation = AnnotationUtils.findAnnotation(controller.getClass(), Threading.class);
392         return annotation == null ? resolveThreadingPolicy() : annotation.value();
393     }
394 
395     @Nonnull
396     private Threading.Policy resolveThreadingPolicy() {
397         Object value = getConfiguration().get(KEY_THREADING_DEFAULT);
398         if (value == null) {
399             return Threading.Policy.OUTSIDE_UITHREAD;
400         }
401 
402         if (value instanceof Threading.Policy) {
403             return (Threading.Policyvalue;
404         }
405 
406         String policy = String.valueOf(value).toLowerCase();
407         switch (policy) {
408             case "sync":
409             case "inside sync":
410             case "inside uithread sync":
411             case "inside_uithread_sync":
412                 return Threading.Policy.INSIDE_UITHREAD_SYNC;
413             case "async":
414             case "inside async":
415             case "inside uithread async":
416             case "inside_uithread_async":
417                 return Threading.Policy.INSIDE_UITHREAD_ASYNC;
418             case "outside":
419             case "outside uithread":
420             case "outside_uithread":
421                 return Threading.Policy.OUTSIDE_UITHREAD;
422             case "skip":
423                 return Threading.Policy.SKIP;
424             default:
425                 throw new IllegalArgumentException("Value '" + policy + "' cannot be translated into " + Threading.Policy.class.getName());
426         }
427     }
428 
429     private boolean isThreadingDisabled(@Nonnull String actionName) {
430         if (getConfiguration().getAsBoolean(KEY_DISABLE_THREADING_INJECTION, false)) {
431             return true;
432         }
433 
434         Map<String, Object> settings = getConfiguration().asFlatMap();
435 
436         String keyName = KEY_THREADING + "." + actionName;
437         while (!KEY_THREADING.equals(keyName)) {
438             Object value = settings.get(keyName);
439             keyName = keyName.substring(0, keyName.lastIndexOf("."));
440             if (value != null && !castToBoolean(value)) return true;
441         }
442 
443         return false;
444     }
445 
446     public void addActionHandler(@Nonnull ActionHandler actionHandler) {
447         requireNonNull(actionHandler, ERROR_ACTION_HANDLER_NULL);
448         if (handlers.contains(actionHandler)) {
449             return;
450         }
451         handlers.add(actionHandler);
452     }
453 
454     public void addActionInterceptor(@Nonnull ActionInterceptor actionInterceptor) {
455         throw new UnsupportedOperationException(ActionInterceptor.class.getName() " have been deprecated and are no longer supported");
456     }
457 
458     @Nonnull
459     protected Action createAndConfigureAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
460         requireNonNull(controller, ERROR_CONTROLLER_NULL);
461         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
462         Action action = createControllerAction(controller, actionName);
463 
464         String normalizeNamed = capitalize(normalizeName(actionName));
465         String keyPrefix = controller.getClass().getName() ".action.";
466 
467         String rsActionName = msg(keyPrefix, normalizeNamed, "name", getNaturalName(normalizeNamed));
468         if (!isBlank(rsActionName)) {
469             LOG.trace("{}{}.name = {}", keyPrefix, normalizeNamed, rsActionName);
470             action.setName(rsActionName);
471         }
472 
473         doConfigureAction(action, controller, normalizeNamed, keyPrefix);
474 
475         action.initialize();
476 
477         return action;
478     }
479 
480     protected abstract void doConfigureAction(@Nonnull Action action, @Nonnull GriffonController controller, @Nonnull String normalizeNamed, @Nonnull String keyPrefix);
481 
482     @Nonnull
483     protected abstract Action createControllerAction(@Nonnull GriffonController controller, @Nonnull String actionName);
484 
485     @Nonnull
486     public String normalizeName(@Nonnull String actionName) {
487         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
488         if (actionName.endsWith(ACTION)) {
489             actionName = actionName.substring(0, actionName.length() - ACTION.length());
490         }
491         return uncapitalize(actionName);
492     }
493 
494     @Nullable
495     protected String msg(@Nonnull String key, @Nonnull String actionName, @Nonnull String subkey, @Nullable String defaultValue) {
496         try {
497             return getMessageSource().getMessage(key + actionName + "." + subkey);
498         catch (NoSuchMessageException nsme) {
499             return getMessageSource().getMessage("application.action." + actionName + "." + subkey, defaultValue);
500         }
501     }
502 
503     private static class ActionWrapper extends ActionDecorator {
504         private final List<ArgInfo> argumentsInfo = new ArrayList<>();
505         private boolean hasContextualArgs;
506 
507         public ActionWrapper(@Nonnull Action delegate, @Nonnull Method method) {
508             super(delegate);
509 
510             Class<?>[] parameterTypes = method.getParameterTypes();
511             Annotation[][] parameterAnnotations = method.getParameterAnnotations();
512             hasContextualArgs = method.getAnnotation(Contextual.class!= null;
513             for (int i = 0; i < parameterTypes.length; i++) {
514                 ArgInfo argInfo = new ArgInfo();
515                 argInfo.type = parameterTypes[i];
516                 argInfo.name = argInfo.type.getCanonicalName();
517 
518                 Annotation[] annotations = parameterAnnotations[i];
519                 if (annotations != null) {
520                     for (Annotation annotation : annotations) {
521                         if (Contextual.class.isAssignableFrom(annotation.annotationType())) {
522                             hasContextualArgs = true;
523                             argInfo.contextual = true;
524                         }
525                         if (Nonnull.class.isAssignableFrom(annotation.annotationType())) {
526                             argInfo.nullable = false;
527                         }
528                         if (Named.class.isAssignableFrom(annotation.annotationType())) {
529                             Named named = (Namedannotation;
530                             if (!isBlank(named.value())) {
531                                 argInfo.name = named.value();
532                             }
533                         }
534                     }
535                 }
536                 argumentsInfo.add(argInfo);
537             }
538         }
539     }
540 
541     private static class ArgInfo {
542         private Class<?> type;
543         private String name;
544         private boolean nullable = true;
545         private boolean contextual = false;
546     }
547 
548     private static class ActionCache {
549         private final Map<WeakReference<GriffonController>, Map<String, ActionWrapper>> cache = new ConcurrentHashMap<>();
550 
551         @Nonnull
552         public Map<String, ActionWrapper> get(@Nonnull GriffonController controller) {
553             synchronized (cache) {
554                 for (Map.Entry<WeakReference<GriffonController>, Map<String, ActionWrapper>> entry : cache.entrySet()) {
555                     GriffonController test = entry.getKey().get();
556                     if (test == controller) {
557                         return entry.getValue();
558                     }
559                 }
560             }
561             return Collections.emptyMap();
562         }
563 
564         public void set(@Nonnull GriffonController controller, @Nonnull Map<String, ActionWrapper> actions) {
565             WeakReference<GriffonController> existingController = null;
566             synchronized (cache) {
567                 for (WeakReference<GriffonController> key : cache.keySet()) {
568                     if (key.get() == controller) {
569                         existingController = key;
570                         break;
571                     }
572                 }
573             }
574 
575             if (null != existingController) {
576                 cache.remove(existingController);
577             }
578 
579             cache.put(new WeakReference<>(controller), actions);
580         }
581 
582         public Collection<Action> allActions() {
583             // create a copy to avoid CME
584             List<Action> actions = new ArrayList<>();
585 
586             synchronized (cache) {
587                 for (Map<String, ActionWrapper> map : cache.values()) {
588                     actions.addAll(map.values());
589                 }
590             }
591 
592             return actions;
593         }
594     }
595 }