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