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.Pipe;
023    import org.crsh.cmdline.CommandCompletion;
024    import org.crsh.cmdline.Delimiter;
025    import org.crsh.cmdline.spi.ValueCompletion;
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    import org.slf4j.Logger;
035    import org.slf4j.LoggerFactory;
036    
037    import java.io.Closeable;
038    import java.io.IOException;
039    import java.util.Iterator;
040    import java.util.LinkedList;
041    import java.util.Map;
042    
043    public final class Processor implements Runnable, Pipe<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 = LoggerFactory.getLogger(Processor.class);
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.debug("Writing welcome message to term");
120          term.provide(new Text(welcome));
121          log.debug("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.error("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 void provide(Chunk element) throws IOException {
297        term.provide(element);
298      }
299    
300      public void flush() throws IOException {
301        throw new UnsupportedOperationException("what does it mean?");
302      }
303    
304      void writePromptFlush() {
305        String prompt = shell.getPrompt();
306        try {
307          String p = prompt == null ? "% " : prompt;
308          StringBuilder sb = new StringBuilder("\r\n").append(p);
309          CharSequence buffer = term.getBuffer();
310          if (buffer != null) {
311            sb.append(buffer);
312          }
313          term.provide(new Text(sb));
314          term.flush();
315        } catch (IOException e) {
316          // Todo : improve that
317          e.printStackTrace();
318        }
319      }
320    
321      private void complete(CharSequence prefix) {
322        log.debug("About to get completions for " + prefix);
323        CommandCompletion completion = shell.complete(prefix.toString());
324        ValueCompletion completions = completion.getValue();
325        log.debug("Completions for " + prefix + " are " + completions);
326    
327        //
328        Delimiter delimiter = completion.getDelimiter();
329    
330        try {
331          // Try to find the greatest prefix among all the results
332          if (completions.getSize() == 0) {
333            // Do nothing
334          } else if (completions.getSize() == 1) {
335            Map.Entry<String, Boolean> entry = completions.iterator().next();
336            Appendable buffer = term.getInsertBuffer();
337            String insert = entry.getKey();
338            term.getInsertBuffer().append(delimiter.escape(insert));
339            if (entry.getValue()) {
340              buffer.append(completion.getDelimiter().getValue());
341            }
342          } else {
343            String commonCompletion = Strings.findLongestCommonPrefix(completions.getSuffixes());
344            if (commonCompletion.length() > 0) {
345              term.getInsertBuffer().append(delimiter.escape(commonCompletion));
346            } else {
347              // Format stuff
348              int width = term.getWidth();
349    
350              //
351              String completionPrefix = completions.getPrefix();
352    
353              // Get the max length
354              int max = 0;
355              for (String suffix : completions.getSuffixes()) {
356                max = Math.max(max, completionPrefix.length() + suffix.length());
357              }
358    
359              // Separator : use two whitespace like in BASH
360              max += 2;
361    
362              //
363              StringBuilder sb = new StringBuilder().append('\n');
364              if (max < width) {
365                int columns = width / max;
366                int index = 0;
367                for (String suffix : completions.getSuffixes()) {
368                  sb.append(completionPrefix).append(suffix);
369                  for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
370                    sb.append(' ');
371                  }
372                  if (++index >= columns) {
373                    index = 0;
374                    sb.append('\n');
375                  }
376                }
377                if (index > 0) {
378                  sb.append('\n');
379                }
380              } else {
381                for (Iterator<String> i = completions.getSuffixes().iterator();i.hasNext();) {
382                  String suffix = i.next();
383                  sb.append(commonCompletion).append(suffix);
384                  if (i.hasNext()) {
385                    sb.append('\n');
386                  }
387                }
388                sb.append('\n');
389              }
390    
391              // We propose
392              term.provide(new Text(sb.toString()));
393              writePromptFlush();
394            }
395          }
396        }
397        catch (IOException e) {
398          log.error("Could not write completion", e);
399        }
400      }
401    }