001    /*
002     * Copyright (C) 2010 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.term.processor;
021    
022    import org.crsh.shell.Shell;
023    import org.crsh.shell.ShellProcess;
024    import org.crsh.term.Term;
025    import org.crsh.term.TermEvent;
026    import org.crsh.util.FutureListener;
027    import org.crsh.util.LatchedFuture;
028    import org.crsh.util.Strings;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    import java.io.Closeable;
033    import java.io.IOException;
034    import java.util.ArrayList;
035    import java.util.Iterator;
036    import java.util.List;
037    import java.util.Map;
038    import java.util.concurrent.ExecutionException;
039    import java.util.concurrent.atomic.AtomicReference;
040    
041    /**
042     * <p>The processor is the glue between a term object and a shell object. It mainly read the input from the
043     * term and executes shell commands.</p>
044     * 
045     * <p>The class implements the {@link Runnable} interface to perform its processing.</p>
046     *
047     * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
048     * @version $Revision$
049     */
050    public final class Processor implements Runnable {
051    
052      /** . */
053      final Logger log = LoggerFactory.getLogger(Processor.class);
054    
055      /** . */
056      final Term term;
057    
058      /** . */
059      private final AtomicReference<Status> status;
060    
061      /** . */
062      private final Shell shell;
063    
064      /** The current process being executed. */
065      volatile ShellProcess process;
066    
067      /** . */
068      private final List<Closeable> listeners;
069    
070      public Processor(Term term, Shell shell) {
071        this.term = term;
072        this.status = new AtomicReference<Status>(State.INITIAL.available);
073        this.shell = shell;
074        this.process = null;
075        this.listeners = new ArrayList<Closeable>();
076      }
077    
078      public void run() {
079        while (true) {
080          Result result = execute();
081          State state = result.getState();
082          if (state == State.CLOSED) {
083            break;
084          }
085        }
086      }
087    
088      public boolean isAvailable() {
089        return process == null;
090      }
091    
092      public State getState() {
093        return status.get().getState();
094      }
095    
096      public Result execute() {
097    
098        //
099        final Status _status = status.get();
100    
101        Task task = null;
102    
103        if (_status == State.INITIAL.available) {
104    
105          task = new Task() {
106            @Override
107            protected LatchedFuture<State> execute() {
108              try {
109                String welcome = shell.getWelcome();
110                log.debug("Writing welcome message to term");
111                term.write(welcome);
112                log.debug("Wrote welcome message to term");
113                writePrompt();
114              }
115              catch (IOException e) {
116                e.printStackTrace();
117              }
118              return new LatchedFuture<State>(State.OPEN);
119            }
120          };
121    
122        } else if (_status == State.OPEN.available) {
123    
124          task = new Task() {
125            @Override
126            protected LatchedFuture<State> execute() {
127              //
128              TermEvent event = null;
129    
130              try {
131                log.debug("About to read next term event");
132                event = term.read();
133                log.debug("Read next term event " + event);
134              } catch (IOException e) {
135                if (status.get() == State.OPEN.available) {
136                  log.error("Could not read term data", e);
137                } else {
138                  log.debug("Exception but term is considered as closed", e);
139                  // We continue it will lead to getting out of the loop
140                }
141              }
142    
143              //
144              if (event == null) {
145                // Do nothing wait until next event
146                return new LatchedFuture<State>(State.OPEN);
147              } else if (event instanceof TermEvent.ReadLine) {
148                String line = ((TermEvent.ReadLine)event).getLine().toString();
149                log.debug("Submitting command " + line);
150    
151                //
152                ShellInvoker invoker = new ShellInvoker(Processor.this);
153    
154                // Process
155                process = shell.createProcess(((TermEvent.ReadLine) event).getLine().toString());
156    
157                //
158                process.execute(invoker);
159    
160                //
161                if (line.length() > 0) {
162                  term.addToHistory(line);
163                }
164    
165                //
166                return invoker.result;
167              } else if (event instanceof TermEvent.Break) {
168                if (process != null) {
169                  process.cancel();
170                } else {
171                  log.debug("Ignoring action " + event);
172                  writePrompt();
173                }
174                return new LatchedFuture<State>(State.OPEN);
175              } else if (event instanceof TermEvent.Complete) {
176                TermEvent.Complete complete = (TermEvent.Complete)event;
177                String prefix = complete.getLine().toString();
178                log.debug("About to get completions for " + prefix);
179                Map<String, String> completions = shell.complete(prefix);
180                log.debug("Completions for " + prefix + " are " + completions);
181    
182                // Try to find the greatest prefix among all the results
183                String commonCompletion;
184                if (completions.size() == 0) {
185                  commonCompletion = "";
186                } else if (completions.size() == 1) {
187                  Map.Entry<String, String> entry = completions.entrySet().iterator().next();
188                  commonCompletion = entry.getKey() + entry.getValue();
189                } else {
190                  commonCompletion = Strings.findLongestCommonPrefix(completions.keySet());
191                }
192    
193                //
194                if (commonCompletion.length() > 0) {
195                  try {
196                    term.bufferInsert(commonCompletion);
197                  }
198                  catch (IOException e) {
199                    e.printStackTrace();
200                  }
201                } else {
202                  if (completions.size() > 1) {
203                    // We propose
204                    StringBuilder sb = new StringBuilder("\n");
205                    for (Iterator<String> i = completions.keySet().iterator();i.hasNext();) {
206                      String completion = i.next();
207                      sb.append(completion);
208                      if (i.hasNext()) {
209                        sb.append(" ");
210                      }
211                    }
212                    sb.append("\n");
213                    try {
214                      term.write(sb.toString());
215                    }
216                    catch (IOException e) {
217                      e.printStackTrace();
218                    }
219                    writePrompt();
220                  }
221                }
222                return new LatchedFuture<State>(State.OPEN);
223              } else if (event instanceof TermEvent.Close) {
224                return new LatchedFuture<State>(State.WANT_CLOSE);
225              } else {
226                return new LatchedFuture<State>(State.OPEN);
227              }
228            }
229          };
230        } else if (_status == State.WANT_CLOSE.available) {
231    
232          task = new Task() {
233            @Override
234            protected LatchedFuture<State> execute() {
235              //
236              log.debug("Closing processor");
237    
238              // Make a copy
239              ArrayList<Closeable> listeners;
240              synchronized (Processor.this.listeners) {
241                listeners = new ArrayList<Closeable>(Processor.this.listeners);
242              }
243    
244              // Status to closed, we won't process any further request
245              // it's important to set the status before closing anything to
246              // avoid a race condition because closing a listener may
247              // cause an interrupted exception
248              status.set(State.CLOSED.available);
249    
250              //
251              for (Closeable listener : listeners) {
252                try {
253                  log.debug("Closing " + listener.getClass().getSimpleName());
254                  listener.close();
255                }
256                catch (Exception e) {
257                  e.printStackTrace();
258                }
259              }
260    
261              //
262              return new LatchedFuture<State>(State.CLOSED);
263            }
264          };
265        } else {
266          //
267        }
268    
269        //
270        if (task != null) {
271    
272          // Add listener to update our state
273          final LatchedFuture<State> futureState = task.execute();
274    
275          // It will update the status
276          futureState.addListener(new FutureListener<State>() {
277            public void completed(State value) {
278              status.set(value.available);
279            }
280          });
281    
282          //
283          return new Result() {
284            public State getState() {
285              try {
286                return futureState.get();
287              } catch (InterruptedException e) {
288                return status.get().getState();
289              } catch (ExecutionException e) {
290                return status.get().getState();
291              }
292            }
293          };
294        } else {
295          return new Result() {
296            public State getState() {
297              return _status.getState();
298            }
299          };
300        }
301      }
302    
303      void writePrompt() {
304        String prompt = shell.getPrompt();
305        try {
306          String p = prompt == null ? "% " : prompt;
307          term.write("\r\n");
308          term.write(p);
309          term.write(term.getBuffer());
310        } catch (IOException e) {
311          e.printStackTrace();
312        }
313      }
314    
315      public void addListener(Closeable listener) {
316        if (listener == null) {
317          throw new NullPointerException();
318        }
319        synchronized (listeners) {
320          if (listeners.contains(listener)) {
321            throw new IllegalStateException("Already listening");
322          }
323          listeners.add(listener);
324        }
325      }
326    }