| 
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 }
 |