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