View Javadoc
1   /**
2    * This file Copyright (c) 2015-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.ui.framework.action.async;
35  
36  import info.magnolia.cms.security.User;
37  import info.magnolia.context.Context;
38  import info.magnolia.context.MgnlContext;
39  import info.magnolia.i18nsystem.SimpleTranslator;
40  import info.magnolia.module.scheduler.CommandJob;
41  import info.magnolia.module.scheduler.SchedulerConsts;
42  import info.magnolia.module.scheduler.SchedulerModule;
43  import info.magnolia.objectfactory.Components;
44  import info.magnolia.ui.api.action.CommandActionDefinition;
45  import info.magnolia.ui.api.app.SubAppContext;
46  import info.magnolia.ui.api.context.UiContext;
47  import info.magnolia.ui.api.message.Message;
48  import info.magnolia.ui.api.message.MessageType;
49  import info.magnolia.ui.framework.message.MessagesManager;
50  import info.magnolia.ui.vaadin.integration.jcr.JcrItemAdapter;
51  
52  import java.util.Calendar;
53  import java.util.List;
54  import java.util.Map;
55  import java.util.concurrent.atomic.AtomicInteger;
56  
57  import javax.inject.Inject;
58  import javax.inject.Provider;
59  import javax.jcr.RepositoryException;
60  
61  import org.apache.commons.lang3.StringUtils;
62  import org.apache.commons.lang3.exception.ExceptionUtils;
63  import org.quartz.JobBuilder;
64  import org.quartz.JobDetail;
65  import org.quartz.JobExecutionContext;
66  import org.quartz.Scheduler;
67  import org.quartz.SchedulerException;
68  import org.quartz.Trigger;
69  import org.quartz.TriggerBuilder;
70  import org.quartz.TriggerListener;
71  import org.quartz.listeners.TriggerListenerSupport;
72  import org.slf4j.Logger;
73  import org.slf4j.LoggerFactory;
74  
75  /**
76   * {@link AsyncActionExecutor} delegating to Magnolia's {@link SchedulerModule} for asynchronous action execution.
77   *
78   * @param <D> {@link info.magnolia.ui.api.action.CommandActionDefinition}.
79   */
80  public class DefaultAsyncActionExecutor<D extends CommandActionDefinition> implements AsyncActionExecutor {
81  
82      private static AtomicInteger idx = new AtomicInteger();
83  
84      private final D definition;
85      private final Provider<SchedulerModule> schedulerModuleProvider;
86      private final User user;
87      private final UiContext uiContext;
88      private final String catalogName;
89      private final SimpleTranslator i18n;
90      private final String commandName;
91      private static final Logger log = LoggerFactory.getLogger(DefaultAsyncActionExecutor.class);
92  
93      @Inject
94      public DefaultAsyncActionExecutor(final D definition, final Provider<SchedulerModule> schedulerModuleProvider, final Context context,
95                                        final UiContext uiContext, final SimpleTranslator i18n) {
96          this.definition = definition;
97          this.schedulerModuleProvider = schedulerModuleProvider;
98          this.user = context.getUser();
99          this.uiContext = uiContext;
100         this.i18n = i18n;
101         this.commandName = definition.getCommand();
102         this.catalogName = definition.getCatalog();
103     }
104 
105     @Override
106     public boolean execute(JcrItemAdapter item, Map<String, Object> params) throws Exception {
107         Calendar cal = Calendar.getInstance();
108         // wait for requested period of time before invocation
109         cal.add(Calendar.SECOND, definition.getDelay());
110 
111         // init waiting time before job is started to avoid issues (when job is finished before timeToWait is initialized)
112         int timeToWait = definition.getTimeToWait();
113 
114         String jobName = "UI Action triggered execution of [" + (StringUtils.isNotEmpty(catalogName) ? (catalogName + ":") : "") + commandName + "] by user [" + StringUtils.defaultIfEmpty(user.getName(), "") + "].";
115         // allowParallel jobs false/true => remove index, or keep index
116         if (definition.isParallel()) {
117             jobName += " (" + idx.getAndIncrement() + ")";
118         }
119 
120         Trigger trigger = TriggerBuilder.newTrigger()
121                 .withIdentity(jobName, SchedulerConsts.SCHEDULER_GROUP_NAME)
122                 .startAt(cal.getTime())
123                 .build();
124 
125         // create job definition
126         final JobDetail jd = JobBuilder.newJob()
127                 .withIdentity(jobName, SchedulerConsts.SCHEDULER_GROUP_NAME)
128                 .ofType(info.magnolia.module.scheduler.CommandJob.class)
129                 .build();
130         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND, commandName);
131         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND_CATALOG, catalogName);
132         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_PARAMS, params);
133 
134         Scheduler scheduler = schedulerModuleProvider.get().getScheduler();
135         TriggerListener triggerListener = getListener(jobName, item);
136         scheduler.getListenerManager().addTriggerListener(triggerListener);
137         try {
138             scheduler.scheduleJob(jd, trigger);
139         } catch (SchedulerException e) {
140             throw new ParallelExecutionException(e);
141         }
142 
143         // wait until job has been executed
144         Thread.sleep(definition.getDelay() * 1000 + 100);
145         int timeToSleep = 500;
146         // check every 500ms if job is running
147         while (timeToWait > 0) {
148             List<JobExecutionContext> jobs = scheduler.getCurrentlyExecutingJobs();
149             if (isJobRunning(jobs, jobName)) {
150                 Thread.sleep(timeToSleep);
151             } else {
152                 break;
153             }
154             timeToWait -= timeToSleep;
155         }
156 
157         boolean isRunningInBackground = (timeToWait == 0);
158         // Throw error in case job completed and job result is unsuccessful
159         if (!isRunningInBackground && triggerListener instanceof DefaultAsyncActionExecutor.CommandActionTriggerListener) {
160             CommandActionTriggerListener commandActionTriggerListener = (CommandActionTriggerListener) triggerListener;
161 
162             if (commandActionTriggerListener.getException() != null) {
163                 throw commandActionTriggerListener.getException();
164             }
165         }
166         return isRunningInBackground;
167     }
168 
169     protected TriggerListener getListener(String jobName, JcrItemAdapter item) throws RepositoryException {
170         return new CommandActionTriggerListener(definition, jobName + "_trigger", uiContext, i18n, item.getJcrItem().getPath(), user.getName());
171     }
172 
173     private boolean isJobRunning(List<JobExecutionContext> jobs, String jobName) {
174         for (JobExecutionContext job : jobs) {
175             if (StringUtils.equals(job.getJobDetail().getKey().getName(), jobName)) {
176                 return true;
177             }
178         }
179         return false;
180     }
181 
182     /**
183      * Takes care of notifying the user about successful or failed executions.
184      *
185      * @param <D> {@link info.magnolia.ui.api.action.CommandActionDefinition}.
186      */
187     public static class CommandActionTriggerListener<D extends CommandActionDefinition> extends TriggerListenerSupport {
188 
189         private final D definition;
190         private final String name;
191         private final SimpleTranslator i18n;
192         private final String successMessageTitle;
193         private final String successMessage;
194         private final String errorMessageTitle;
195         private final String errorMessage;
196         private final String userName;
197         private Exception exception = null;
198 
199         /**
200          * @deprecated since 5.4.10, please use {@link #CommandActionTriggerListener(D, String, UiContext, SimpleTranslator, String, String)} instead.
201          */
202         @Deprecated
203         public CommandActionTriggerListener(D definition, String triggerName, UiContext uiContext, SimpleTranslator i18n, String path) {
204             this(definition, triggerName, uiContext, i18n, path, MgnlContext.getUser().getName());
205         }
206 
207         @Inject
208         public CommandActionTriggerListener(D definition, String triggerName, UiContext uiContext, SimpleTranslator i18n, String path, String userName) {
209             this.definition = definition;
210             this.name = triggerName;
211             this.i18n = i18n;
212 
213             String appName = uiContext instanceof SubAppContext ? ((SubAppContext) uiContext).getSubAppDescriptor().getLabel() : null;
214             this.successMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.successTitle", definition.getLabel());
215             this.successMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.successMessage", definition.getLabel(), appName, path);
216             this.errorMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.errorTitle", definition.getLabel());
217             this.errorMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.errorMessage", definition.getLabel(), appName, path);
218             this.userName = userName;
219         }
220 
221         @Override
222         public String getName() {
223             return name;
224         }
225 
226         @Override
227         public void triggerComplete(final Trigger trigger, final JobExecutionContext jobExecutionContext, Trigger.CompletedExecutionInstruction completedExecutionInstruction) {
228             if (!definition.isNotifyUser()) {
229                 return;
230             }
231             MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
232                 @Override
233                 public void doExec() {
234                     // User should be notified always when the long running action has finished. Otherwise he gets NO feedback.
235                     MessagesManager messagesManager = Components.getComponent(MessagesManager.class);
236                     // result 1 stands for success, 0 for error - see info.magnolia.module.scheduler.CommandJob
237                     CommandJob.JobResult result = (CommandJob.JobResult) jobExecutionContext.getResult();
238                     exception = result.getException();
239                     if (result.isSuccess()) {
240                         messagesManager.sendMessage(userName, new Message(MessageType.INFO, successMessageTitle, successMessage));
241                     } else {
242                         Message msg = new Message(MessageType.WARNING, errorMessageTitle, errorMessage);
243                         msg.setView("ui-admincentral:longRunning");
244                         msg.addProperty("exception", ExceptionUtils.getMessage(result.getException()));
245                         msg.addProperty("comment", i18n.translate("ui-framework.abstractcommand.asyncaction.errorComment"));
246                         messagesManager.sendMessage(userName, msg);
247                     }
248                 }
249             });
250             try {
251                 // MGNLUI-4004 Once done, removes self from the collection of Quartz's TriggerListener(s) as Quartz scheduler
252                 // doesn't seem to do it itself. This should be safe as this listener is only used once to notify the UI
253                 // that the job has completed. Failing to remove the trigger listener, will retain it in memory until
254                 // Tomcat is restarted as part of the QuartzScheduler object which is in turn a field of
255                 // Magnolia's SchedulerModule class which is a singleton
256                 jobExecutionContext.getScheduler().getListenerManager().removeTriggerListener(getName());
257             } catch (SchedulerException e) {
258                 log.warn("Failed to remove trigger listener {}", getName(), e);
259             }
260         }
261 
262         public Exception getException() {
263             return exception;
264         }
265     }
266 }