001    /*
002     * Copyright (C) 2003-2009 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.shell.impl;
020    
021    import groovy.lang.Binding;
022    import groovy.lang.GroovyShell;
023    import org.codehaus.groovy.control.CompilationFailedException;
024    import org.codehaus.groovy.control.CompilerConfiguration;
025    import org.crsh.command.CommandInvoker;
026    import org.crsh.command.ShellCommand;
027    import org.crsh.plugin.PluginContext;
028    import org.crsh.plugin.ResourceKind;
029    import org.crsh.shell.ErrorType;
030    import org.crsh.shell.Shell;
031    import org.crsh.shell.ShellProcess;
032    import org.crsh.shell.ShellProcessContext;
033    import org.crsh.util.TimestampedObject;
034    import org.crsh.util.Utils;
035    import org.crsh.vfs.Resource;
036    import org.slf4j.Logger;
037    import org.slf4j.LoggerFactory;
038    
039    import java.util.Collections;
040    import java.util.HashMap;
041    import java.util.Map;
042    import java.util.concurrent.ConcurrentHashMap;
043    
044    /**
045     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
046     * @version $Revision$
047     */
048    public class CRaSH implements Shell {
049    
050      /** . */
051      static final Logger log = LoggerFactory.getLogger(CRaSH.class);
052    
053      /** . */
054      private final GroovyShell groovyShell;
055    
056      /** . */
057      private final PluginContext context;
058    
059      /** . */
060      private final Map<String, TimestampedObject<Class<? extends ShellCommand>>> commands;
061    
062      /** . */
063      final Map<String, Object> attributes;
064    
065      /**
066       * Attempt to obtain a command instance. Null is returned when such command does not exist.
067       *
068       * @param name the command name
069       * @return a command instance
070       * @throws CreateCommandException if an error occured preventing the command creation
071       * @throws NullPointerException if the name argument is null
072       */
073      public ShellCommand getCommand(String name) throws CreateCommandException, NullPointerException {
074        if (name == null) {
075          throw new NullPointerException("No null argument alloed");
076        }
077    
078        TimestampedObject<Class<? extends ShellCommand>> providerRef = commands.get(name);
079    
080        //
081        Resource script = context.loadResource(name, ResourceKind.SCRIPT);
082    
083        //
084        if (script != null) {
085          if (providerRef != null) {
086            if (script.getTimestamp() != providerRef.getTimestamp()) {
087              providerRef = null;
088            }
089          }
090    
091          //
092          if (providerRef == null) {
093    
094            Class<?> clazz;
095            try {
096              clazz = groovyShell.getClassLoader().parseClass(script.getContent(), name);
097            }
098            catch (CompilationFailedException e) {
099              throw new CreateCommandException(ErrorType.INTERNAL, "Could not compile command script", e);
100            }
101    
102            //
103            if (ShellCommand.class.isAssignableFrom(clazz)) {
104              Class<? extends ShellCommand> providerClass = clazz.asSubclass(ShellCommand.class);
105              providerRef = new TimestampedObject<Class<? extends ShellCommand>>(script.getTimestamp(), providerClass);
106              commands.put(name, providerRef);
107            } else {
108              throw new CreateCommandException(ErrorType.INTERNAL, "Parsed script " + clazz.getName() +
109                " does not implements " + CommandInvoker.class.getName());
110            }
111          }
112        }
113    
114        //
115        if (providerRef == null) {
116          return null;
117        }
118    
119        //
120        try {
121          return providerRef.getObject().newInstance();
122        }
123        catch (Exception e) {
124          throw new CreateCommandException(ErrorType.INTERNAL, "Could not create command " + providerRef.getObject().getName() + " instance", e);
125        }
126      }
127    
128      public GroovyShell getGroovyShell() {
129        return groovyShell;
130      }
131    
132      public Object getAttribute(String name) {
133        return attributes.get(name);
134      }
135    
136      public void setAttribute(String name, Object value) {
137        attributes.put(name, value);
138      }
139    
140      public CRaSH(final PluginContext context) {
141        HashMap<String, Object> attributes = new HashMap<String, Object>();
142    
143        // Set variable available to all scripts
144        attributes.put("shellContext", context);
145        attributes.put("shell", this);
146    
147        //
148        CompilerConfiguration config = new CompilerConfiguration();
149        config.setRecompileGroovySource(true);
150        config.setScriptBaseClass(GroovyScriptCommand.class.getName());
151        GroovyShell groovyShell = new GroovyShell(context.getLoader(), new Binding(attributes), config);
152    
153        // Evaluate login script
154        String script = context.loadResource("login", ResourceKind.LIFECYCLE).getContent();
155        groovyShell.evaluate(script, "login");
156    
157        //
158        this.attributes = attributes;
159        this.groovyShell = groovyShell;
160        this.commands = new ConcurrentHashMap<String, TimestampedObject<Class<? extends ShellCommand>>>();
161        this.context = context;
162      }
163    
164      public void close() {
165        // Evaluate logout script
166        String script = context.loadResource("logout", ResourceKind.LIFECYCLE).getContent();
167        groovyShell.evaluate(script, "logout");
168      }
169    
170      // Shell implementation **********************************************************************************************
171    
172      public String getWelcome() {
173        return groovyShell.evaluate("welcome();").toString();
174      }
175    
176      public String getPrompt() {
177        return groovyShell.evaluate("prompt();").toString();
178      }
179    
180      public ShellProcess createProcess(String request) {
181        log.debug("Invoking request " + request);
182    
183        //
184        CRaSHProcessFactory factory = new CRaSHProcessFactory(this, request);
185    
186        //
187        return factory.create();
188      }
189    
190      /**
191       * For now basic implementation
192       */
193      public Map<String, String> complete(final String prefix) {
194        log.debug("Want prefix of " + prefix);
195        AST ast = new Parser(prefix).parse();
196        String termPrefix;
197        if (ast != null) {
198          AST.Term last = ast.lastTerm();
199          termPrefix = Utils.trimLeft(last.getLine());
200        } else {
201          termPrefix = "";
202        }
203    
204        //
205        log.debug("Retained term prefix is " + prefix);
206        Map<String, String> completions = Collections.emptyMap();
207        int pos = termPrefix.indexOf(' ');
208        if (pos == -1) {
209          completions = new HashMap<String, String>();
210          for (String resourceId : context.listResourceId(ResourceKind.SCRIPT)) {
211            if (resourceId.startsWith(termPrefix)) {
212              completions.put(resourceId.substring(termPrefix.length()), " ");
213            }
214          }
215        } else {
216          String commandName = termPrefix.substring(0, pos);
217          termPrefix = termPrefix.substring(pos);
218          try {
219            ShellCommand command = getCommand(commandName);
220            if (command != null) {
221              completions = command.complete(new CommandContextImpl(attributes), termPrefix);
222            }
223          }
224          catch (CreateCommandException e) {
225            log.debug("Could not create command for completion of " + prefix, e);
226          }
227        }
228    
229        //
230        log.debug("Found completions for " + prefix + ": " + completions);
231        return completions;
232      }
233    }