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        break;
140      }
141    }
142  }
143
144  boolean iterate() throws InterruptedException, IOException {
145
146    //
147    Runnable runnable;
148    synchronized (lock) {
149      switch (status) {
150        case AVAILABLE:
151          runnable =  peekProcess();
152          if (runnable != null) {
153            break;
154          }
155        case PROCESSING:
156        case CANCELLING:
157          runnable = READ_TERM;
158          break;
159        case CLOSED:
160          return false;
161        default:
162          throw new AssertionError();
163      }
164    }
165
166    //
167    runnable.run();
168
169    //
170    return true;
171  }
172
173  // We assume this is called under lock synchronization
174  ProcessContext peekProcess() {
175    while (true) {
176      synchronized (lock) {
177        if (status == Status.AVAILABLE) {
178          if (queue.size() > 0) {
179            TermEvent event = queue.removeFirst();
180            if (event instanceof TermEvent.Complete) {
181              complete(((TermEvent.Complete)event).getLine());
182            } else {
183              String line = ((TermEvent.ReadLine)event).getLine().toString();
184              if (line.length() > 0) {
185                term.addToHistory(line);
186              }
187              ShellProcess process = shell.createProcess(line);
188              current =  new ProcessContext(this, process);
189              status = Status.PROCESSING;
190              return current;
191            }
192          } else {
193            break;
194          }
195        } else {
196          break;
197        }
198      }
199    }
200    return null;
201  }
202
203  /** . */
204  private final Object termLock = new Object();
205
206  private boolean reading = false;
207
208  void readTerm() {
209
210    //
211    synchronized (termLock) {
212      if (reading) {
213        try {
214          termLock.wait();
215          return;
216        }
217        catch (InterruptedException e) {
218          throw new AssertionError(e);
219        }
220      } else {
221        reading = true;
222      }
223    }
224
225    //
226    try {
227      TermEvent event = term.read();
228
229      //
230      Runnable runnable;
231      if (event instanceof TermEvent.Break) {
232        synchronized (lock) {
233          queue.clear();
234          if (status == Status.PROCESSING) {
235            status = Status.CANCELLING;
236            runnable = new Runnable() {
237              ProcessContext context = current;
238              public void run() {
239                context.process.cancel();
240              }
241            };
242          }
243          else if (status == Status.AVAILABLE) {
244            runnable = WRITE_PROMPT;
245          } else {
246            runnable = NOOP;
247          }
248        }
249      } else if (event instanceof TermEvent.Close) {
250        synchronized (lock) {
251          queue.clear();
252          if (status == Status.PROCESSING) {
253            runnable = new Runnable() {
254              ProcessContext context = current;
255              public void run() {
256                context.process.cancel();
257                close();
258              }
259            };
260          } else if (status != Status.CLOSED) {
261            runnable = CLOSE;
262          } else {
263            runnable = NOOP;
264          }
265          status = Status.CLOSED;
266        }
267      } else {
268        synchronized (queue) {
269          queue.addLast(event);
270          runnable = NOOP;
271        }
272      }
273
274      //
275      runnable.run();
276    }
277    catch (IOException e) {
278      log.log(Level.SEVERE, "Error when reading term", e);
279    }
280    finally {
281      synchronized (termLock) {
282        reading = false;
283        termLock.notifyAll();
284      }
285    }
286  }
287
288  void close() {
289    listeners.close();
290  }
291
292  public void addListener(Closeable listener) {
293    listeners.add(listener);
294  }
295
296  public Class<Chunk> getConsumedType() {
297    return Chunk.class;
298  }
299
300  public void provide(Chunk element) throws IOException {
301    term.write(element);
302  }
303
304  public void flush() throws IOException {
305    throw new UnsupportedOperationException("what does it mean?");
306  }
307
308  void writePromptFlush() {
309    String prompt = shell.getPrompt();
310    try {
311      StringBuilder sb = new StringBuilder("\r\n");
312      String p = prompt == null ? "% " : prompt;
313      sb.append(p);
314      CharSequence buffer = term.getBuffer();
315      if (buffer != null) {
316        sb.append(buffer);
317      }
318      term.write(Text.create(sb));
319      term.flush();
320    } catch (IOException e) {
321      // Todo : improve that
322      e.printStackTrace();
323    }
324  }
325
326  private void complete(CharSequence prefix) {
327    log.log(Level.FINE, "About to get completions for " + prefix);
328    CompletionMatch completion = shell.complete(prefix.toString());
329    Completion completions = completion.getValue();
330    log.log(Level.FINE, "Completions for " + prefix + " are " + completions);
331
332    //
333    Delimiter delimiter = completion.getDelimiter();
334
335    try {
336      // Try to find the greatest prefix among all the results
337      if (completions.getSize() == 0) {
338        // Do nothing
339      } else if (completions.getSize() == 1) {
340        Map.Entry<String, Boolean> entry = completions.iterator().next();
341        Appendable buffer = term.getDirectBuffer();
342        String insert = entry.getKey();
343        term.getDirectBuffer().append(delimiter.escape(insert));
344        if (entry.getValue()) {
345          buffer.append(completion.getDelimiter().getValue());
346        }
347      } else {
348        String commonCompletion = Strings.findLongestCommonPrefix(completions.getValues());
349
350        // Format stuff
351        int width = term.getWidth();
352
353        //
354        String completionPrefix = completions.getPrefix();
355
356        // Get the max length
357        int max = 0;
358        for (String suffix : completions.getValues()) {
359          max = Math.max(max, completionPrefix.length() + suffix.length());
360        }
361
362        // Separator : use two whitespace like in BASH
363        max += 2;
364
365        //
366        StringBuilder sb = new StringBuilder().append('\n');
367        if (max < width) {
368          int columns = width / max;
369          int index = 0;
370          for (String suffix : completions.getValues()) {
371            sb.append(completionPrefix).append(suffix);
372            for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
373              sb.append(' ');
374            }
375            if (++index >= columns) {
376              index = 0;
377              sb.append('\n');
378            }
379          }
380          if (index > 0) {
381            sb.append('\n');
382          }
383        } else {
384          for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) {
385            String suffix = i.next();
386            sb.append(commonCompletion).append(suffix);
387            if (i.hasNext()) {
388              sb.append('\n');
389            }
390          }
391          sb.append('\n');
392        }
393
394        // We propose
395        term.write(Text.create(sb.toString()));
396
397        // Rewrite prompt
398        writePromptFlush();
399
400        // If we have common completion we append it now
401        if (commonCompletion.length() > 0) {
402          term.getDirectBuffer().append(delimiter.escape(commonCompletion));
403        }
404      }
405    }
406    catch (IOException e) {
407      log.log(Level.SEVERE, "Could not write completion", e);
408    }
409  }
410}