View Javadoc
1   package org.cyclopsgroup.jcli.jline;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.List;
6   import java.util.concurrent.atomic.AtomicBoolean;
7   import java.util.concurrent.atomic.AtomicInteger;
8   import org.cyclopsgroup.caff.token.TokenEvent;
9   import org.cyclopsgroup.caff.token.TokenEventHandler;
10  import org.cyclopsgroup.caff.token.ValueTokenizer;
11  import org.cyclopsgroup.jcli.ArgumentProcessor;
12  import org.cyclopsgroup.jcli.AutoCompletable;
13  import org.cyclopsgroup.jcli.spi.Option;
14  import org.cyclopsgroup.jcli.spi.ParsingContext;
15  import org.jline.reader.Candidate;
16  import org.jline.reader.Completer;
17  import org.jline.reader.LineReader;
18  import org.jline.reader.ParsedLine;
19  import com.google.common.base.Preconditions;
20  import com.google.common.base.Strings;
21  
22  /**
23   * JLine completor implemented with JCli
24   *
25   * @author <a href="mailto:jiaqi.guo@gmail.com">Jiaqi Guo</a>
26   */
27  public class CliCompletor implements Completer {
28    private static List<String> filterList(List<String> list, String prefix) {
29      if (Strings.isNullOrEmpty(prefix)) {
30        return list;
31      }
32      List<String> results = new ArrayList<String>();
33      for (String item : list) {
34        if (item.startsWith(prefix)) {
35          results.add(item);
36        }
37      }
38      return results;
39    }
40  
41    private final ParsingContext context;
42  
43    private final AutoCompletable completable;
44  
45    private final ValueTokenizer tokenizer;
46  
47    /**
48     * @param cliBean Entyped AutoCompletable implementation or an normal bean
49     * @param tokenizer Tokenizer for argument parsing
50     */
51    public CliCompletor(final Object cliBean, final ValueTokenizer tokenizer) {
52      Preconditions.checkNotNull(cliBean, "Cli bean can't be null.");
53      Preconditions.checkNotNull(tokenizer, "String tokenizer can't be null.");
54      context = ArgumentProcessor.forType(cliBean.getClass()).createParsingContext();
55      if (cliBean instanceof AutoCompletable) {
56        this.completable = (AutoCompletable) cliBean;
57      } else {
58        this.completable = new AutoCompletable() {
59          public List<String> suggestArgument(String partialArgument) {
60            return Collections.emptyList();
61          }
62  
63          public List<String> suggestOption(String optionName, String partialOption) {
64            return Collections.emptyList();
65          }
66        };
67      }
68      this.tokenizer = tokenizer;
69    }
70  
71    public void complete(LineReader reader, ParsedLine line, List<Candidate> suggestions) {
72      String command = line.line();
73      ArgumentsInspectortor.html#ArgumentsInspector">ArgumentsInspector inspector = new ArgumentsInspector(context);
74      final AtomicBoolean terminated = new AtomicBoolean(true);
75      final AtomicInteger lastWordStart = new AtomicInteger(0);
76      if (!Strings.isNullOrEmpty(command)) {
77        final List<String> args = new ArrayList<String>();
78        tokenizer.parse(command, new TokenEventHandler() {
79  
80          public void handleEvent(TokenEvent ev) {
81            args.add(ev.getToken());
82            terminated.set(ev.isTerminated());
83            lastWordStart.set(ev.getStart());
84          }
85        });
86        for (String arg : args) {
87          inspector.consume(arg);
88        }
89        if (terminated.get()) {
90          inspector.end();
91        }
92      }
93      // System.err.println( "command=[" + command + "], cursor=" + cursor +
94      // ", state=" + inspector.getState().name()
95      // + ", value=" + inspector.getCurrentValue() );
96      List<String> candidates = new ArrayList<String>();
97      switch (inspector.getState()) {
98        case READY:
99          for (Option o : inspector.getRemainingOptions()) {
100           candidates.add("-" + o.getName());
101         }
102         Collections.sort(candidates);
103         candidates.addAll(suggestArguments(null));
104         break;
105       case OPTION:
106       case LONG_OPTION:
107         candidates.addAll(suggestOptionNames(inspector, inspector.getCurrentValue()));
108         break;
109       case OPTION_VALUE:
110         candidates
111             .addAll(suggestOptionValue(inspector.getCurrentOption(), inspector.getCurrentValue()));
112         break;
113       case ARGUMENT:
114         candidates.addAll(suggestArguments(inspector.getCurrentValue()));
115     }
116     for (String candidate : candidates) {
117       suggestions.add(new Candidate(tokenizer.escape(candidate)));
118     }
119   }
120 
121   private List<String> suggestArguments(String partialArgument) {
122     List<String> results;
123     if (Strings.isNullOrEmpty(partialArgument)) {
124       results = completable.suggestArgument(null);
125     } else {
126       results = completable.suggestArgument(partialArgument);
127       if (results == null) {
128         results = filterList(completable.suggestArgument(null), partialArgument);
129       }
130     }
131     if (results == null) {
132       results = Collections.emptyList();
133     } else {
134       results = new ArrayList<String>(results);
135       Collections.sort(results);
136     }
137     return results;
138   }
139 
140   private List<String> suggestOptionNames(ArgumentsInspector inspector, String value) {
141     List<String> results = new ArrayList<String>();
142     for (Option o : inspector.getRemainingOptions()) {
143       if (value.startsWith("--") && o.getLongName() != null
144           && ("--" + o.getLongName()).startsWith(value)) {
145         results.add("--" + o.getLongName());
146       } else if (value.startsWith("-") && ("-" + o.getName()).startsWith(value)) {
147         results.add("-" + o.getName());
148       }
149     }
150     Collections.sort(results);
151     return results;
152   }
153 
154   private List<String> suggestOptionValue(Option option, String partialValue) {
155     List<String> results;
156     if (Strings.isNullOrEmpty(partialValue)) {
157       results = completable.suggestOption(option.getName(), null);
158     } else {
159       results = completable.suggestOption(option.getName(), partialValue);
160       if (results == null) {
161         results = filterList(completable.suggestOption(option.getName(), null), partialValue);
162       }
163     }
164     if (results == null) {
165       results = Collections.emptyList();
166     } else {
167       results = new ArrayList<String>(results);
168       Collections.sort(results);
169     }
170     return results;
171   }
172 }