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      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.provide(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.provide(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          String p = prompt == null ? "% " : prompt;
312          StringBuilder sb = new StringBuilder("\r\n").append(p);
313          CharSequence buffer = term.getBuffer();
314          if (buffer != null) {
315            sb.append(buffer);
316          }
317          term.provide(Text.create(sb));
318          term.flush();
319        } catch (IOException e) {
320          // Todo : improve that
321          e.printStackTrace();
322        }
323      }
324    
325      private void complete(CharSequence prefix) {
326        log.log(Level.FINE, "About to get completions for " + prefix);
327        CompletionMatch completion = shell.complete(prefix.toString());
328        Completion completions = completion.getValue();
329        log.log(Level.FINE, "Completions for " + prefix + " are " + completions);
330    
331        //
332        Delimiter delimiter = completion.getDelimiter();
333    
334        try {
335          // Try to find the greatest prefix among all the results
336          if (completions.getSize() == 0) {
337            // Do nothing
338          } else if (completions.getSize() == 1) {
339            Map.Entry<String, Boolean> entry = completions.iterator().next();
340            Appendable buffer = term.getDirectBuffer();
341            String insert = entry.getKey();
342            term.getDirectBuffer().append(delimiter.escape(insert));
343            if (entry.getValue()) {
344              buffer.append(completion.getDelimiter().getValue());
345            }
346          } else {
347            String commonCompletion = Strings.findLongestCommonPrefix(completions.getValues());
348            if (commonCompletion.length() > 0) {
349              term.getDirectBuffer().append(delimiter.escape(commonCompletion));
350            } else {
351              // Format stuff
352              int width = term.getWidth();
353    
354              //
355              String completionPrefix = completions.getPrefix();
356    
357              // Get the max length
358              int max = 0;
359              for (String suffix : completions.getValues()) {
360                max = Math.max(max, completionPrefix.length() + suffix.length());
361              }
362    
363              // Separator : use two whitespace like in BASH
364              max += 2;
365    
366              //
367              StringBuilder sb = new StringBuilder().append('\n');
368              if (max < width) {
369                int columns = width / max;
370                int index = 0;
371                for (String suffix : completions.getValues()) {
372                  sb.append(completionPrefix).append(suffix);
373                  for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
374                    sb.append(' ');
375                  }
376                  if (++index >= columns) {
377                    index = 0;
378                    sb.append('\n');
379                  }
380                }
381                if (index > 0) {
382                  sb.append('\n');
383                }
384              } else {
385                for (Iterator<String> i = completions.getValues().iterator();i.hasNext();) {
386                  String suffix = i.next();
387                  sb.append(commonCompletion).append(suffix);
388                  if (i.hasNext()) {
389                    sb.append('\n');
390                  }
391                }
392                sb.append('\n');
393              }
394    
395              // We propose
396              term.provide(Text.create(sb.toString()));
397              writePromptFlush();
398            }
399          }
400        }
401        catch (IOException e) {
402          log.log(Level.SEVERE, "Could not write completion", e);
403        }
404      }
405    }