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 }