View Javadoc
1   /**
2    * This file Copyright (c) 2003-2017 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_MS = 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, UI, 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         // if some other component already set a poll interval but it's less than ours, we go for the higher value
168         if (ui.getPollInterval() < POLL_INTERVAL_MS) {
169             ui.setPollInterval(POLL_INTERVAL_MS);
170         }
171         ui.addPollListener(new ScriptDonePollListener(task, ui, out, callback, messagesManager, i18n));
172     }
173 
174     private Script createScript(String code, Writer out) throws CompilationFailedException, IOException {
175         return createScript(new ByteArrayInputStream(code.getBytes()), out);
176     }
177 
178     private Script createScript(InputStream in, Writer out) throws CompilationFailedException, IOException {
179         Script script = parse(new InputStreamReader(in, CharEncoding.UTF_8), generateScriptName());
180         script.setProperty("out", out);
181         script.setProperty("err", out);
182         return script;
183     }
184 
185     /**
186      * Parses the given script and returns it ready to be run. When running in a secure environment
187      * (-Djava.security.manager) codeSource.getCodeSource() determines what policy grants should be given to the script.
188      *
189      * @param codeSource
190      * @return ready to run script
191      */
192     @Override
193     public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
194         return InvokerHelper.createScript(parseClass(codeSource), getContext());
195     }
196 
197     @Override
198     public String generateScriptName() {
199         // needed to expose it publicly (it's protected in parent class)
200         return super.generateScriptName();
201     }
202 
203     /**
204      * Parses the groovy code contained in codeSource and returns a java class.
205      */
206     private Class parseClass(final GroovyCodeSource codeSource) throws CompilationFailedException {
207         // Don't cache scripts
208         return GROOVY_CLASS_LOADER.parseClass(codeSource, false);
209     }
210 
211     /**
212      * Callback to handle script progress and result asynchronously.
213      */
214     public interface ScriptCallback {
215         /**
216          * Called when the script is completed successfully.
217          *
218          * @param result the script outcome, including everything which has been printed to stdout during execution.
219          */
220         void onSuccess(String result);
221 
222         /**
223          * Called when the script is completed with an error.
224          *
225          * @param e the error occurred during script execution
226          */
227         void onFailure(Throwable e);
228 
229         /**
230          * Called when the script is running at each poll interval.
231          *
232          * @param out the script output thus far
233          */
234         void onProgress(Writer out);
235 
236         /**
237          * Called when the script is completed, either successfully or with an error.
238          */
239         boolean requiresNotificationMessageUponCompletion();
240     }
241 
242     private static void handleScriptDone(ScriptCallback callback, final Future<Object> scriptTask, UI ui, Writer out, MessagesManager messagesManager, SimpleTranslator i18n, boolean sendNotificationMessage) {
243         try {
244             String resultAsString = scriptTask.get() == null ? "" : Objects.toString(scriptTask.get());
245             String completeOutput;
246             if (out.toString().isEmpty()) {
247                 completeOutput = resultAsString;
248             } else {
249                 completeOutput = out.toString().concat("\n").concat(resultAsString);
250             }
251 
252             log.debug("Script run successfully with result {}", completeOutput);
253             callback.onSuccess(completeOutput);
254             if (sendNotificationMessage) {
255                 messagesManager.sendLocalMessage(new Message(MessageType.INFO, i18n.translate("groovy.script.console.done.success"), completeOutput));
256             }
257 
258         } catch (InterruptedException | ExecutionException e) {
259             log.error("An error occurred while running a Groovy script", e);
260             callback.onFailure(e);
261 
262             if (sendNotificationMessage) {
263                 messagesManager.sendLocalMessage(new Message(MessageType.WARNING, i18n.translate("groovy.script.console.done.error"), e.getMessage()));
264             }
265         } finally {
266             log.debug("Stop polling");
267             ui.setPollInterval(-1);
268         }
269     }
270 
271     @SuppressWarnings("serial")
272     private static final class ScriptDonePollListener implements PollListener {
273         private Future<Object> scriptTask;
274         private UI ui;
275         private ScriptCallback callback;
276         private Writer out;
277         private MessagesManager messagesManager;
278         private SimpleTranslator i18n;
279 
280         public ScriptDonePollListener(Future<Object> scriptTask, UI ui, Writer out, ScriptCallback callback, MessagesManager messagesManager, SimpleTranslator i18n) {
281             this.scriptTask = scriptTask;
282             this.ui = ui;
283             this.callback = callback;
284             this.out = out;
285             this.messagesManager = messagesManager;
286             this.i18n = i18n;
287         }
288 
289         @Override
290         public void poll(PollEvent event) {
291             if (scriptTask.isDone()) {
292                 handleScriptDone(callback, scriptTask, ui, out, messagesManager, i18n, callback.requiresNotificationMessageUponCompletion());
293                 // we're done, we can remove ourselves from UI's PollListener(s)
294                 ui.removePollListener(this);
295             } else {
296                 // make sure we keep on polling until done.
297                 // it may happen that a different component using polling ends before us and stops before we're done.
298                 // if some other component already set a poll interval but it's less than ours, we go for the higher value
299                 if (ui.getPollInterval() < POLL_INTERVAL_MS) {
300                     ui.setPollInterval(POLL_INTERVAL_MS);
301                 }
302                 callback.onProgress(out);
303             }
304         }
305     }
306 }