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 }