001    /*
002     * Copyright (C) 2010 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    
020    package org.crsh.command;
021    
022    import org.crsh.cmdline.ClassDescriptor;
023    import org.crsh.cmdline.CommandCompletion;
024    import org.crsh.cmdline.CommandFactory;
025    import org.crsh.cmdline.Delimiter;
026    import org.crsh.cmdline.IntrospectionException;
027    import org.crsh.cmdline.annotations.Man;
028    import org.crsh.cmdline.annotations.Option;
029    import org.crsh.cmdline.OptionDescriptor;
030    import org.crsh.cmdline.ParameterDescriptor;
031    import org.crsh.cmdline.annotations.Usage;
032    import org.crsh.cmdline.matcher.*;
033    import org.crsh.cmdline.spi.Completer;
034    import org.crsh.cmdline.spi.ValueCompletion;
035    import org.crsh.util.TypeResolver;
036    import org.slf4j.Logger;
037    import org.slf4j.LoggerFactory;
038    
039    import java.io.IOException;
040    import java.io.PrintWriter;
041    import java.io.StringWriter;
042    import java.lang.reflect.Method;
043    import java.lang.reflect.Type;
044    
045    /**
046     * A real CRaSH command, the most powerful kind of command.
047     *
048     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
049     * @version $Revision$
050     */
051    public abstract class CRaSHCommand extends GroovyCommand implements ShellCommand {
052    
053      /** . */
054      private final Logger log = LoggerFactory.getLogger(getClass());
055    
056      /** . */
057      private CommandContext context;
058    
059      /** . */
060      private boolean unquoteArguments;
061    
062      /** . */
063      private final ClassDescriptor<?> descriptor;
064    
065      /** The unmatched text, only valid during an invocation. */
066      private String unmatched;
067    
068      /** . */
069      @Option(names = {"h","help"})
070      @Usage("command usage")
071      @Man("Provides command usage")
072      private boolean help;
073    
074      protected CRaSHCommand() throws IntrospectionException {
075        this.context = null;
076        this.unquoteArguments = true;
077        this.descriptor = CommandFactory.create(getClass());
078        this.help = false;
079        this.unmatched = null;
080      }
081    
082      /**
083       * Returns the command descriptor.
084       *
085       * @return the command descriptor
086       */
087      public ClassDescriptor<?> getDescriptor() {
088        return descriptor;
089      }
090    
091      /**
092       * Returns true if the command wants its arguments to be unquoted.
093       *
094       * @return true if arguments must be unquoted
095       */
096      public final boolean getUnquoteArguments() {
097        return unquoteArguments;
098      }
099    
100      public final void setUnquoteArguments(boolean unquoteArguments) {
101        this.unquoteArguments = unquoteArguments;
102      }
103    
104      protected final String readLine(String msg) {
105        return readLine(msg, true);
106      }
107    
108      protected final String readLine(String msg, boolean echo) {
109        if (context instanceof InvocationContext) {
110          return ((InvocationContext)context).readLine(msg, echo);
111        } else {
112          throw new IllegalStateException("No current context of interaction with the term");
113        }
114      }
115    
116      public final String getUnmatched() {
117        return unmatched;
118      }
119    
120      @Override
121      protected final CommandContext getContext() {
122        return context;
123      }
124    
125      public final CommandCompletion complete(CommandContext context, String line) {
126    
127        // WTF
128        Matcher analyzer = Matcher.createMatcher("main", descriptor);
129    
130        //
131        Completer completer = this instanceof Completer ? (Completer)this : null;
132    
133        //
134        try {
135          this.context = context;
136    
137          //
138          return analyzer.complete(completer, line);
139        }
140        catch (CmdCompletionException e) {
141          log.error("Error during completion of line " + line, e);
142          return new CommandCompletion(Delimiter.EMPTY, ValueCompletion.create());
143        } finally {
144          this.context = null;
145        }
146      }
147    
148      public final String describe(String line, DescriptionFormat mode) {
149    
150        // WTF
151        Matcher analyzer = Matcher.createMatcher("main", descriptor);
152    
153        //
154        CommandMatch match = analyzer.match(line);
155    
156        //
157        try {
158          switch (mode) {
159            case DESCRIBE:
160              return match.getDescriptor().getUsage();
161            case MAN:
162              StringWriter sw = new StringWriter();
163              PrintWriter pw = new PrintWriter(sw);
164              match.printMan(pw);
165              return sw.toString();
166            case USAGE:
167              StringWriter sw2 = new StringWriter();
168              PrintWriter pw2 = new PrintWriter(sw2);
169              match.printUsage(pw2);
170              return sw2.toString();
171          }
172        }
173        catch (IOException e) {
174          throw new AssertionError(e);
175        }
176    
177        //
178        return null;
179      }
180    
181      static ScriptException toScript(Throwable cause) {
182        if (cause instanceof ScriptException) {
183          return (ScriptException)cause;
184        } if (cause instanceof groovy.util.ScriptException) {
185          // Special handling for groovy.util.ScriptException
186          // which may be thrown by scripts because it is imported by default
187          // by groovy imports
188          String msg = cause.getMessage();
189          ScriptException translated;
190          if (msg != null) {
191            translated = new ScriptException(msg);
192          } else {
193            translated = new ScriptException();
194          }
195          translated.setStackTrace(cause.getStackTrace());
196          return translated;
197        } else {
198          return new ScriptException(cause);
199        }
200      }
201    
202      public final CommandInvoker<?, ?> createInvoker(final String line) {
203    
204        // Remove surrounding quotes if there are
205        if (unquoteArguments) {
206          // todo ?
207        }
208    
209        // WTF
210        Matcher analyzer = Matcher.createMatcher("main", descriptor);
211    
212        //
213        final CommandMatch<CRaSHCommand, ?, ?> match = analyzer.match(line);
214    
215        //
216        if (match instanceof MethodMatch) {
217    
218          //
219          final MethodMatch<CRaSHCommand> methodMatch = (MethodMatch<CRaSHCommand>)match;
220    
221          //
222          boolean help = false;
223          for (OptionMatch optionMatch : methodMatch.getOwner().getOptionMatches()) {
224            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
225            if (parameterDesc instanceof OptionDescriptor<?>) {
226              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
227              if (optionDesc.getNames().contains("h")) {
228                help = true;
229              }
230            }
231          }
232          final boolean doHelp = help;
233    
234          //
235          return new CommandInvoker() {
236    
237            Class consumedType = Void.class;
238            Class producedType = Void.class;
239    
240            {
241              // Try to find a command context argument
242              Method m = methodMatch.getDescriptor().getMethod();
243    
244              //
245              Class<?>[] parameterTypes = m.getParameterTypes();
246              for (int i = 0;i < parameterTypes.length;i++) {
247                Class<?> parameterType = parameterTypes[i];
248                if (InvocationContext.class.isAssignableFrom(parameterType)) {
249                  Type contextGenericParameterType = m.getGenericParameterTypes()[i];
250                  consumedType = TypeResolver.resolveToClass(contextGenericParameterType, InvocationContext.class, 0);
251                  producedType = TypeResolver.resolveToClass(contextGenericParameterType, InvocationContext.class, 1);
252                }
253              }
254            }
255    
256            public void invoke(InvocationContext context) throws ScriptException {
257    
258              if (doHelp) {
259                try {
260                  match.printUsage(context.getWriter());
261                }
262                catch (IOException e) {
263                  throw new AssertionError(e);
264                }
265              } else {
266                CRaSHCommand.this.context = context;
267                CRaSHCommand.this.unmatched = methodMatch.getRest();
268                try {
269                  org.crsh.cmdline.matcher.InvocationContext invocationContext = new org.crsh.cmdline.matcher.InvocationContext();
270                  invocationContext.setAttribute(InvocationContext.class, context);
271                  Object o = methodMatch.invoke(invocationContext, CRaSHCommand.this);
272                  if (o != null) {
273                    context.getWriter().print(o);
274                  }
275                } catch (CmdSyntaxException e) {
276                  throw new SyntaxException(e.getMessage());
277                } catch (CmdInvocationException e) {
278                  throw toScript(e.getCause());
279                } finally {
280                  CRaSHCommand.this.context = null;
281                  CRaSHCommand.this.unmatched = null;
282                }
283              }
284            }
285    
286            public Class getProducedType() {
287              return producedType;
288            }
289    
290            public Class getConsumedType() {
291              return consumedType;
292            }
293          };
294        } else if (match instanceof ClassMatch) {
295    
296          //
297          final ClassMatch<?> classMatch = (ClassMatch)match;
298    
299          //
300          boolean help = false;
301          for (OptionMatch optionMatch : classMatch.getOptionMatches()) {
302            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
303            if (parameterDesc instanceof OptionDescriptor<?>) {
304              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
305              if (optionDesc.getNames().contains("h")) {
306                help = true;
307              }
308            }
309          }
310          final boolean doHelp = help;
311    
312          //
313          return new CommandInvoker<Void, Void>() {
314            public void invoke(InvocationContext<Void, Void> context) throws ScriptException {
315              try {
316                if (doHelp) {
317                  match.printUsage(context.getWriter());
318                } else {
319                  classMatch.printUsage(context.getWriter());
320                }
321              }
322              catch (IOException e) {
323                throw new AssertionError(e);
324              }
325            }
326    
327            public Class<Void> getProducedType() {
328              return Void.class;
329            }
330    
331            public Class<Void> getConsumedType() {
332              return Void.class;
333            }
334          };
335    
336        } else {
337          return null;
338        }
339      }
340    }