001    /*
002     * Copyright (C) 2012 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.text.RenderPrintWriter;
036    import org.crsh.util.TypeResolver;
037    import org.slf4j.Logger;
038    import org.slf4j.LoggerFactory;
039    
040    import java.io.IOException;
041    import java.io.PrintWriter;
042    import java.io.StringWriter;
043    import java.lang.reflect.Method;
044    import java.lang.reflect.Type;
045    import java.util.List;
046    import java.util.Map;
047    
048    public abstract class CRaSHCommand extends GroovyCommand implements ShellCommand {
049    
050      /** . */
051      private final Logger log = LoggerFactory.getLogger(getClass());
052    
053      /** . */
054      private boolean unquoteArguments;
055    
056      /** . */
057      private final ClassDescriptor<?> descriptor;
058    
059      /** The unmatched text, only valid during an invocation. */
060      protected String unmatched;
061    
062      /** . */
063      @Option(names = {"h","help"})
064      @Usage("command usage")
065      @Man("Provides command usage")
066      private boolean help;
067    
068      protected CRaSHCommand() throws IntrospectionException {
069        this.unquoteArguments = true;
070        this.descriptor = CommandFactory.create(getClass());
071        this.help = false;
072        this.unmatched = null;
073      }
074    
075      /**
076       * Returns the command descriptor.
077       *
078       * @return the command descriptor
079       */
080      public ClassDescriptor<?> getDescriptor() {
081        return descriptor;
082      }
083    
084      /**
085       * Returns true if the command wants its arguments to be unquoted.
086       *
087       * @return true if arguments must be unquoted
088       */
089      public final boolean getUnquoteArguments() {
090        return unquoteArguments;
091      }
092    
093      public final void setUnquoteArguments(boolean unquoteArguments) {
094        this.unquoteArguments = unquoteArguments;
095      }
096    
097      protected final String readLine(String msg) {
098        return readLine(msg, true);
099      }
100    
101      protected final String readLine(String msg, boolean echo) {
102        if (context instanceof InvocationContext) {
103          return ((InvocationContext)context).readLine(msg, echo);
104        } else {
105          throw new IllegalStateException("No current context of interaction with the term");
106        }
107      }
108    
109      public final String getUnmatched() {
110        return unmatched;
111      }
112    
113      public final CommandCompletion complete(CommandContext context, String line) {
114    
115        // WTF
116        Matcher analyzer = descriptor.matcher("main");
117    
118        //
119        Completer completer = this instanceof Completer ? (Completer)this : null;
120    
121        //
122        try {
123          this.context = context;
124    
125          //
126          return analyzer.complete(completer, line);
127        }
128        catch (CmdCompletionException e) {
129          log.error("Error during completion of line " + line, e);
130          return new CommandCompletion(Delimiter.EMPTY, ValueCompletion.create());
131        } finally {
132          this.context = null;
133        }
134      }
135    
136      public final String describe(String line, DescriptionFormat mode) {
137    
138        // WTF
139        Matcher analyzer = descriptor.matcher("main");
140    
141        //
142        CommandMatch match = analyzer.match(line);
143    
144        //
145        try {
146          switch (mode) {
147            case DESCRIBE:
148              return match.getDescriptor().getUsage();
149            case MAN:
150              StringWriter sw = new StringWriter();
151              PrintWriter pw = new PrintWriter(sw);
152              match.printMan(pw);
153              return sw.toString();
154            case USAGE:
155              StringWriter sw2 = new StringWriter();
156              PrintWriter pw2 = new PrintWriter(sw2);
157              match.printUsage(pw2);
158              return sw2.toString();
159          }
160        }
161        catch (IOException e) {
162          throw new AssertionError(e);
163        }
164    
165        //
166        return null;
167      }
168    
169      static ScriptException toScript(Throwable cause) {
170        if (cause instanceof ScriptException) {
171          return (ScriptException)cause;
172        } if (cause instanceof groovy.util.ScriptException) {
173          // Special handling for groovy.util.ScriptException
174          // which may be thrown by scripts because it is imported by default
175          // by groovy imports
176          String msg = cause.getMessage();
177          ScriptException translated;
178          if (msg != null) {
179            translated = new ScriptException(msg);
180          } else {
181            translated = new ScriptException();
182          }
183          translated.setStackTrace(cause.getStackTrace());
184          return translated;
185        } else {
186          return new ScriptException(cause);
187        }
188      }
189    
190      public CommandInvoker<?, ?> resolveInvoker(String name, Map<String, ?> options, List<?> args) {
191        if (options.containsKey("h") || options.containsKey("help")) {
192          throw new UnsupportedOperationException("Implement me");
193        } else {
194    
195          Matcher matcher = descriptor.matcher("main");
196          CommandMatch<CRaSHCommand, ?, ?> match = matcher.match(name, options, args);
197          return resolveInvoker(match);
198        }
199      }
200    
201      public CommandInvoker<?, ?> resolveInvoker(String line) {
202        Matcher analyzer = descriptor.matcher("main");
203        final CommandMatch<CRaSHCommand, ?, ?> match = analyzer.match(line);
204        return resolveInvoker(match);
205      }
206    
207      public final CommandInvoker<?, ?> resolveInvoker(final CommandMatch<CRaSHCommand, ?, ?> match) {
208        if (match instanceof MethodMatch) {
209    
210          //
211          final MethodMatch<CRaSHCommand> methodMatch = (MethodMatch<CRaSHCommand>)match;
212    
213          //
214          boolean help = false;
215          for (OptionMatch optionMatch : methodMatch.getOwner().getOptionMatches()) {
216            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
217            if (parameterDesc instanceof OptionDescriptor<?>) {
218              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
219              if (optionDesc.getNames().contains("h")) {
220                help = true;
221              }
222            }
223          }
224          final boolean doHelp = help;
225    
226          //
227          return new CommandInvoker() {
228    
229            Class consumedType = Void.class;
230            Class producedType = Object.class;
231    
232            {
233              // Try to find a command context argument
234              Method m = methodMatch.getDescriptor().getMethod();
235    
236              //
237              Class<?>[] parameterTypes = m.getParameterTypes();
238              for (int i = 0;i < parameterTypes.length;i++) {
239                Class<?> parameterType = parameterTypes[i];
240                if (InvocationContext.class.isAssignableFrom(parameterType)) {
241                  Type contextGenericParameterType = m.getGenericParameterTypes()[i];
242                  producedType = TypeResolver.resolveToClass(contextGenericParameterType, InvocationContext.class, 0);
243                  break;
244                }
245              }
246    
247              //
248              if (PipeCommand.class.isAssignableFrom(m.getReturnType())) {
249                Type ret = m.getGenericReturnType();
250                consumedType = TypeResolver.resolveToClass(ret, PipeCommand.class, 0);
251              }
252            }
253    
254            public PipeCommand invoke(final InvocationContext context) throws ScriptException {
255              if (doHelp) {
256                try {
257                  match.printUsage(context.getWriter());
258                  return new PipeCommand() {
259                    public void provide(Object element) throws IOException {
260                    }
261                  };
262                }
263                catch (IOException e) {
264                  throw new AssertionError(e);
265                }
266              } else {
267    
268                //
269                pushContext(context);
270    
271                //
272                CRaSHCommand.this.unmatched = methodMatch.getRest();
273    
274                //
275                final Resolver resolver = new Resolver() {
276                  public <T> T resolve(Class<T> type) {
277                    if (type.equals(InvocationContext.class)) {
278                      return type.cast(context);
279                    } else {
280                      return null;
281                    }
282                  }
283                };
284    
285                //
286                if (consumedType == Void.class) {
287                  return new PipeCommand() {
288    
289                    @Override
290                    public void open() throws ScriptException {
291                      Object o;
292                      try {
293                        o = methodMatch.invoke(resolver, CRaSHCommand.this);
294                      } catch (CmdSyntaxException e) {
295                        throw new SyntaxException(e.getMessage());
296                      } catch (CmdInvocationException e) {
297                        throw toScript(e.getCause());
298                      } finally {
299                        CRaSHCommand.this.context = null;
300                        CRaSHCommand.this.unmatched = null;
301                      }
302                      if (o != null) {
303                        context.getWriter().print(o);
304                      }
305                    }
306    
307                    @Override
308                    public void provide(Object element) throws ScriptException, IOException {
309                      // We just drop the elements
310                    }
311    
312                    @Override
313                    public void flush() throws IOException {
314                      context.flush();
315                    }
316    
317                    @Override
318                    public void close() throws ScriptException {
319                      popContext();
320                    }
321                  };
322                } else {
323    
324                  // JULIEN : WE SHOULD SOMEHOW HONNOR THE FINALLY CLAUSE LIKE IN THE IF BLOCK
325    
326                  try {
327                    return (PipeCommand)methodMatch.invoke(resolver, CRaSHCommand.this);
328                  } catch (CmdSyntaxException e) {
329                    throw new SyntaxException(e.getMessage());
330                  } catch (CmdInvocationException e) {
331                    throw toScript(e.getCause());
332                  }
333                }
334              }
335            }
336    
337            public Class getProducedType() {
338              return producedType;
339            }
340    
341            public Class getConsumedType() {
342              return consumedType;
343            }
344          };
345        } else if (match instanceof ClassMatch) {
346    
347          //
348          final ClassMatch<?> classMatch = (ClassMatch)match;
349    
350          //
351          boolean help = false;
352          for (OptionMatch optionMatch : classMatch.getOptionMatches()) {
353            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
354            if (parameterDesc instanceof OptionDescriptor<?>) {
355              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
356              if (optionDesc.getNames().contains("h")) {
357                help = true;
358              }
359            }
360          }
361          final boolean doHelp = help;
362    
363          //
364          return new CommandInvoker<Void, Object>() {
365            public PipeCommand<Void> invoke(final InvocationContext<Object> context) throws ScriptException {
366              try {
367                if (doHelp) {
368                  match.printUsage(context.getWriter());
369                } else {
370                  classMatch.printUsage(context.getWriter());
371                }
372                return new PipeCommand<Void>() {
373                  public void provide(Void element) throws IOException {
374                  }
375                  @Override
376                  public void flush() throws IOException {
377                    context.flush();
378                  }
379                };
380              }
381              catch (IOException e) {
382                throw new AssertionError(e);
383              }
384            }
385    
386            public Class<Object> getProducedType() {
387              return Object.class;
388            }
389    
390            public Class<Void> getConsumedType() {
391              return Void.class;
392            }
393          };
394    
395        } else {
396          return null;
397        }
398      }
399    }