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}