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