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 = (GriffonControllerClass) controller.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 != 1 ? "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 = (RuntimeException) sanitize(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 = (ActionWrapper) action;
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] != null) context.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 == 1 && 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.Policy) value;
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 = (Named) annotation;
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 }
|