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