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 }