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.lang.java.ShellCommandImpl;
036import org.crsh.shell.impl.command.spi.CommandCreationException;
037import org.crsh.shell.impl.command.spi.ShellCommand;
038import org.crsh.lang.groovy.command.GroovyScriptShellCommand;
039import org.crsh.shell.impl.command.CRaSHSession;
040import org.crsh.shell.impl.command.spi.CommandResolution;
041import org.crsh.util.ClassCache;
042import org.crsh.shell.impl.command.spi.CommandManager;
043import org.crsh.lang.groovy.command.GroovyScript;
044import org.crsh.lang.groovy.command.GroovyScriptCommand;
045import org.crsh.plugin.PluginContext;
046import org.crsh.plugin.ResourceKind;
047import org.crsh.shell.ErrorType;
048import org.crsh.util.TimestampedObject;
049
050import java.io.UnsupportedEncodingException;
051import java.util.HashMap;
052import java.util.Set;
053import java.util.logging.Level;
054import java.util.logging.Logger;
055
056/** @author Julien Viet */
057public class GroovyCommandManagerImpl implements CommandManager {
058
059  /** . */
060  static final Logger log = Logger.getLogger(GroovyCommandManagerImpl.class.getName());
061
062  /** . */
063  private ClassCache<GroovyScript> scriptCache;
064
065  /** . */
066  private GroovyClassFactory<Object> objectGroovyClassFactory;
067
068  public GroovyCommandManagerImpl(PluginContext context) {
069    this.objectGroovyClassFactory = new GroovyClassFactory<Object>(context.getLoader(), Object.class, GroovyScriptCommand.class);
070    this.scriptCache = new ClassCache<GroovyScript>(context, new GroovyClassFactory<GroovyScript>(context.getLoader(), GroovyScript.class, GroovyScript.class), ResourceKind.LIFECYCLE);
071  }
072
073  public Set<String> getExtensions() {
074    return GroovyCommandManager.EXT;
075  }
076
077  public boolean isActive() {
078    return true;
079  }
080
081  public String doCallBack(HashMap<String, Object> session, String name, String defaultValue) {
082    return eval(session, name, defaultValue);
083  }
084
085  public void init(HashMap<String, Object> session) {
086    try {
087      GroovyScript login = getLifeCycle(session, "login");
088      if (login != null) {
089        login.setBinding(new Binding(session));
090        login.run();
091      }
092    }
093    catch (CommandCreationException e) {
094      e.printStackTrace();
095    }
096  }
097
098  public void destroy(HashMap<String, Object> session) {
099    try {
100      GroovyScript logout = getLifeCycle(session, "logout");
101      if (logout != null) {
102        logout.setBinding(new Binding(session));
103        logout.run();
104      }
105    }
106    catch (CommandCreationException e) {
107      e.printStackTrace();
108    }
109  }
110
111  /**
112   * The underlying groovu shell used for the REPL.
113   *
114   * @return a groovy shell operating on the session attributes
115   */
116  public static GroovyShell getGroovyShell(CRaSHSession session) {
117    GroovyShell shell = (GroovyShell)session.get("shell");
118    if (shell == null) {
119      CompilerConfiguration config = new CompilerConfiguration();
120      config.setRecompileGroovySource(true);
121      ShellBinding binding = new ShellBinding(session, session);
122      shell = new GroovyShell(session.crash.getContext().getLoader(), binding, config);
123      session.put("shell", shell);
124    }
125    return shell;
126  }
127
128  private String eval(HashMap<String, Object> session, String name, String def) {
129    try {
130      GroovyShell shell = getGroovyShell((CRaSHSession)session);
131      Object ret = shell.getContext().getVariable(name);
132      if (ret instanceof Closure) {
133        log.log(Level.FINEST, "Invoking " + name + " closure");
134        Closure c = (Closure)ret;
135        ret = c.call();
136      } else if (ret == null) {
137        log.log(Level.FINEST, "No " + name + " will use empty");
138        return def;
139      }
140      return String.valueOf(ret);
141    }
142    catch (Exception e) {
143      log.log(Level.SEVERE, "Could not get a " + name + " message, will use empty", e);
144      return def;
145    }
146  }
147
148  public GroovyScript getLifeCycle(HashMap<String, Object> session, String name) throws CommandCreationException, NullPointerException {
149    TimestampedObject<Class<? extends GroovyScript>> ref = scriptCache.getClass(name);
150    if (ref != null) {
151      Class<? extends GroovyScript> scriptClass = ref.getObject();
152      GroovyScript script = (GroovyScript)InvokerHelper.createScript(scriptClass, new Binding(session));
153      script.setBinding(new Binding(session));
154      return script;
155    } else {
156      return null;
157    }
158  }
159
160  public CommandResolution resolveCommand(final String name, byte[] source) throws CommandCreationException, NullPointerException {
161
162    //
163    if (source == null) {
164      throw new NullPointerException("No null command source allowed");
165    }
166
167    //
168    final String script;
169    try {
170      script = new String(source, "UTF-8");
171    }
172    catch (UnsupportedEncodingException e) {
173      throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not compile command script " + name, e);
174    }
175
176    // Get the description using a partial compilation because it is much faster than compiling the class
177    // the class will be compiled lazyly
178    String resolveDescription = null;
179    CompilationUnit cu = new CompilationUnit(objectGroovyClassFactory.config);
180    cu.addSource(name, script);
181    try {
182      cu.compile(Phases.CONVERSION);
183    }
184    catch (CompilationFailedException e) {
185      throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not compile command", e);
186    }
187    CompileUnit ast = cu.getAST();
188    if (ast.getClasses().size() > 0) {
189      ClassNode classNode= (ClassNode)ast.getClasses().get(0);
190      if (classNode != null) {
191        for (AnnotationNode annotation : classNode.getAnnotations()) {
192          if (annotation.getClassNode().getName().equals(Usage.class.getSimpleName())) {
193            resolveDescription = annotation.getMember("value").getText();
194            break;
195          }
196        }
197        if (resolveDescription == null) {
198          for (MethodNode main : classNode.getMethods("main")) {
199            for (AnnotationNode annotation : main.getAnnotations()) {
200              if (annotation.getClassNode().getName().equals(Usage.class.getSimpleName())) {
201                resolveDescription = annotation.getMember("value").getText();
202                break;
203              }
204            }
205          }
206        }
207      }
208    }
209    final String description = resolveDescription;
210
211    //
212    return new CommandResolution() {
213      ShellCommand<?> command;
214      @Override
215      public String getDescription() {
216        return description;
217      }
218      @Override
219      public ShellCommand<?> getCommand() throws CommandCreationException {
220        if (command == null) {
221          Class<?> clazz = objectGroovyClassFactory.parse(name, script);
222          if (BaseCommand.class.isAssignableFrom(clazz)) {
223            Class<? extends BaseCommand> cmd = clazz.asSubclass(BaseCommand.class);
224            command = make(cmd);
225          }
226          else if (GroovyScriptCommand.class.isAssignableFrom(clazz)) {
227            Class<? extends GroovyScriptCommand> cmd = clazz.asSubclass(GroovyScriptCommand.class);
228            command = make2(cmd);
229          }
230          else {
231            throw new CommandCreationException(name, ErrorType.INTERNAL, "Could not create command " + name + " instance");
232          }
233        }
234        return command;
235      }
236    };
237  }
238
239  private <C extends BaseCommand> ShellCommandImpl<C> make(Class<C> clazz) {
240    return new ShellCommandImpl<C>(clazz);
241  }
242  private <C extends GroovyScriptCommand> GroovyScriptShellCommand<C> make2(Class<C> clazz) {
243    return new GroovyScriptShellCommand<C>(clazz);
244  }
245}