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