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