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