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     */
019    package org.crsh.lang.groovy;
020    
021    import groovy.lang.Binding;
022    import groovy.lang.Closure;
023    import groovy.lang.GroovyShell;
024    import org.codehaus.groovy.ast.AnnotationNode;
025    import org.codehaus.groovy.ast.ClassNode;
026    import org.codehaus.groovy.ast.CompileUnit;
027    import org.codehaus.groovy.ast.MethodNode;
028    import org.codehaus.groovy.control.CompilationFailedException;
029    import org.codehaus.groovy.control.CompilationUnit;
030    import org.codehaus.groovy.control.CompilerConfiguration;
031    import org.codehaus.groovy.control.Phases;
032    import org.codehaus.groovy.runtime.InvokerHelper;
033    import org.crsh.cli.Usage;
034    import org.crsh.command.BaseCommand;
035    import org.crsh.lang.java.ShellCommandImpl;
036    import org.crsh.shell.impl.command.spi.CommandCreationException;
037    import org.crsh.shell.impl.command.spi.ShellCommand;
038    import org.crsh.lang.groovy.command.GroovyScriptShellCommand;
039    import org.crsh.shell.impl.command.CRaSHSession;
040    import org.crsh.shell.impl.command.spi.CommandResolution;
041    import org.crsh.util.ClassCache;
042    import org.crsh.shell.impl.command.spi.CommandManager;
043    import org.crsh.lang.groovy.command.GroovyScript;
044    import org.crsh.lang.groovy.command.GroovyScriptCommand;
045    import org.crsh.plugin.PluginContext;
046    import org.crsh.plugin.ResourceKind;
047    import org.crsh.shell.ErrorType;
048    import org.crsh.util.TimestampedObject;
049    
050    import java.io.UnsupportedEncodingException;
051    import java.util.HashMap;
052    import java.util.Set;
053    import java.util.logging.Level;
054    import java.util.logging.Logger;
055    
056    /** @author Julien Viet */
057    public 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    }