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