AbstractActionManager.java
001 /*
002  * Copyright 2008-2015 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                 updatedArgs = injectFromContext(action, updatedArgs);
225 
226                 final String qualifiedActionName = action.getFullyQualifiedName();
227                 ActionExecutionStatus status = ActionExecutionStatus.OK;
228 
229                 if (LOG.isDebugEnabled()) {
230                     int size = copy.size();
231                     LOG.debug("Executing " + size + " handler" (size != "s" """ for " + qualifiedActionName);
232                 }
233 
234                 for (ActionHandler handler : copy) {
235                     invokedHandlers.add(handler);
236                     try {
237                         LOG.trace("Calling {}.before() on {}", handler, qualifiedActionName);
238                         updatedArgs = handler.before(action, updatedArgs);
239                     catch (AbortActionExecution aae) {
240                         status = ActionExecutionStatus.ABORTED;
241                         LOG.debug("Execution of {} was aborted by {}", qualifiedActionName, handler);
242                         break;
243                     }
244                 }
245 
246                 LOG.trace("Status before execution of {} is {}", qualifiedActionName, status);
247                 RuntimeException exception = null;
248                 boolean exceptionWasHandled = false;
249                 if (status == ActionExecutionStatus.OK) {
250                     try {
251                         doInvokeAction(controller, actionName, updatedArgs);
252                     catch (RuntimeException e) {
253                         status = ActionExecutionStatus.EXCEPTION;
254                         exception = (RuntimeExceptionsanitize(e);
255                         LOG.warn("An exception occurred when executing {}", qualifiedActionName, exception);
256                     }
257                     LOG.trace("Status after execution of {} is {}", qualifiedActionName, status);
258 
259                     if (exception != null) {
260                         for (ActionHandler handler : reverse(invokedHandlers)) {
261                             LOG.trace("Calling {}.exception() on {}", handler, qualifiedActionName);
262                             exceptionWasHandled = handler.exception(exception, action, updatedArgs);
263                         }
264                     }
265                 }
266 
267                 for (ActionHandler handler : reverse(invokedHandlers)) {
268                     LOG.trace("Calling {}.after() on {}", handler, qualifiedActionName);
269                     handler.after(status, action, updatedArgs);
270                 }
271 
272                 if (exception != null && !exceptionWasHandled) {
273                     // throw it again
274                     throw exception;
275                 }
276             }
277         };
278         invokeAction(controller, actionName, runnable);
279     }
280 
281     @Nonnull
282     private Object[] injectFromContext(@Nonnull Action action, @Nonnull Object[] args) {
283         ActionWrapper wrappedAction = null;
284         if (action instanceof ActionWrapper) {
285             wrappedAction = (ActionWrapperaction;
286         else {
287             wrappedAction = wrapAction(action, findActionAsMethod(action.getController(), action.getActionName()));
288         }
289 
290         MVCGroup group = action.getController().getMvcGroup();
291         if (group == null) {
292             // This case only occurs during testing, when an artifact is
293             // instantiated without a group
294             return args;
295         }
296 
297         Context context = group.getContext();
298         List<String> namedArgs = wrappedAction.namedArgs;
299 
300         if (wrappedAction.hasContextualArgs) {
301             args = new Object[namedArgs.size()];
302             for (int i = 0; i < namedArgs.size(); i++) {
303                 args[i= context.get(wrappedAction.namedArgs.get(i));
304             }
305         }
306 
307         return args;
308     }
309 
310     public void invokeAction(@Nonnull final GriffonController controller, @Nonnull final String actionName, @Nonnull final Object... args) {
311         requireNonNull(controller, ERROR_CONTROLLER_NULL);
312         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
313         invokeAction(actionFor(controller, actionName), args);
314     }
315 
316     protected void doInvokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Object[] updatedArgs) {
317         try {
318             invokeInstanceMethod(controller, actionName, updatedArgs);
319         catch (InstanceMethodInvocationException imie) {
320             if (imie.getCause() instanceof NoSuchMethodException) {
321                 // try again but this time remove the 1st arg if it's
322                 // descendant of java.util.EventObject
323                 if (updatedArgs.length == && updatedArgs[0!= null && EventObject.class.isAssignableFrom(updatedArgs[0].getClass())) {
324                     invokeExactInstanceMethod(controller, actionName, EMPTY_ARGS);
325                 else {
326                     throw imie;
327                 }
328             else {
329                 throw imie;
330             }
331         }
332     }
333 
334     private void invokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Runnable runnable) {
335         String fullQualifiedActionName = controller.getClass().getName() "." + actionName;
336         Threading.Policy policy = threadingPolicies.get(fullQualifiedActionName);
337         if (policy == null) {
338             if (isThreadingDisabled(fullQualifiedActionName)) {
339                 policy = Threading.Policy.SKIP;
340             else {
341                 policy = resolveThreadingPolicy(controller, actionName);
342             }
343             threadingPolicies.put(fullQualifiedActionName, policy);
344         }
345 
346         LOG.debug("Executing {} with policy {}", fullQualifiedActionName, policy);
347 
348         switch (policy) {
349             case OUTSIDE_UITHREAD:
350                 getUiThreadManager().runOutsideUI(runnable);
351                 break;
352             case INSIDE_UITHREAD_SYNC:
353                 getUiThreadManager().runInsideUISync(runnable);
354                 break;
355             case INSIDE_UITHREAD_ASYNC:
356                 getUiThreadManager().runInsideUIAsync(runnable);
357                 break;
358             case SKIP:
359             default:
360                 runnable.run();
361         }
362     }
363 
364     @Nonnull
365     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller, @Nonnull String actionName) {
366         Method method = findActionAsMethod(controller, actionName);
367         if (method != null) {
368             Threading annotation = method.getAnnotation(Threading.class);
369             return annotation == null ? resolveThreadingPolicy(controller: annotation.value();
370         }
371 
372         return Threading.Policy.OUTSIDE_UITHREAD;
373     }
374 
375     @Nonnull
376     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller) {
377         Threading annotation = AnnotationUtils.findAnnotation(controller.getClass(), Threading.class);
378         return annotation == null ? resolveThreadingPolicy() : annotation.value();
379     }
380 
381     @Nonnull
382     private Threading.Policy resolveThreadingPolicy() {
383         Object value = getConfiguration().get(KEY_THREADING_DEFAULT);
384         if (value == null) {
385             return Threading.Policy.OUTSIDE_UITHREAD;
386         }
387 
388         if (value instanceof Threading.Policy) {
389             return (Threading.Policyvalue;
390         }
391 
392         String policy = String.valueOf(value).toLowerCase();
393         switch (policy) {
394             case "sync":
395             case "inside sync":
396             case "inside uithread sync":
397             case "inside_uithread_sync":
398                 return Threading.Policy.INSIDE_UITHREAD_SYNC;
399             case "async":
400             case "inside async":
401             case "inside uithread async":
402             case "inside_uithread_async":
403                 return Threading.Policy.INSIDE_UITHREAD_ASYNC;
404             case "outside":
405             case "outside uithread":
406             case "outside_uithread":
407                 return Threading.Policy.OUTSIDE_UITHREAD;
408             case "skip":
409                 return Threading.Policy.SKIP;
410             default:
411                 throw new IllegalArgumentException("Value '" + policy + "' cannot be translated into " + Threading.Policy.class.getName());
412         }
413     }
414 
415     private boolean isThreadingDisabled(@Nonnull String actionName) {
416         if (getConfiguration().getAsBoolean(KEY_DISABLE_THREADING_INJECTION, false)) {
417             return true;
418         }
419 
420         Map<String, Object> settings = getConfiguration().asFlatMap();
421 
422         String keyName = KEY_THREADING + "." + actionName;
423         while (!KEY_THREADING.equals(keyName)) {
424             Object value = settings.get(keyName);
425             keyName = keyName.substring(0, keyName.lastIndexOf("."));
426             if (value != null && !castToBoolean(value)) return true;
427         }
428 
429         return false;
430     }
431 
432     public void addActionHandler(@Nonnull ActionHandler actionHandler) {
433         requireNonNull(actionHandler, ERROR_ACTION_HANDLER_NULL);
434         if (handlers.contains(actionHandler)) {
435             return;
436         }
437         handlers.add(actionHandler);
438     }
439 
440     public void addActionInterceptor(@Nonnull ActionInterceptor actionInterceptor) {
441         throw new UnsupportedOperationException(ActionInterceptor.class.getName() " have been deprecated and are no longer supported");
442     }
443 
444     @Nonnull
445     protected Action createAndConfigureAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
446         requireNonNull(controller, ERROR_CONTROLLER_NULL);
447         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
448         Action action = createControllerAction(controller, actionName);
449 
450         String normalizeNamed = capitalize(normalizeName(actionName));
451         String keyPrefix = controller.getClass().getName() ".action.";
452 
453         String rsActionName = msg(keyPrefix, normalizeNamed, "name", getNaturalName(normalizeNamed));
454         if (!isBlank(rsActionName)) {
455             LOG.trace("{}{}.name = {}", keyPrefix, normalizeNamed, rsActionName);
456             action.setName(rsActionName);
457         }
458 
459         doConfigureAction(action, controller, normalizeNamed, keyPrefix);
460 
461         action.initialize();
462 
463         return action;
464     }
465 
466     protected abstract void doConfigureAction(@Nonnull Action action, @Nonnull GriffonController controller, @Nonnull String normalizeNamed, @Nonnull String keyPrefix);
467 
468     @Nonnull
469     protected abstract Action createControllerAction(@Nonnull GriffonController controller, @Nonnull String actionName);
470 
471     @Nonnull
472     public String normalizeName(@Nonnull String actionName) {
473         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
474         if (actionName.endsWith(ACTION)) {
475             actionName = actionName.substring(0, actionName.length() - ACTION.length());
476         }
477         return uncapitalize(actionName);
478     }
479 
480     @Nullable
481     protected String msg(@Nonnull String key, @Nonnull String actionName, @Nonnull String subkey, @Nullable String defaultValue) {
482         try {
483             return getMessageSource().getMessage(key + actionName + "." + subkey);
484         catch (NoSuchMessageException nsme) {
485             return getMessageSource().getMessage("application.action." + actionName + "." + subkey, defaultValue);
486         }
487     }
488 
489     private static class ActionWrapper extends ActionDecorator {
490         private final List<String> namedArgs = new ArrayList<>();
491         private boolean hasContextualArgs;
492 
493         public ActionWrapper(@Nonnull Action delegate, @Nonnull Method method) {
494             super(delegate);
495 
496             Class<?>[] parameterTypes = method.getParameterTypes();
497             Annotation[][] parameterAnnotations = method.getParameterAnnotations();
498             hasContextualArgs = method.getAnnotation(Contextual.class!= null;
499             for (int i = 0; i < parameterTypes.length; i++) {
500                 Class<?> type = parameterTypes[i];
501 
502                 Annotation[] annotations = parameterAnnotations[i];
503                 String name = type.getCanonicalName();
504                 if (annotations != null) {
505                     for (Annotation annotation : annotations) {
506                         if (Contextual.class.isAssignableFrom(annotation.annotationType())) {
507                             hasContextualArgs = true;
508                         }
509                         if (Named.class.isAssignableFrom(annotation.annotationType())) {
510                             Named named = (Namedannotation;
511                             if (!isBlank(named.value())) {
512                                 name = named.value();
513                             }
514                         }
515                     }
516                 }
517                 namedArgs.add(name);
518             }
519         }
520     }
521 
522     private static class ActionCache {
523         private final Map<WeakReference<GriffonController>, Map<String, ActionWrapper>> cache = new ConcurrentHashMap<>();
524 
525         @Nonnull
526         public Map<String, ActionWrapper> get(@Nonnull GriffonController controller) {
527             synchronized (cache) {
528                 for (Map.Entry<WeakReference<GriffonController>, Map<String, ActionWrapper>> entry : cache.entrySet()) {
529                     GriffonController test = entry.getKey().get();
530                     if (test == controller) {
531                         return entry.getValue();
532                     }
533                 }
534             }
535             return Collections.emptyMap();
536         }
537 
538         public void set(@Nonnull GriffonController controller, @Nonnull Map<String, ActionWrapper> actions) {
539             WeakReference<GriffonController> existingController = null;
540             synchronized (cache) {
541                 for (WeakReference<GriffonController> key : cache.keySet()) {
542                     if (key.get() == controller) {
543                         existingController = key;
544                         break;
545                     }
546                 }
547             }
548 
549             if (null != existingController) {
550                 cache.remove(existingController);
551             }
552 
553             cache.put(new WeakReference<>(controller), actions);
554         }
555 
556         public Collection<Action> allActions() {
557             // create a copy to avoid CME
558             List<Action> actions = new ArrayList<>();
559 
560             synchronized (cache) {
561                 for (Map<String, ActionWrapper> map : cache.values()) {
562                     actions.addAll(map.values());
563                 }
564             }
565 
566             return actions;
567         }
568     }
569 }