View Javadoc
1   /**
2    * This file Copyright (c) 2015-2016 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.JobDetail;
64  import org.quartz.JobExecutionContext;
65  import org.quartz.Scheduler;
66  import org.quartz.SchedulerException;
67  import org.quartz.SimpleTrigger;
68  import org.quartz.Trigger;
69  import org.quartz.TriggerListener;
70  import org.quartz.listeners.TriggerListenerSupport;
71  
72  /**
73   * {@link AsyncActionExecutor} delegating to Magnolia's {@link SchedulerModule} for asynchronous action execution.
74   *
75   * @param <D> {@link info.magnolia.ui.api.action.CommandActionDefinition}.
76   */
77  public class DefaultAsyncActionExecutor<D extends CommandActionDefinition> implements AsyncActionExecutor {
78  
79      private static AtomicInteger idx = new AtomicInteger();
80  
81      private final D definition;
82      private final Provider<SchedulerModule> schedulerModuleProvider;
83      private final User user;
84      private final UiContext uiContext;
85      private final String catalogName;
86      private final SimpleTranslator i18n;
87      private final String commandName;
88  
89      @Inject
90      public DefaultAsyncActionExecutor(final D definition, final Provider<SchedulerModule> schedulerModuleProvider, final Context context,
91                                        final UiContext uiContext, final SimpleTranslator i18n) {
92          this.definition = definition;
93          this.schedulerModuleProvider = schedulerModuleProvider;
94          this.user = context.getUser();
95          this.uiContext = uiContext;
96          this.i18n = i18n;
97          this.commandName = definition.getCommand();
98          this.catalogName = definition.getCatalog();
99      }
100 
101     @Override
102     public boolean execute(JcrItemAdapter item, Map<String, Object> params) throws Exception {
103         Calendar cal = Calendar.getInstance();
104         // wait for requested period of time before invocation
105         cal.add(Calendar.SECOND, definition.getDelay());
106 
107         // init waiting time before job is started to avoid issues (when job is finished before timeToWait is initialized)
108         int timeToWait = definition.getTimeToWait();
109 
110         String jobName = "UI Action triggered execution of [" + (StringUtils.isNotEmpty(catalogName) ? (catalogName + ":") : "") + commandName + "] by user [" + StringUtils.defaultIfEmpty(user.getName(), "") + "].";
111         // allowParallel jobs false/true => remove index, or keep index
112         if (definition.isParallel()) {
113             jobName += " (" + idx.getAndIncrement() + ")";
114         }
115         SimpleTrigger trigger = new SimpleTrigger(jobName, SchedulerConsts.SCHEDULER_GROUP_NAME, cal.getTime());
116         trigger.addTriggerListener(jobName + "_trigger");
117         // create job definition
118         final JobDetail jd = new JobDetail(jobName, SchedulerConsts.SCHEDULER_GROUP_NAME, info.magnolia.module.scheduler.CommandJob.class);
119         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND, commandName);
120         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_COMMAND_CATALOG, catalogName);
121         jd.getJobDataMap().put(SchedulerConsts.CONFIG_JOB_PARAMS, params);
122 
123         Scheduler scheduler = schedulerModuleProvider.get().getScheduler();
124         TriggerListener triggerListener = getListener(jobName, item);
125         scheduler.addTriggerListener(triggerListener);
126         try {
127             scheduler.scheduleJob(jd, trigger);
128         }
129         catch (SchedulerException e) {
130              throw new ParallelExecutionException(e);
131         }
132 
133         // wait until job has been executed
134         Thread.sleep(definition.getDelay() * 1000 + 100);
135         int timeToSleep = 500;
136         // check every 500ms if job is running
137         while (timeToWait > 0) {
138             List<JobExecutionContext> jobs = scheduler.getCurrentlyExecutingJobs();
139             if (isJobRunning(jobs, jobName)) {
140                 Thread.sleep(timeToSleep);
141             } else {
142                 break;
143             }
144             timeToWait -= timeToSleep;
145         }
146 
147         boolean isRunningInBackground = (timeToWait == 0);
148         // Throw error in case job completed and job result is unsuccessful
149         if (!isRunningInBackground && triggerListener instanceof DefaultAsyncActionExecutor.CommandActionTriggerListener) {
150             CommandActionTriggerListener commandActionTriggerListener = (CommandActionTriggerListener) triggerListener;
151 
152             if (commandActionTriggerListener.getException() != null) {
153                 throw commandActionTriggerListener.getException();
154             }
155         }
156         return isRunningInBackground;
157     }
158 
159     protected TriggerListener getListener(String jobName, JcrItemAdapter item) throws RepositoryException {
160         return new CommandActionTriggerListener(definition, jobName + "_trigger", uiContext, i18n, item.getJcrItem().getPath());
161     }
162 
163     private boolean isJobRunning(List<JobExecutionContext> jobs, String jobName) {
164         for (JobExecutionContext job : jobs) {
165             if (job.getJobDetail().getName().equals(jobName)) {
166                 return true;
167             }
168         }
169         return false;
170     }
171 
172     /**
173      * Takes care of notifying the user about successful or failed executions.
174      */
175     public class CommandActionTriggerListener extends TriggerListenerSupport {
176 
177         private final D definition;
178         private final String name;
179         private final SimpleTranslator i18n;
180         private final String successMessageTitle;
181         private final String successMessage;
182         private final String errorMessageTitle;
183         private final String errorMessage;
184         private Exception exception = null;
185 
186         @Inject
187         public CommandActionTriggerListener(D definition, String triggerName, UiContext uiContext, SimpleTranslator i18n, String path) {
188             this.definition = definition;
189             this.name = triggerName;
190             this.i18n = i18n;
191 
192             String appName = uiContext instanceof SubAppContext ? ((SubAppContext) uiContext).getSubAppDescriptor().getLabel() : null;
193             this.successMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.successTitle", definition.getLabel());
194             this.successMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.successMessage", definition.getLabel(), appName, path);
195             this.errorMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.errorTitle", definition.getLabel());
196             this.errorMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.errorMessage", definition.getLabel(), appName, path);
197         }
198 
199         @Override
200         public String getName() {
201             return name;
202         }
203 
204         @Override
205         public void triggerComplete(final Trigger trigger, final JobExecutionContext jobExecutionContext, int i) {
206             if (!definition.isNotifyUser()) {
207                 return;
208             }
209             MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
210                 @Override
211                 public void doExec() {
212                     // User should be notified always when the long running action has finished. Otherwise he gets NO feedback.
213                     MessagesManager messagesManager = Components.getComponent(MessagesManager.class);
214                     // result 1 stands for success, 0 for error - see info.magnolia.module.scheduler.CommandJob
215                     CommandJob.JobResult result = (CommandJob.JobResult) jobExecutionContext.getResult();
216                     exception = result.getException();
217                     if (result.isSuccess()) {
218                         messagesManager.sendMessage(user.getName(), new Message(MessageType.INFO, successMessageTitle, successMessage));
219                     } else {
220                         Message msg = new Message(MessageType.WARNING, errorMessageTitle, errorMessage);
221                         msg.setView("ui-admincentral:longRunning");
222                         msg.addProperty("exception", ExceptionUtils.getMessage(result.getException()));
223                         msg.addProperty("comment", i18n.translate("ui-framework.abstractcommand.asyncaction.errorComment"));
224                         messagesManager.sendMessage(user.getName(), msg);
225                     }
226                 }
227             });
228         }
229 
230         public Exception getException() {
231             return exception;
232         }
233     }
234 }