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 = (GriffonControllerClass) controller.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 != 1 ? "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 = (RuntimeException) sanitize(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 == 1 && 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.Policy) value;
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 }
|