View Javadoc
1   /**
2    * This file Copyright (c) 2003-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.module.groovy.console;
35  
36  import info.magnolia.context.Context;
37  import info.magnolia.context.MgnlContext;
38  import info.magnolia.i18nsystem.SimpleTranslator;
39  import info.magnolia.module.groovy.support.classes.MgnlGroovyClassLoader;
40  import info.magnolia.objectfactory.Components;
41  import info.magnolia.ui.api.message.Message;
42  import info.magnolia.ui.api.message.MessageType;
43  import info.magnolia.ui.framework.message.MessagesManager;
44  
45  import java.io.ByteArrayInputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.io.InputStreamReader;
49  import java.io.StringWriter;
50  import java.io.Writer;
51  import java.util.Objects;
52  import java.util.concurrent.Callable;
53  import java.util.concurrent.ExecutionException;
54  import java.util.concurrent.ExecutorService;
55  import java.util.concurrent.Executors;
56  import java.util.concurrent.Future;
57  
58  import org.apache.commons.lang3.CharEncoding;
59  import org.codehaus.groovy.control.CompilationFailedException;
60  import org.codehaus.groovy.runtime.InvokerHelper;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  import com.vaadin.event.UIEvents.PollEvent;
65  import com.vaadin.event.UIEvents.PollListener;
66  import com.vaadin.ui.UI;
67  
68  import groovy.lang.Binding;
69  import groovy.lang.GroovyCodeSource;
70  import groovy.lang.GroovyShell;
71  import groovy.lang.Script;
72  
73  /**
74   * Groovy console using {@link MgnlGroovyClassLoader} to parse Groovy source.
75   */
76  public class MgnlGroovyConsole extends GroovyShell {
77  
78      private static final Logger log = LoggerFactory.getLogger(MgnlGroovyConsole.class);
79  
80      private static final MgnlGroovyClassLoader GROOVY_CLASS_LOADER = new MgnlGroovyClassLoader();
81  
82      private static final int POLL_INTERVAL = 1000;
83  
84      private MessagesManager messagesManager;
85  
86      private SimpleTranslator i18n;
87  
88      /**
89       * @deprecated since 2.4.6, please use {@link MgnlGroovyConsole#MgnlGroovyConsole(Binding, MessagesManager, SimpleTranslator))}
90       */
91      public MgnlGroovyConsole(Binding binding) {
92          this(binding, Components.getComponent(MessagesManager.class), Components.getComponent(SimpleTranslator.class));
93      }
94  
95      public MgnlGroovyConsole(Binding binding, MessagesManager messagesManager, SimpleTranslator simpleTranslator) {
96          super(binding);
97          this.messagesManager = messagesManager;
98          this.i18n = simpleTranslator;
99      }
100 
101     /**
102      * Creates and run a {@link Script} synchronously.
103      * {@link MgnlContext} is wrapped into a {@link MgnlGroovyConsoleContext} for the script running time
104      * and then set back to its original value.
105      * 
106      * @deprecated since 2.4.6, please use {@link #runAsync(String, ScriptCallback)} instead.
107      */
108     @Deprecated
109     public Object evaluate(InputStream in, String fileName, Writer out) throws CompilationFailedException {
110         Context originalCtx = MgnlContext.getInstance();
111         MgnlGroovyConsoleContext groovyCtx = new MgnlGroovyConsoleContext(originalCtx);
112         MgnlContext.setInstance(groovyCtx);
113 
114         Script script = null;
115         try {
116             script = createScript(in, out);
117             script.setProperty("ctx", MgnlContext.getInstance());
118             return script.run();
119         } catch (IOException e) {
120             throw new RuntimeException(e);
121         } finally {
122             if (script != null) {
123                 InvokerHelper.removeClass(script.getClass());
124             }
125             MgnlContext.setInstance(originalCtx);
126         }
127     }
128 
129     /**
130      * Runs a {@link Script} asynchronously in a separate thread from the UI one.
131      * This prevents AdminCentral from being frozen until the script is done, in case of long running scripts.
132      * <p>
133      * {@link MgnlContext} is wrapped into a {@link MgnlGroovyConsoleContext} within the thread running the script.
134      *
135      * @throws IOException
136      * @throws InterruptedException
137      */
138     public void runAsync(String source, UI ui, ScriptCallback callback) throws IOException, InterruptedException {
139         Writer out = new StringWriter();
140         final Script script = createScript(source, out);
141 
142         final Context originalCtx = MgnlContext.getInstance();
143         final MgnlGroovyConsoleContext groovyCtx = new MgnlGroovyConsoleContext(originalCtx);
144 
145         ExecutorService executorService = Executors.newSingleThreadExecutor();
146 
147         final Future<Object> task = executorService.submit(new Callable<Object>() {
148             @Override
149             public Object call() throws Exception {
150                 // this context is local to the thread running the script
151                 // which is separate from the UI one, so no need to reset it.
152                 MgnlContext.setInstance(groovyCtx);
153                 script.setProperty("ctx", groovyCtx);
154                 return script.run();
155             }
156         });
157 
158         // Allow a quick task to finish w/o starting polling. If done, just return.
159         // Otherwise users would likely get the impression of a sluggish console which needs one or two seconds
160         // just to return e.g. ctx.getJCRSession('website') or 1+1
161         Thread.sleep(200);
162         if (task.isDone()) {
163             handleScriptDone(callback, task, ui, out, messagesManager, i18n, false);
164             return;
165         }
166 
167         // Remove any PollListener(s) possibly registered by us in a previous script run
168         for (Object listener : ui.getListeners(PollEvent.class)) {
169             if (listener instanceof ScriptDonePollListener) {
170                 ui.removePollListener((ScriptDonePollListener) listener);
171             }
172         }
173 
174         // if some other component already set a poll interval but it's less than ours, we go for the higher value
175         if (ui.getPollInterval() < POLL_INTERVAL) {
176             ui.setPollInterval(POLL_INTERVAL);
177         }
178         ui.addPollListener(new ScriptDonePollListener(task, ui, out, callback, messagesManager, i18n));
179     }
180 
181     private Script createScript(String code, Writer out) throws CompilationFailedException, IOException {
182         return createScript(new ByteArrayInputStream(code.getBytes()), out);
183     }
184 
185     private Script createScript(InputStream in, Writer out) throws CompilationFailedException, IOException {
186         Script script = parse(new InputStreamReader(in, CharEncoding.UTF_8), generateScriptName());
187         script.setProperty("out", out);
188         script.setProperty("err", out);
189         return script;
190     }
191 
192     /**
193      * Parses the given script and returns it ready to be run. When running in a secure environment
194      * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be given to the script.
195      *
196      * @param codeSource
197      * @return ready to run script
198      */
199     @Override
200     public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
201         return InvokerHelper.createScript(parseClass(codeSource), getContext());
202     }
203 
204     @Override
205     public String generateScriptName() {
206         // needed to expose it publicly (it's protected in parent class)
207         return super.generateScriptName();
208     }
209 
210     /**
211      * Parses the groovy code contained in codeSource and returns a java class.
212      */
213     private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
214         // Don't cache scripts
215         return GROOVY_CLASS_LOADER.parseClass(codeSource, false);
216     }
217 
218     /**
219      * Callback to handle script progress and result asynchronously.
220      */
221     public interface ScriptCallback {
222         /**
223          * Called when the script is completed successfully.
224          *
225          * @param result the script outcome, including everything which has been printed to stdout during execution.
226          */
227         void onSuccess(String result);
228 
229         /**
230          * Called when the script is completed with an error.
231          *
232          * @param e the error occurred during script execution
233          */
234         void onFailure(Throwable e);
235 
236         /**
237          * Called when the script is running at each poll interval.
238          *
239          * @param out the script output thus far
240          */
241         void onProgress(Writer out);
242 
243         /**
244          * Called when the script is completed, either successfully or with an error.
245          */
246         boolean requiresNotificationMessageUponCompletion();
247     }
248 
249     private static void handleScriptDone(ScriptCallback callback, final Future<Object> scriptTask, UI ui, Writer out, MessagesManager messagesManager, SimpleTranslator i18n, boolean sendNotificationMessage) {
250         try {
251             String resultAsString = scriptTask.get() == null ? "" : Objects.toString(scriptTask.get());
252             String completeOutput;
253             if (out.toString().isEmpty()) {
254                 completeOutput = resultAsString;
255             } else {
256                 completeOutput = out.toString().concat("\n").concat(resultAsString);
257             }
258 
259             log.debug("Script run successfully with result {}", completeOutput);
260             callback.onSuccess(completeOutput);
261             if (sendNotificationMessage) {
262                 messagesManager.sendLocalMessage(new Message(MessageType.INFO, i18n.translate("groovy.script.console.done.success"), completeOutput));
263             }
264 
265         } catch (InterruptedException | ExecutionException e) {
266             log.error("An error occurred while running a Groovy script", e);
267             callback.onFailure(e);
268 
269             if (sendNotificationMessage) {
270                 messagesManager.sendLocalMessage(new Message(MessageType.WARNING, i18n.translate("groovy.script.console.done.error"), e.getMessage()));
271             }
272         } finally {
273             log.debug("Stop polling");
274             ui.setPollInterval(-1);
275         }
276     }
277 
278     @SuppressWarnings("serial")
279     private static final class ScriptDonePollListener implements PollListener {
280         private Future<Object> scriptTask;
281         private UI ui;
282         private ScriptCallback callback;
283         private Writer out;
284         private MessagesManager messagesManager;
285         private SimpleTranslator i18n;
286 
287         public ScriptDonePollListener(Future<Object> scriptTask, UI ui, Writer out, ScriptCallback callback, MessagesManager messagesManager, SimpleTranslator i18n) {
288             this.scriptTask = scriptTask;
289             this.ui = ui;
290             this.callback = callback;
291             this.out = out;
292             this.messagesManager = messagesManager;
293             this.i18n = i18n;
294         }
295 
296         @Override
297         public void poll(PollEvent event) {
298             if (scriptTask.isDone()) {
299                 handleScriptDone(callback, scriptTask, ui, out, messagesManager, i18n, callback.requiresNotificationMessageUponCompletion());
300             } else {
301                 // make sure we keep on polling until done.
302                 // it may happen that a different component using polling ends before us and stops before we're done.
303                 // if some other component already set a poll interval but it's less than ours, we go for the higher value
304                 if (ui.getPollInterval() < POLL_INTERVAL) {
305                     ui.setPollInterval(POLL_INTERVAL);
306                 }
307                 callback.onProgress(out);
308             }
309         }
310     }
311 }