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