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.ClassMatch;
033    import org.crsh.cmdline.matcher.CmdCompletionException;
034    import org.crsh.cmdline.matcher.CmdInvocationException;
035    import org.crsh.cmdline.matcher.CmdSyntaxException;
036    import org.crsh.cmdline.matcher.CommandMatch;
037    import org.crsh.cmdline.matcher.Matcher;
038    import org.crsh.cmdline.matcher.MethodMatch;
039    import org.crsh.cmdline.matcher.OptionMatch;
040    import org.crsh.cmdline.matcher.Resolver;
041    import org.crsh.cmdline.spi.Completer;
042    import org.crsh.cmdline.spi.Completion;
043    import org.crsh.shell.InteractionContext;
044    import org.crsh.util.TypeResolver;
045    
046    import java.io.IOException;
047    import java.io.PrintWriter;
048    import java.io.StringWriter;
049    import java.lang.reflect.Method;
050    import java.lang.reflect.Type;
051    import java.util.List;
052    import java.util.Map;
053    import java.util.logging.Level;
054    import java.util.logging.Logger;
055    
056    public abstract class CRaSHCommand extends GroovyCommand implements ShellCommand {
057    
058      /** . */
059      private final Logger log = Logger.getLogger(getClass().getName());
060    
061      /** . */
062      private final ClassDescriptor<?> descriptor;
063    
064      /** The unmatched text, only valid during an invocation. */
065      protected String unmatched;
066    
067      /** . */
068      @Option(names = {"h","help"})
069      @Usage("command usage")
070      @Man("Provides command usage")
071      private boolean help;
072    
073      protected CRaSHCommand() throws IntrospectionException {
074        this.descriptor = new CommandFactory(getClass().getClassLoader()).create(getClass());
075        this.help = false;
076        this.unmatched = null;
077      }
078    
079      /**
080       * Returns the command descriptor.
081       *
082       * @return the command descriptor
083       */
084      public ClassDescriptor<?> getDescriptor() {
085        return descriptor;
086      }
087    
088      protected final String readLine(String msg) {
089        return readLine(msg, true);
090      }
091    
092      protected final String readLine(String msg, boolean echo) {
093        if (context instanceof InvocationContext) {
094          return ((InvocationContext)context).readLine(msg, echo);
095        } else {
096          throw new IllegalStateException("Cannot invoke read line without an invocation context");
097        }
098      }
099    
100      public final String getUnmatched() {
101        return unmatched;
102      }
103    
104      public final CommandCompletion complete(CommandContext context, String line) {
105    
106        // WTF
107        Matcher analyzer = descriptor.matcher("main");
108    
109        //
110        Completer completer = this instanceof Completer ? (Completer)this : null;
111    
112        //
113        this.context = context;
114        try {
115          return analyzer.complete(completer, line);
116        }
117        catch (CmdCompletionException e) {
118          log.log(Level.SEVERE, "Error during completion of line " + line, e);
119          return new CommandCompletion(Delimiter.EMPTY, Completion.create());
120        }
121        finally {
122          this.context = null;
123        }
124      }
125    
126      public final String describe(String line, DescriptionFormat mode) {
127    
128        // WTF
129        Matcher analyzer = descriptor.matcher("main");
130    
131        //
132        CommandMatch match = analyzer.match(line);
133    
134        //
135        try {
136          switch (mode) {
137            case DESCRIBE:
138              return match.getDescriptor().getUsage();
139            case MAN:
140              StringWriter sw = new StringWriter();
141              PrintWriter pw = new PrintWriter(sw);
142              match.printMan(pw);
143              return sw.toString();
144            case USAGE:
145              StringWriter sw2 = new StringWriter();
146              PrintWriter pw2 = new PrintWriter(sw2);
147              match.printUsage(pw2);
148              return sw2.toString();
149          }
150        }
151        catch (IOException e) {
152          throw new AssertionError(e);
153        }
154    
155        //
156        return null;
157      }
158    
159      static ScriptException toScript(Throwable cause) {
160        if (cause instanceof ScriptException) {
161          return (ScriptException)cause;
162        } if (cause instanceof groovy.util.ScriptException) {
163          // Special handling for groovy.util.ScriptException
164          // which may be thrown by scripts because it is imported by default
165          // by groovy imports
166          String msg = cause.getMessage();
167          ScriptException translated;
168          if (msg != null) {
169            translated = new ScriptException(msg);
170          } else {
171            translated = new ScriptException();
172          }
173          translated.setStackTrace(cause.getStackTrace());
174          return translated;
175        } else {
176          return new ScriptException(cause);
177        }
178      }
179    
180      public CommandInvoker<?, ?> resolveInvoker(String name, Map<String, ?> options, List<?> args) {
181        if (options.containsKey("h") || options.containsKey("help")) {
182          throw new UnsupportedOperationException("Implement me");
183        } else {
184    
185          Matcher matcher = descriptor.matcher("main");
186          CommandMatch<CRaSHCommand, ?, ?> match = matcher.match(name, options, args);
187          return resolveInvoker(match);
188        }
189      }
190    
191      public CommandInvoker<?, ?> resolveInvoker(String line) {
192        Matcher analyzer = descriptor.matcher("main");
193        final CommandMatch<CRaSHCommand, ?, ?> match = analyzer.match(line);
194        return resolveInvoker(match);
195      }
196    
197      public final void execute(String s) throws ScriptException, IOException {
198        InvocationContext<?> context = peekContext();
199        CommandInvoker invoker = context.resolve(s);
200        invoker.open(context);
201        invoker.flush();
202        invoker.close();
203      }
204    
205      public final CommandInvoker<?, ?> resolveInvoker(final CommandMatch<CRaSHCommand, ?, ?> match) {
206        if (match instanceof MethodMatch) {
207    
208          //
209          final MethodMatch<CRaSHCommand> methodMatch = (MethodMatch<CRaSHCommand>)match;
210    
211          //
212          boolean help = false;
213          for (OptionMatch optionMatch : methodMatch.getOwner().getOptionMatches()) {
214            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
215            if (parameterDesc instanceof OptionDescriptor<?>) {
216              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
217              if (optionDesc.getNames().contains("h")) {
218                help = true;
219              }
220            }
221          }
222          final boolean doHelp = help;
223    
224          //
225          Class consumedType;
226          Class producedType;
227          Method m = methodMatch.getDescriptor().getMethod();
228          if (PipeCommand.class.isAssignableFrom(m.getReturnType())) {
229            Type ret = m.getGenericReturnType();
230            consumedType = TypeResolver.resolveToClass(ret, PipeCommand.class, 0);
231            producedType = TypeResolver.resolveToClass(ret, PipeCommand.class, 1);
232          } else {
233            consumedType = Void.class;
234            producedType = Object.class;
235            Class<?>[] parameterTypes = m.getParameterTypes();
236            for (int i = 0;i < parameterTypes.length;i++) {
237              Class<?> parameterType = parameterTypes[i];
238              if (InvocationContext.class.isAssignableFrom(parameterType)) {
239                Type contextGenericParameterType = m.getGenericParameterTypes()[i];
240                producedType = TypeResolver.resolveToClass(contextGenericParameterType, InvocationContext.class, 0);
241                break;
242              }
243            }
244          }
245          final Class _consumedType = consumedType;
246          final Class _producedType = producedType;
247    
248          if (doHelp) {
249            return new CommandInvoker<Object, Object>() {
250    
251              /** . */
252              private CommandContext session;
253    
254              /** . */
255              private InvocationContextImpl context;
256    
257              public Class<Object> getProducedType() {
258                return _producedType;
259              }
260    
261              public void setSession(CommandContext session) {
262                this.session = session;
263              }
264    
265              public Class<Object> getConsumedType() {
266                return _consumedType;
267              }
268    
269              public void open(InteractionContext<Object> consumer) {
270                this.context = new InvocationContextImpl(consumer, session);
271                try {
272                  match.printUsage(this.context.getWriter());
273                }
274                catch (IOException e) {
275                  throw new AssertionError(e);
276                }
277              }
278    
279              public void setPiped(boolean piped) {
280              }
281    
282              public void provide(Object element) throws IOException {
283              }
284    
285              public void flush() throws IOException {
286                this.context.flush();
287              }
288    
289              public void close() {
290              }
291            };
292          } else {
293            if (consumedType == Void.class) {
294    
295              return new CommandInvoker<Object, Object>() {
296    
297                /** . */
298                private CommandContext session;
299    
300                public void setSession(CommandContext session) {
301                  this.session = session;
302                }
303    
304                public Class<Object> getProducedType() {
305                  return _producedType;
306                }
307    
308                public Class<Object> getConsumedType() {
309                  return _consumedType;
310                }
311    
312                public void open(final InteractionContext<Object> consumer) {
313    
314                  //
315                  pushContext(new InvocationContextImpl<Object>(consumer, session));
316                  CRaSHCommand.this.unmatched = methodMatch.getRest();
317                  final Resolver resolver = new Resolver() {
318                    public <T> T resolve(Class<T> type) {
319                      if (type.equals(InvocationContext.class)) {
320                        return type.cast(peekContext());
321                      } else {
322                        return null;
323                      }
324                    }
325                  };
326    
327                  //
328                  Object o;
329                  try {
330                    o = methodMatch.invoke(resolver, CRaSHCommand.this);
331                  } catch (CmdSyntaxException e) {
332                    throw new SyntaxException(e.getMessage());
333                  } catch (CmdInvocationException e) {
334                    throw toScript(e.getCause());
335                  }
336                  if (o != null) {
337                    peekContext().getWriter().print(o);
338                  }
339                }
340                public void setPiped(boolean piped) {
341                }
342                public void provide(Object element) throws IOException {
343                  // We just drop the elements
344                }
345                public void flush() throws IOException {
346                  peekContext().flush();
347                }
348                public void close() {
349                  CRaSHCommand.this.unmatched = null;
350                  popContext();
351                }
352              };
353            } else {
354              return new CommandInvoker<Object, Object>() {
355    
356                /** . */
357                PipeCommand real;
358    
359                /** . */
360                boolean piped;
361    
362                /** . */
363                private CommandContext session;
364    
365                public Class<Object> getProducedType() {
366                  return _producedType;
367                }
368    
369                public Class<Object> getConsumedType() {
370                  return _consumedType;
371                }
372    
373                public void setSession(CommandContext session) {
374                  this.session = session;
375                }
376    
377                public void setPiped(boolean piped) {
378                  this.piped = piped;
379                }
380    
381                public void open(final InteractionContext<Object> consumer) {
382    
383                  //
384                  final InvocationContextImpl<Object> invocationContext = new InvocationContextImpl<Object>(consumer, session);
385    
386                  //
387                  pushContext(invocationContext);
388                  CRaSHCommand.this.unmatched = methodMatch.getRest();
389                  final Resolver resolver = new Resolver() {
390                    public <T> T resolve(Class<T> type) {
391                      if (type.equals(InvocationContext.class)) {
392                        return type.cast(invocationContext);
393                      } else {
394                        return null;
395                      }
396                    }
397                  };
398                  try {
399                    real = (PipeCommand)methodMatch.invoke(resolver, CRaSHCommand.this);
400                  }
401                  catch (CmdSyntaxException e) {
402                    throw new SyntaxException(e.getMessage());
403                  } catch (CmdInvocationException e) {
404                    throw toScript(e.getCause());
405                  }
406    
407                  //
408                  real.setPiped(piped);
409                  real.doOpen(invocationContext);
410                }
411    
412                public void provide(Object element) throws IOException {
413                  real.provide(element);
414                }
415    
416                public void flush() throws IOException {
417                  real.flush();
418                }
419    
420                public void close() {
421                  try {
422                    real.close();
423                  }
424                  finally {
425                    popContext();
426                  }
427                }
428              };
429            }
430          }
431        } else if (match instanceof ClassMatch) {
432    
433          //
434          final ClassMatch<?> classMatch = (ClassMatch)match;
435    
436          //
437          boolean help = false;
438          for (OptionMatch optionMatch : classMatch.getOptionMatches()) {
439            ParameterDescriptor<?> parameterDesc = optionMatch.getParameter();
440            if (parameterDesc instanceof OptionDescriptor<?>) {
441              OptionDescriptor<?> optionDesc = (OptionDescriptor<?>)parameterDesc;
442              if (optionDesc.getNames().contains("h")) {
443                help = true;
444              }
445            }
446          }
447          final boolean doHelp = help;
448    
449          //
450          return new CommandInvoker<Void, Object>() {
451    
452            /** . */
453            private CommandContext session;
454    
455            /** . */
456            InvocationContext context;
457    
458            public void open(InteractionContext<Object> consumer) {
459              this.context = new InvocationContextImpl(consumer, session);
460              try {
461                if (doHelp) {
462                  match.printUsage(context.getWriter());
463                } else {
464                  classMatch.printUsage(context.getWriter());
465                }
466              }
467              catch (IOException e) {
468                throw new AssertionError(e);
469              }
470            }
471    
472            public void setSession(CommandContext session) {
473              this.session = session;
474            }
475    
476            public void setPiped(boolean piped) {
477            }
478    
479            public void close() {
480              this.context = null;
481            }
482    
483            public void provide(Void element) throws IOException {
484            }
485    
486            public void flush() throws IOException {
487              context.flush();
488            }
489    
490            public Class<Object> getProducedType() {
491              return Object.class;
492            }
493    
494            public Class<Void> getConsumedType() {
495              return Void.class;
496            }
497          };
498    
499        } else {
500          return null;
501        }
502      }
503    }