001    package org.crsh.processor.term;
002    
003    import org.crsh.cmdline.CommandCompletion;
004    import org.crsh.cmdline.Delimiter;
005    import org.crsh.cmdline.spi.ValueCompletion;
006    import org.crsh.shell.Shell;
007    import org.crsh.shell.ShellProcess;
008    import org.crsh.term.Term;
009    import org.crsh.term.TermEvent;
010    import org.crsh.util.CloseableList;
011    import org.crsh.util.Strings;
012    import org.slf4j.Logger;
013    import org.slf4j.LoggerFactory;
014    
015    import java.io.Closeable;
016    import java.io.IOException;
017    import java.util.Iterator;
018    import java.util.LinkedList;
019    import java.util.Map;
020    
021    /** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
022    public final class Processor implements Runnable {
023    
024      /** . */
025      static final Runnable NOOP = new Runnable() {
026        public void run() {
027        }
028      };
029    
030      /** . */
031      final Runnable WRITE_PROMPT = new Runnable() {
032        public void run() {
033          writePrompt();
034        }
035      };
036    
037      /** . */
038      final Runnable CLOSE = new Runnable() {
039        public void run() {
040          close();
041        }
042      };
043    
044      /** . */
045      private final Runnable READ_TERM = new Runnable() {
046        public void run() {
047          readTerm();
048        }
049      };
050    
051      /** . */
052      final Logger log = LoggerFactory.getLogger(Processor.class);
053    
054      /** . */
055      final Term term;
056    
057      /** . */
058      final Shell shell;
059    
060      /** . */
061      final LinkedList<TermEvent> queue;
062    
063      /** . */
064      final Object lock;
065    
066      /** . */
067      ProcessContext current;
068    
069      /** . */
070      Status status;
071    
072      /** A flag useful for unit testing to know when the thread is reading. */
073      volatile boolean waitingEvent;
074    
075      /** . */
076      private final CloseableList listeners;
077    
078      public Processor(Term term, Shell shell) {
079        this.term = term;
080        this.shell = shell;
081        this.queue = new LinkedList<TermEvent>();
082        this.lock = new Object();
083        this.status = Status.AVAILABLE;
084        this.listeners = new CloseableList();
085        this.waitingEvent = false;
086      }
087    
088      public boolean isWaitingEvent() {
089        return waitingEvent;
090      }
091    
092      public void run() {
093    
094    
095        // Display initial stuff
096        try {
097          String welcome = shell.getWelcome();
098          log.debug("Writing welcome message to term");
099          term.write(welcome);
100          log.debug("Wrote welcome message to term");
101          writePrompt();
102        }
103        catch (IOException e) {
104          e.printStackTrace();
105        }
106    
107        //
108        while (true) {
109          try {
110            if (!iterate()) {
111              break;
112            }
113          }
114          catch (IOException e) {
115            e.printStackTrace();
116          }
117          catch (InterruptedException e) {
118            break;
119          }
120        }
121      }
122    
123      boolean iterate() throws InterruptedException, IOException {
124    
125        //
126        Runnable runnable;
127        synchronized (lock) {
128          switch (status) {
129            case AVAILABLE:
130              runnable =  peekProcess();
131              if (runnable != null) {
132                break;
133              }
134            case PROCESSING:
135            case CANCELLING:
136              runnable = READ_TERM;
137              break;
138            case CLOSED:
139              return false;
140            default:
141              throw new AssertionError();
142          }
143        }
144    
145        //
146        runnable.run();
147    
148        //
149        return true;
150      }
151    
152      // We assume this is called under lock synchronization
153      ProcessContext peekProcess() {
154        while (true) {
155          synchronized (lock) {
156            if (status == Status.AVAILABLE) {
157              if (queue.size() > 0) {
158                TermEvent event = queue.removeFirst();
159                if (event instanceof TermEvent.Complete) {
160                  complete(((TermEvent.Complete)event).getLine());
161                } else {
162                  String line = ((TermEvent.ReadLine)event).getLine().toString();
163                  if (line.length() > 0) {
164                    term.addToHistory(line);
165                  }
166                  ShellProcess process = shell.createProcess(line);
167                  current =  new ProcessContext(this, process);
168                  status = Status.PROCESSING;
169                  return current;
170                }
171              } else {
172                break;
173              }
174            } else {
175              break;
176            }
177          }
178        }
179        return null;
180      }
181    
182      /** . */
183      private final Object termLock = new Object();
184    
185      private boolean reading = false;
186    
187      void readTerm() {
188    
189        //
190        synchronized (termLock) {
191          if (reading) {
192            try {
193              termLock.wait();
194              return;
195            }
196            catch (InterruptedException e) {
197              throw new AssertionError(e);
198            }
199          } else {
200            reading = true;
201          }
202        }
203    
204        //
205        try {
206          TermEvent event = term.read();
207    
208          //
209          Runnable runnable;
210          if (event instanceof TermEvent.Break) {
211            synchronized (lock) {
212              queue.clear();
213              if (status == Status.PROCESSING) {
214                status = Status.CANCELLING;
215                runnable = new Runnable() {
216                  ProcessContext context = current;
217                  public void run() {
218                    context.process.cancel();
219                  }
220                };
221              }
222              else if (status == Status.AVAILABLE) {
223                runnable = WRITE_PROMPT;
224              } else {
225                runnable = NOOP;
226              }
227            }
228          } else if (event instanceof TermEvent.Close) {
229            synchronized (lock) {
230              queue.clear();
231              if (status == Status.PROCESSING) {
232                runnable = new Runnable() {
233                  ProcessContext context = current;
234                  public void run() {
235                    context.process.cancel();
236                    close();
237                  }
238                };
239              } else if (status != Status.CLOSED) {
240                runnable = CLOSE;
241              } else {
242                runnable = NOOP;
243              }
244              status = Status.CLOSED;
245            }
246          } else {
247            synchronized (queue) {
248              queue.addLast(event);
249              runnable = NOOP;
250            }
251          }
252    
253          //
254          runnable.run();
255        }
256        catch (IOException e) {
257          log.error("Error when reading term", e);
258        }
259        finally {
260          synchronized (termLock) {
261            reading = false;
262            termLock.notifyAll();
263          }
264        }
265      }
266    
267      void close() {
268        listeners.close();
269      }
270    
271      public void addListener(Closeable listener) {
272        listeners.add(listener);
273      }
274    
275      void write(String text) {
276        try {
277          term.write(text);
278        }
279        catch (IOException e) {
280          log.error("Write to term failure", e);
281        }
282      }
283    
284      void writePrompt() {
285        String prompt = shell.getPrompt();
286        try {
287          String p = prompt == null ? "% " : prompt;
288          term.write("\r\n");
289          term.write(p);
290          term.write(term.getBuffer());
291        } catch (IOException e) {
292          e.printStackTrace();
293        }
294      }
295    
296      private void complete(CharSequence prefix) {
297        log.debug("About to get completions for " + prefix);
298        CommandCompletion completion = shell.complete(prefix.toString());
299        ValueCompletion completions = completion.getValue();
300        log.debug("Completions for " + prefix + " are " + completions);
301    
302        //
303        Delimiter delimiter = completion.getDelimiter();
304    
305        try {
306          // Try to find the greatest prefix among all the results
307          if (completions.getSize() == 0) {
308            // Do nothing
309          } else if (completions.getSize() == 1) {
310            Map.Entry<String, Boolean> entry = completions.iterator().next();
311            Appendable buffer = term.getInsertBuffer();
312            String insert = entry.getKey();
313            delimiter.escape(insert, term.getInsertBuffer());
314            if (entry.getValue()) {
315              buffer.append(completion.getDelimiter().getValue());
316            }
317          } else {
318            String commonCompletion = Strings.findLongestCommonPrefix(completions.getSuffixes());
319            if (commonCompletion.length() > 0) {
320              delimiter.escape(commonCompletion, term.getInsertBuffer());
321            } else {
322              // Format stuff
323              int width = term.getWidth();
324    
325              //
326              String completionPrefix = completions.getPrefix();
327    
328              // Get the max length
329              int max = 0;
330              for (String suffix : completions.getSuffixes()) {
331                max = Math.max(max, completionPrefix.length() + suffix.length());
332              }
333    
334              // Separator : use two whitespace like in BASH
335              max += 2;
336    
337              //
338              StringBuilder sb = new StringBuilder().append('\n');
339              if (max < width) {
340                int columns = width / max;
341                int index = 0;
342                for (String suffix : completions.getSuffixes()) {
343                  sb.append(completionPrefix).append(suffix);
344                  for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
345                    sb.append(' ');
346                  }
347                  if (++index >= columns) {
348                    index = 0;
349                    sb.append('\n');
350                  }
351                }
352                if (index > 0) {
353                  sb.append('\n');
354                }
355              } else {
356                for (Iterator<String> i = completions.getSuffixes().iterator();i.hasNext();) {
357                  String suffix = i.next();
358                  sb.append(commonCompletion).append(suffix);
359                  if (i.hasNext()) {
360                    sb.append('\n');
361                  }
362                }
363                sb.append('\n');
364              }
365    
366              // We propose
367              term.write(sb.toString());
368              writePrompt();
369            }
370          }
371        }
372        catch (IOException e) {
373          log.error("Could not write completion", e);
374        }
375      }
376    }