001/*
002 * Copyright (C) 2012 eXo Platform SAS.
003 *
004 * This is free software; you can redistribute it and/or modify it
005 * under the terms of the GNU Lesser General Public License as
006 * published by the Free Software Foundation; either version 2.1 of
007 * the License, or (at your option) any later version.
008 *
009 * This software is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public
015 * License along with this software; if not, write to the Free
016 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
017 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
018 */
019package org.crsh.lang.groovy;
020
021import groovy.lang.Binding;
022import groovy.lang.Closure;
023import groovy.lang.GroovyShell;
024import org.codehaus.groovy.ast.AnnotationNode;
025import org.codehaus.groovy.ast.ClassNode;
026import org.codehaus.groovy.ast.CompileUnit;
027import org.codehaus.groovy.ast.MethodNode;
028import org.codehaus.groovy.control.CompilationFailedException;
029import org.codehaus.groovy.control.CompilationUnit;
030import org.codehaus.groovy.control.CompilerConfiguration;
031import org.codehaus.groovy.control.Phases;
032import org.codehaus.groovy.runtime.InvokerHelper;
033import org.crsh.cli.Usage;
034import org.crsh.command.BaseCommand;
035import org.crsh.command.BaseShellCommand;
036import org.crsh.command.CommandCreationException;
037import org.crsh.command.ShellCommand;
038import org.crsh.lang.groovy.command.GroovyScriptShellCommand;
039import org.crsh.plugin.CRaSHPlugin;
040import org.crsh.shell.impl.command.CommandResolution;
041import org.crsh.util.AbstractClassCache;
042import org.crsh.util.ClassCache;
043import org.crsh.shell.impl.command.CommandManager;
044import org.crsh.lang.groovy.command.GroovyScript;
045import org.crsh.lang.groovy.command.GroovyScriptCommand;
046import org.crsh.plugin.PluginContext;
047import org.crsh.plugin.ResourceKind;
048import org.crsh.shell.ErrorType;
049import org.crsh.util.TimestampedObject;
050
051import java.io.UnsupportedEncodingException;
052import java.util.Collections;
053import java.util.HashMap;
054import java.util.Map;
055import java.util.Set;
056import java.util.logging.Level;
057import java.util.logging.Logger;
058
059/** @author Julien Viet */
060public class GroovyCommandManager extends CRaSHPlugin<CommandManager> implements CommandManager {
061
062  /** . */
063  static final Logger log = Logger.getLogger(GroovyCommandManager.class.getName());
064
065  /** . */
066  static final Set<String> EXT = Collections.singleton("groovy");
067
068  /** . */
069  private AbstractClassCache<GroovyScript> scriptCache;
070
071  /** . */
072  private GroovyClassFactory<Object> objectGroovyClassFactory;
073
074  public GroovyCommandManager() {
075  }
076
077  @Override
078  public CommandManager getImplementation() {
079    return this;
080  }
081
082  public Set<String> getExtensions() {
083    return EXT;
084  }
085
086  @Override
087  public void init() {
088    PluginContext context = getContext();
089
090    //
091    this.objectGroovyClassFactory = new GroovyClassFactory<Object>(context.getLoader(), Object.class, GroovyScriptCommand.class);
092    this.scriptCache = new ClassCache<GroovyScript>(context, new GroovyClassFactory<GroovyScript>(context.getLoader(), GroovyScript.class, GroovyScript.class), ResourceKind.LIFECYCLE);
093  }
094
095  public String doCallBack(HashMap<String, Object> session, String name, String defaultValue) {
096    return eval(session, name, defaultValue);
097  }
098
099  public void init(HashMap<String, Object> session) {
100    try {
101      GroovyScript login = getLifeCycle(session, "login");
102      if (login != null) {
103        login.setBinding(new Binding(session));
104        login.run();
105      }
106    }
107    catch (CommandCreationException e) {
108      e.printStackTrace();
109    }
110  }
111
112  public void destroy(HashMap<String, Object> session) {
113    try {
114      GroovyScript logout = getLifeCycle(session, "logout");
115      if (logout != null) {
116        logout.setBinding(new Binding(session));
117        logout.run();
118      }
119    }
120    catch (CommandCreationException e) {
121      e.printStackTrace();
122    }
123  }
124
125  public GroovyShell getGroovyShell(Map<String, Object> session) {
126    return getGroovyShell(getContext(), session);
127  }
128
129  /**
130   * The underlying groovu shell used for the REPL.
131   *
132   * @return a groovy shell operating on the session attributes
133   */
134  public static GroovyShell getGroovyShell(PluginContext context, Map<String, Object> session) {
135    GroovyShell shell = (GroovyShell)session.get("shell");
136    if (shell == null) {
137      CompilerConfiguration config = new CompilerConfiguration();
138      config.setRecompileGroovySource(true);
139      ShellBinding binding = new ShellBinding(session);
140      shell = new GroovyShell(context.getLoader(), binding, config);
141      session.put("shell", shell);
142    }
143    return shell;
144  }
145
146  private String eval(HashMap<String, Object> session, String name, String def) {
147    try {
148      GroovyShell shell = getGroovyShell(session);
149      Object ret = shell.getContext().getVariable(name);
150      if (ret instanceof Closure) {
151        log.log(Level.FINEST, "Invoking " + name + " closure");
152        Closure c = (Closure)ret;
153        ret = c.call();
154      } else if (ret == null) {
155        log.log(Level.FINEST, "No " + name + " will use empty");
156        return def;
157      }
158      return String.valueOf(ret);
159    }
160    catch (Exception e) {
161      log.log(Level.SEVERE, "Could not get a " + name + " message, will use empty", e);
162      return def;
163    }
164  }
165
166  public GroovyScript getLifeCycle(HashMap<String, Object> session, String name) throws CommandCreationException, NullPointerException {
167    TimestampedObject<Class<? extends GroovyScript>> ref = scriptCache.getClass(name);
168    if (ref != null) {
169      Class<? extends GroovyScript> scriptClass = ref.getObject();
170      GroovyScript script = (GroovyScript)InvokerHelper.createScript(scriptClass, new Binding(session));
171      script.setBinding(new Binding(session));
172      return script;
173    } else {
174      return null;
175    }
176  }
177
178  public CommandResolution resolveCommand(final String name, byte[] source) throws CommandCreationException, NullPointerException {
179
180    //
181    if (source == null) {
182      throw new NullPointerException("No null command source allowed");
183    }
184
185    //
186    final String script;
187    try {
188      script = new String(source, "UTF-8");
189    }
190    catch (UnsupportedEncodingException e) {
191      throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not compile command script " + name, e);
192    }
193
194    // Get the description using a partial compilation because it is much faster than compiling the class
195    // the class will be compiled lazyly
196    String resolveDescription = null;
197    CompilationUnit cu = new CompilationUnit(objectGroovyClassFactory.config);
198    cu.addSource(name, script);
199    try {
200      cu.compile(Phases.CONVERSION);
201    }
202    catch (CompilationFailedException e) {
203      throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not compile command", e);
204    }
205    CompileUnit ast = cu.getAST();
206    if (ast.getClasses().size() > 0) {
207      ClassNode classNode= (ClassNode)ast.getClasses().get(0);
208      if (classNode != null) {
209        for (AnnotationNode annotation : classNode.getAnnotations()) {
210          if (annotation.getClassNode().getName().equals(Usage.class.getSimpleName())) {
211            resolveDescription = annotation.getMember("value").getText();
212            break;
213          }
214        }
215        if (resolveDescription == null) {
216          for (MethodNode main : classNode.getMethods("main")) {
217            for (AnnotationNode annotation : main.getAnnotations()) {
218              if (annotation.getClassNode().getName().equals(Usage.class.getSimpleName())) {
219                resolveDescription = annotation.getMember("value").getText();
220                break;
221              }
222            }
223          }
224        }
225      }
226    }
227    final String description = resolveDescription;
228
229    //
230    return new CommandResolution() {
231      ShellCommand command;
232      @Override
233      public String getDescription() {
234        return description;
235      }
236      @Override
237      public ShellCommand getCommand() throws CommandCreationException {
238        if (command == null) {
239          Class<?> clazz = objectGroovyClassFactory.parse(name, script);
240          if (BaseCommand.class.isAssignableFrom(clazz)) {
241            Class<? extends BaseCommand> cmd = clazz.asSubclass(BaseCommand.class);
242            command = make(cmd);
243          }
244          else if (GroovyScriptCommand.class.isAssignableFrom(clazz)) {
245            Class<? extends GroovyScriptCommand> cmd = clazz.asSubclass(GroovyScriptCommand.class);
246            command = make2(cmd);
247          }
248          else {
249            throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not create command " + name + " instance");
250          }
251        }
252        return command;
253      }
254    };
255  }
256
257  private <C extends BaseCommand> BaseShellCommand<C> make(Class<C> clazz) {
258    return new BaseShellCommand<C>(clazz);
259  }
260  private <C extends GroovyScriptCommand> GroovyScriptShellCommand<C> make2(Class<C> clazz) {
261    return new GroovyScriptShellCommand<C>(clazz);
262  }
263}