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