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