AbstractActionManager.java
001 /*
002  * Copyright 2008-2014 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.GriffonApplication;
020 import griffon.core.artifact.GriffonController;
021 import griffon.core.artifact.GriffonControllerClass;
022 import griffon.core.controller.AbortActionExecution;
023 import griffon.core.controller.Action;
024 import griffon.core.controller.ActionExecutionStatus;
025 import griffon.core.controller.ActionHandler;
026 import griffon.core.controller.ActionInterceptor;
027 import griffon.core.controller.ActionManager;
028 import griffon.core.i18n.MessageSource;
029 import griffon.core.i18n.NoSuchMessageException;
030 import griffon.core.threading.UIThreadManager;
031 import griffon.exceptions.InstanceMethodInvocationException;
032 import griffon.transform.Threading;
033 import griffon.util.AnnotationUtils;
034 import org.slf4j.Logger;
035 import org.slf4j.LoggerFactory;
036 
037 import javax.annotation.Nonnull;
038 import javax.annotation.Nullable;
039 import javax.inject.Inject;
040 import java.lang.ref.WeakReference;
041 import java.lang.reflect.Method;
042 import java.util.ArrayList;
043 import java.util.Collection;
044 import java.util.Collections;
045 import java.util.EventObject;
046 import java.util.List;
047 import java.util.Map;
048 import java.util.TreeMap;
049 import java.util.concurrent.ConcurrentHashMap;
050 import java.util.concurrent.CopyOnWriteArrayList;
051 
052 import static griffon.core.GriffonExceptionHandler.sanitize;
053 import static griffon.util.CollectionUtils.reverse;
054 import static griffon.util.GriffonClassUtils.EMPTY_ARGS;
055 import static griffon.util.GriffonClassUtils.invokeExactInstanceMethod;
056 import static griffon.util.GriffonNameUtils.capitalize;
057 import static griffon.util.GriffonNameUtils.getNaturalName;
058 import static griffon.util.GriffonNameUtils.isBlank;
059 import static griffon.util.GriffonNameUtils.requireNonBlank;
060 import static griffon.util.GriffonNameUtils.uncapitalize;
061 import static griffon.util.TypeUtils.castToBoolean;
062 import static java.lang.reflect.Modifier.isPublic;
063 import static java.lang.reflect.Modifier.isStatic;
064 import static java.util.Objects.requireNonNull;
065 
066 /**
067  @author Andres Almiray
068  @since 2.0.0
069  */
070 public abstract class AbstractActionManager implements ActionManager {
071     private static final Logger LOG = LoggerFactory.getLogger(AbstractActionManager.class);
072 
073     private static final String KEY_THREADING = "controller.threading";
074     private static final String KEY_THREADING_DEFAULT = "controller.threading.default";
075     private static final String KEY_DISABLE_THREADING_INJECTION = "griffon.disable.threading.injection";
076     private static final String ERROR_CONTROLLER_NULL = "Argument 'controller' must not be null";
077     private static final String ERROR_ACTION_NAME_BLANK = "Argument 'actionName' must not be blank";
078     private static final String ERROR_ACTION_HANDLER_NULL = "Argument 'actionHandler' must not be null";
079     private static final String ERROR_ACTION_NULL = "Argument 'action' must not be null";
080     private final ActionCache actionCache = new ActionCache();
081     private final Map<String, Threading.Policy> threadingPolicies = new ConcurrentHashMap<>();
082     private final List<ActionHandler> handlers = new CopyOnWriteArrayList<>();
083 
084     private final GriffonApplication application;
085 
086     @Inject
087     public AbstractActionManager(@Nonnull GriffonApplication application) {
088         this.application = requireNonNull(application, "Argument 'application' must not be null");
089     }
090 
091     @Nonnull
092     protected Configuration getConfiguration() {
093         return application.getConfiguration();
094     }
095 
096     @Nonnull
097     protected MessageSource getMessageSource() {
098         return application.getMessageSource();
099     }
100 
101     @Nonnull
102     protected UIThreadManager getUiThreadManager() {
103         return application.getUIThreadManager();
104     }
105 
106     @Nonnull
107     protected Map<String, Threading.Policy> getThreadingPolicies() {
108         return threadingPolicies;
109     }
110 
111     @Nonnull
112     public Map<String, Action> actionsFor(@Nonnull GriffonController controller) {
113         requireNonNull(controller, ERROR_CONTROLLER_NULL);
114         Map<String, Action> actions = actionCache.get(controller);
115         if (actions.isEmpty()) {
116             LOG.trace("No actions defined for controller {}", controller);
117         }
118         return actions;
119     }
120 
121     @Nullable
122     public Action actionFor(@Nonnull GriffonController controller, @Nonnull String actionName) {
123         requireNonNull(controller, ERROR_CONTROLLER_NULL);
124         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
125         return actionCache.get(controller).get(normalizeName(actionName));
126     }
127 
128     public void createActions(@Nonnull GriffonController controller) {
129         GriffonControllerClass griffonClass = (GriffonControllerClasscontroller.getGriffonClass();
130         for (String actionName : griffonClass.getActionNames()) {
131             Action action = createAndConfigureAction(controller, actionName);
132 
133             Method method = findActionAsMethod(controller, actionName);
134             final String qualifiedActionName = action.getFullyQualifiedName();
135             for (ActionHandler handler : handlers) {
136                 if (method != null) {
137                     LOG.debug("Configuring action {} with {}", qualifiedActionName, handler);
138                     handler.configure(action, method);
139                 }
140             }
141 
142             Map<String, Action> actions = actionCache.get(controller);
143             if (actions.isEmpty()) {
144                 actions = new TreeMap<>();
145                 actionCache.set(controller, actions);
146             }
147             String actionKey = normalizeName(actionName);
148             LOG.trace("Action for {} stored as {}", qualifiedActionName, actionKey);
149             actions.put(actionKey, action);
150         }
151     }
152 
153     @Override
154     public void updateActions() {
155         for (Action action : actionCache.allActions()) {
156             updateAction(action);
157         }
158     }
159 
160     @Override
161     public void updateActions(@Nonnull GriffonController controller) {
162         for (Action action : actionsFor(controller).values()) {
163             updateAction(action);
164         }
165     }
166 
167     @Override
168     public void updateAction(@Nonnull Action action) {
169         requireNonNull(action, ERROR_ACTION_NULL);
170 
171         final String qualifiedActionName = action.getFullyQualifiedName();
172         for (ActionHandler handler : handlers) {
173             LOG.trace("Calling {}.update() on {}", handler, qualifiedActionName);
174             handler.update(action);
175         }
176     }
177 
178     @Override
179     public void updateAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
180         requireNonNull(controller, ERROR_CONTROLLER_NULL);
181         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
182         updateAction(actionFor(controller, actionName));
183     }
184 
185     @Override
186     public void invokeAction(@Nonnull final Action action, @Nonnull final Object... args) {
187         requireNonNull(action, ERROR_ACTION_NULL);
188         final GriffonController controller = action.getController();
189         final String actionName = action.getActionName();
190         Runnable runnable = new Runnable() {
191             @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
192             public void run() {
193                 Object[] updatedArgs = args;
194                 List<ActionHandler> copy = new ArrayList<>(handlers);
195                 List<ActionHandler> invokedHandlers = new ArrayList<>();
196 
197                 final String qualifiedActionName = action.getFullyQualifiedName();
198                 ActionExecutionStatus status = ActionExecutionStatus.OK;
199 
200                 if (LOG.isDebugEnabled()) {
201                     int size = copy.size();
202                     LOG.debug("Executing " + size + " handler" (size != "s" """ for " + qualifiedActionName);
203                 }
204 
205                 for (ActionHandler handler : copy) {
206                     invokedHandlers.add(handler);
207                     try {
208                         LOG.trace("Calling {}.before() on {}", handler, qualifiedActionName);
209                         updatedArgs = handler.before(action, updatedArgs);
210                     catch (AbortActionExecution aae) {
211                         status = ActionExecutionStatus.ABORTED;
212                         LOG.debug("Execution of {} was aborted by {}", qualifiedActionName, handler);
213                         break;
214                     }
215                 }
216 
217                 LOG.trace("Status before execution of {} is {}", qualifiedActionName, status);
218                 RuntimeException exception = null;
219                 boolean exceptionWasHandled = false;
220                 if (status == ActionExecutionStatus.OK) {
221                     try {
222                         doInvokeAction(controller, actionName, updatedArgs);
223                     catch (RuntimeException e) {
224                         status = ActionExecutionStatus.EXCEPTION;
225                         exception = (RuntimeExceptionsanitize(e);
226                         LOG.warn("An exception occurred when executing {}", qualifiedActionName, exception);
227                     }
228                     LOG.trace("Status after execution of {} is {}", qualifiedActionName, status);
229 
230                     if (exception != null) {
231                         for (ActionHandler handler : reverse(invokedHandlers)) {
232                             LOG.trace("Calling {}.exception() on {}", handler, qualifiedActionName);
233                             exceptionWasHandled = handler.exception(exception, action, updatedArgs);
234                         }
235                     }
236                 }
237 
238                 for (ActionHandler handler : reverse(invokedHandlers)) {
239                     LOG.trace("Calling {}.after() on {}", handler, qualifiedActionName);
240                     handler.after(status, action, updatedArgs);
241                 }
242 
243                 if (exception != null && !exceptionWasHandled) {
244                     // throw it again
245                     throw exception;
246                 }
247             }
248         };
249         invokeAction(controller, actionName, runnable);
250     }
251 
252     public void invokeAction(@Nonnull final GriffonController controller, @Nonnull final String actionName, @Nonnull final Object... args) {
253         requireNonNull(controller, ERROR_CONTROLLER_NULL);
254         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
255         invokeAction(actionFor(controller, actionName), args);
256     }
257 
258     protected void doInvokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Object[] updatedArgs) {
259         try {
260             invokeExactInstanceMethod(controller, actionName, updatedArgs);
261         catch (InstanceMethodInvocationException imie) {
262             if (imie.getCause() instanceof NoSuchMethodException) {
263                 // try again but this time remove the 1st arg if it's
264                 // descendant of java.util.EventObject
265                 if (updatedArgs.length == && updatedArgs[0!= null && EventObject.class.isAssignableFrom(updatedArgs[0].getClass())) {
266                     invokeExactInstanceMethod(controller, actionName, EMPTY_ARGS);
267                 else {
268                     throw imie;
269                 }
270             else {
271                 throw imie;
272             }
273         }
274     }
275 
276     private void invokeAction(@Nonnull GriffonController controller, @Nonnull String actionName, @Nonnull Runnable runnable) {
277         String fullQualifiedActionName = controller.getClass().getName() "." + actionName;
278         Threading.Policy policy = threadingPolicies.get(fullQualifiedActionName);
279         if (policy == null) {
280             if (isThreadingDisabled(fullQualifiedActionName)) {
281                 policy = Threading.Policy.SKIP;
282             else {
283                 policy = resolveThreadingPolicy(controller, actionName);
284             }
285             threadingPolicies.put(fullQualifiedActionName, policy);
286         }
287 
288         LOG.debug("Executing {} with policy {}", fullQualifiedActionName, policy);
289 
290         switch (policy) {
291             case OUTSIDE_UITHREAD:
292                 getUiThreadManager().runOutsideUI(runnable);
293                 break;
294             case INSIDE_UITHREAD_SYNC:
295                 getUiThreadManager().runInsideUISync(runnable);
296                 break;
297             case INSIDE_UITHREAD_ASYNC:
298                 getUiThreadManager().runInsideUIAsync(runnable);
299                 break;
300             case SKIP:
301             default:
302                 runnable.run();
303         }
304     }
305 
306     @Nullable
307     private static Method findActionAsMethod(@Nonnull GriffonController controller, @Nonnull String actionName) {
308         for (Method method : controller.getClass().getMethods()) {
309             if (actionName.equals(method.getName()) &&
310                 isPublic(method.getModifiers()) &&
311                 !isStatic(method.getModifiers()) &&
312                 method.getReturnType() == Void.TYPE) {
313                 return method;
314             }
315         }
316         return null;
317     }
318 
319     @Nonnull
320     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller, @Nonnull String actionName) {
321         Method method = findActionAsMethod(controller, actionName);
322         if (method != null) {
323             Threading annotation = method.getAnnotation(Threading.class);
324             return annotation == null ? resolveThreadingPolicy(controller: annotation.value();
325         }
326 
327         return Threading.Policy.OUTSIDE_UITHREAD;
328     }
329 
330     @Nonnull
331     private Threading.Policy resolveThreadingPolicy(@Nonnull GriffonController controller) {
332         Threading annotation = AnnotationUtils.findAnnotation(controller.getClass(), Threading.class);
333         return annotation == null ? resolveThreadingPolicy() : annotation.value();
334     }
335 
336     @Nonnull
337     private Threading.Policy resolveThreadingPolicy() {
338         Object value = getConfiguration().get(KEY_THREADING_DEFAULT);
339         if (value == null) {
340             return Threading.Policy.OUTSIDE_UITHREAD;
341         }
342 
343         if (value instanceof Threading.Policy) {
344             return (Threading.Policyvalue;
345         }
346 
347         String policy = String.valueOf(value).toLowerCase();
348         switch (policy) {
349             case "sync":
350             case "inside sync":
351             case "inside uithread sync":
352             case "inside_uithread_sync":
353                 return Threading.Policy.INSIDE_UITHREAD_SYNC;
354             case "async":
355             case "inside async":
356             case "inside uithread async":
357             case "inside_uithread_async":
358                 return Threading.Policy.INSIDE_UITHREAD_ASYNC;
359             case "outside":
360             case "outside uithread":
361             case "outside_uithread":
362                 return Threading.Policy.OUTSIDE_UITHREAD;
363             case "skip":
364                 return Threading.Policy.SKIP;
365             default:
366                 throw new IllegalArgumentException("Value '" + policy + "' cannot be translated into " + Threading.Policy.class.getName());
367         }
368     }
369 
370     private boolean isThreadingDisabled(@Nonnull String actionName) {
371         if (getConfiguration().getAsBoolean(KEY_DISABLE_THREADING_INJECTION, false)) {
372             return true;
373         }
374 
375         Map<String, Object> settings = getConfiguration().asFlatMap();
376 
377         String keyName = KEY_THREADING + "." + actionName;
378         while (!KEY_THREADING.equals(keyName)) {
379             Object value = settings.get(keyName);
380             keyName = keyName.substring(0, keyName.lastIndexOf("."));
381             if (value != null && !castToBoolean(value)) return true;
382         }
383 
384         return false;
385     }
386 
387     public void addActionHandler(@Nonnull ActionHandler actionHandler) {
388         requireNonNull(actionHandler, ERROR_ACTION_HANDLER_NULL);
389         if (handlers.contains(actionHandler)) {
390             return;
391         }
392         handlers.add(actionHandler);
393     }
394 
395     public void addActionInterceptor(@Nonnull ActionInterceptor actionInterceptor) {
396         throw new UnsupportedOperationException(ActionInterceptor.class.getName() " have been deprecated and are no longer supported");
397     }
398 
399     @Nonnull
400     protected Action createAndConfigureAction(@Nonnull GriffonController controller, @Nonnull String actionName) {
401         requireNonNull(controller, ERROR_CONTROLLER_NULL);
402         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
403         Action action = createControllerAction(controller, actionName);
404 
405         String normalizeNamed = capitalize(normalizeName(actionName));
406         String keyPrefix = controller.getClass().getName() ".action.";
407 
408         String rsActionName = msg(keyPrefix, normalizeNamed, "name", getNaturalName(normalizeNamed));
409         if (!isBlank(rsActionName)) {
410             LOG.trace("{}{}.name = {}", keyPrefix, normalizeNamed, rsActionName);
411             action.setName(rsActionName);
412         }
413 
414         doConfigureAction(action, controller, normalizeNamed, keyPrefix);
415 
416         action.initialize();
417 
418         return action;
419     }
420 
421     protected abstract void doConfigureAction(@Nonnull Action action, @Nonnull GriffonController controller, @Nonnull String normalizeNamed, @Nonnull String keyPrefix);
422 
423     @Nonnull
424     protected abstract Action createControllerAction(@Nonnull GriffonController controller, @Nonnull String actionName);
425 
426     @Nonnull
427     public String normalizeName(@Nonnull String actionName) {
428         requireNonBlank(actionName, ERROR_ACTION_NAME_BLANK);
429         if (actionName.endsWith(ACTION)) {
430             actionName = actionName.substring(0, actionName.length() - ACTION.length());
431         }
432         return uncapitalize(actionName);
433     }
434 
435     @Nullable
436     protected String msg(@Nonnull String key, @Nonnull String actionName, @Nonnull String subkey, @Nullable String defaultValue) {
437         try {
438             return getMessageSource().getMessage(key + actionName + "." + subkey);
439         catch (NoSuchMessageException nsme) {
440             return getMessageSource().getMessage("application.action." + actionName + "." + subkey, defaultValue);
441         }
442     }
443 
444     private static class ActionCache {
445         private final Map<WeakReference<GriffonController>, Map<String, Action>> cache = new ConcurrentHashMap<>();
446 
447         @Nonnull
448         public Map<String, Action> get(@Nonnull GriffonController controller) {
449             synchronized (cache) {
450                 for (Map.Entry<WeakReference<GriffonController>, Map<String, Action>> entry : cache.entrySet()) {
451                     GriffonController test = entry.getKey().get();
452                     if (test == controller) {
453                         return entry.getValue();
454                     }
455                 }
456             }
457             return Collections.emptyMap();
458         }
459 
460         public void set(@Nonnull GriffonController controller, @Nonnull Map<String, Action> actions) {
461             WeakReference<GriffonController> existingController = null;
462             synchronized (cache) {
463                 for (WeakReference<GriffonController> key : cache.keySet()) {
464                     if (key.get() == controller) {
465                         existingController = key;
466                         break;
467                     }
468                 }
469             }
470 
471             if (null != existingController) {
472                 cache.remove(existingController);
473             }
474 
475             cache.put(new WeakReference<>(controller), actions);
476         }
477 
478         public Collection<Action> allActions() {
479             // create a copy to avoid CME
480             List<Action> actions = new ArrayList<>();
481 
482             synchronized (cache) {
483                 for (Map<String, Action> map : cache.values()) {
484                     actions.addAll(map.values());
485                 }
486             }
487 
488             return actions;
489         }
490     }
491 }