View Javadoc
1   /**
2    * This file Copyright (c) 2015 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         scheduler.addTriggerListener(getListener(jobName, item));
125         try {
126             scheduler.scheduleJob(jd, trigger);
127         }
128         catch (SchedulerException e) {
129              throw new ParallelExecutionException(e);
130         }
131 
132         // wait until job has been executed
133         Thread.sleep(definition.getDelay() * 1000 + 100);
134         int timeToSleep = 500;
135         // check every 500ms if job is running
136         while (timeToWait > 0) {
137             List<JobExecutionContext> jobs = scheduler.getCurrentlyExecutingJobs();
138             if (isJobRunning(jobs, jobName)) {
139                 Thread.sleep(timeToSleep);
140             } else {
141                 break;
142             }
143             timeToWait -= timeToSleep;
144         }
145 
146         // action execution is running in background
147         return (timeToWait == 0);
148     }
149 
150     protected TriggerListener getListener(String jobName, JcrItemAdapter item) throws RepositoryException {
151         return new CommandActionTriggerListener(definition, jobName + "_trigger", uiContext, i18n, item.getJcrItem().getPath());
152     }
153 
154     private boolean isJobRunning(List<JobExecutionContext> jobs, String jobName) {
155         for (JobExecutionContext job : jobs) {
156             if (job.getJobDetail().getName().equals(jobName)) {
157                 return true;
158             }
159         }
160         return false;
161     }
162 
163     /**
164      * Takes care of notifying the user about successful or failed executions.
165      */
166     public class CommandActionTriggerListener extends TriggerListenerSupport {
167 
168         private final D definition;
169         private final String name;
170         private final SimpleTranslator i18n;
171         private final String successMessageTitle;
172         private final String successMessage;
173         private final String errorMessageTitle;
174         private final String errorMessage;
175 
176         @Inject
177         public CommandActionTriggerListener(D definition, String triggerName, UiContext uiContext, SimpleTranslator i18n, String path) {
178             this.definition = definition;
179             this.name = triggerName;
180             this.i18n = i18n;
181 
182             String appName = uiContext instanceof SubAppContext ? ((SubAppContext) uiContext).getSubAppDescriptor().getLabel() : null;
183             this.successMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.successTitle", definition.getLabel());
184             this.successMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.successMessage", definition.getLabel(), appName, path);
185             this.errorMessageTitle = i18n.translate("ui-framework.abstractcommand.asyncaction.errorTitle", definition.getLabel());
186             this.errorMessage = i18n.translate("ui-framework.abstractcommand.asyncaction.errorMessage", definition.getLabel(), appName, path);
187         }
188 
189         @Override
190         public String getName() {
191             return name;
192         }
193 
194         @Override
195         public void triggerComplete(final Trigger trigger, final JobExecutionContext jobExecutionContext, int i) {
196             if (!definition.isNotifyUser()) {
197                 return;
198             }
199             MgnlContext.doInSystemContext(new MgnlContext.VoidOp() {
200                 @Override
201                 public void doExec() {
202                     // User should be notified always when the long running action has finished. Otherwise he gets NO feedback.
203                     MessagesManager messagesManager = Components.getComponent(MessagesManager.class);
204                     // result 1 stands for success, 0 for error - see info.magnolia.module.scheduler.CommandJob
205                     CommandJob.JobResult result = (CommandJob.JobResult) jobExecutionContext.getResult();
206                     if (result.isSuccess()) {
207                         messagesManager.sendMessage(user.getName(), new Message(MessageType.INFO, successMessageTitle, successMessage));
208                     } else {
209                         Message msg = new Message(MessageType.WARNING, errorMessageTitle, errorMessage);
210                         msg.setView("ui-admincentral:longRunning");
211                         msg.addProperty("exception", ExceptionUtils.getMessage(result.getException()));
212                         msg.addProperty("comment", i18n.translate("ui-framework.abstractcommand.asyncaction.errorComment"));
213                         messagesManager.sendMessage(user.getName(), msg);
214                     }
215                 }
216             });
217         }
218     }
219 }