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