001 package org.crsh.processor.term;
002
003 import org.crsh.cmdline.CommandCompletion;
004 import org.crsh.cmdline.Delimiter;
005 import org.crsh.cmdline.spi.ValueCompletion;
006 import org.crsh.shell.Shell;
007 import org.crsh.shell.ShellProcess;
008 import org.crsh.text.CharReader;
009 import org.crsh.term.Term;
010 import org.crsh.term.TermEvent;
011 import org.crsh.util.CloseableList;
012 import org.crsh.util.Strings;
013 import org.slf4j.Logger;
014 import org.slf4j.LoggerFactory;
015
016 import java.io.Closeable;
017 import java.io.IOException;
018 import java.util.Iterator;
019 import java.util.LinkedList;
020 import java.util.Map;
021
022 /** @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a> */
023 public final class Processor implements Runnable {
024
025 /** . */
026 static final Runnable NOOP = new Runnable() {
027 public void run() {
028 }
029 };
030
031 /** . */
032 final Runnable WRITE_PROMPT = new Runnable() {
033 public void run() {
034 writePrompt();
035 }
036 };
037
038 /** . */
039 final Runnable CLOSE = new Runnable() {
040 public void run() {
041 close();
042 }
043 };
044
045 /** . */
046 private final Runnable READ_TERM = new Runnable() {
047 public void run() {
048 readTerm();
049 }
050 };
051
052 /** . */
053 final Logger log = LoggerFactory.getLogger(Processor.class);
054
055 /** . */
056 final Term term;
057
058 /** . */
059 final Shell shell;
060
061 /** . */
062 final LinkedList<TermEvent> queue;
063
064 /** . */
065 final Object lock;
066
067 /** . */
068 ProcessContext current;
069
070 /** . */
071 Status status;
072
073 /** A flag useful for unit testing to know when the thread is reading. */
074 volatile boolean waitingEvent;
075
076 /** . */
077 private final CloseableList listeners;
078
079 public Processor(Term term, Shell shell) {
080 this.term = term;
081 this.shell = shell;
082 this.queue = new LinkedList<TermEvent>();
083 this.lock = new Object();
084 this.status = Status.AVAILABLE;
085 this.listeners = new CloseableList();
086 this.waitingEvent = false;
087 }
088
089 public boolean isWaitingEvent() {
090 return waitingEvent;
091 }
092
093 public void run() {
094
095
096 // Display initial stuff
097 try {
098 String welcome = shell.getWelcome();
099 log.debug("Writing welcome message to term");
100 term.write(new CharReader(welcome));
101 log.debug("Wrote welcome message to term");
102 writePrompt();
103 }
104 catch (IOException e) {
105 e.printStackTrace();
106 }
107
108 //
109 while (true) {
110 try {
111 if (!iterate()) {
112 break;
113 }
114 }
115 catch (IOException e) {
116 e.printStackTrace();
117 }
118 catch (InterruptedException e) {
119 break;
120 }
121 }
122 }
123
124 boolean iterate() throws InterruptedException, IOException {
125
126 //
127 Runnable runnable;
128 synchronized (lock) {
129 switch (status) {
130 case AVAILABLE:
131 runnable = peekProcess();
132 if (runnable != null) {
133 break;
134 }
135 case PROCESSING:
136 case CANCELLING:
137 runnable = READ_TERM;
138 break;
139 case CLOSED:
140 return false;
141 default:
142 throw new AssertionError();
143 }
144 }
145
146 //
147 runnable.run();
148
149 //
150 return true;
151 }
152
153 // We assume this is called under lock synchronization
154 ProcessContext peekProcess() {
155 while (true) {
156 synchronized (lock) {
157 if (status == Status.AVAILABLE) {
158 if (queue.size() > 0) {
159 TermEvent event = queue.removeFirst();
160 if (event instanceof TermEvent.Complete) {
161 complete(((TermEvent.Complete)event).getLine());
162 } else {
163 String line = ((TermEvent.ReadLine)event).getLine().toString();
164 if (line.length() > 0) {
165 term.addToHistory(line);
166 }
167 ShellProcess process = shell.createProcess(line);
168 current = new ProcessContext(this, process);
169 status = Status.PROCESSING;
170 return current;
171 }
172 } else {
173 break;
174 }
175 } else {
176 break;
177 }
178 }
179 }
180 return null;
181 }
182
183 /** . */
184 private final Object termLock = new Object();
185
186 private boolean reading = false;
187
188 void readTerm() {
189
190 //
191 synchronized (termLock) {
192 if (reading) {
193 try {
194 termLock.wait();
195 return;
196 }
197 catch (InterruptedException e) {
198 throw new AssertionError(e);
199 }
200 } else {
201 reading = true;
202 }
203 }
204
205 //
206 try {
207 TermEvent event = term.read();
208
209 //
210 Runnable runnable;
211 if (event instanceof TermEvent.Break) {
212 synchronized (lock) {
213 queue.clear();
214 if (status == Status.PROCESSING) {
215 status = Status.CANCELLING;
216 runnable = new Runnable() {
217 ProcessContext context = current;
218 public void run() {
219 context.process.cancel();
220 }
221 };
222 }
223 else if (status == Status.AVAILABLE) {
224 runnable = WRITE_PROMPT;
225 } else {
226 runnable = NOOP;
227 }
228 }
229 } else if (event instanceof TermEvent.Close) {
230 synchronized (lock) {
231 queue.clear();
232 if (status == Status.PROCESSING) {
233 runnable = new Runnable() {
234 ProcessContext context = current;
235 public void run() {
236 context.process.cancel();
237 close();
238 }
239 };
240 } else if (status != Status.CLOSED) {
241 runnable = CLOSE;
242 } else {
243 runnable = NOOP;
244 }
245 status = Status.CLOSED;
246 }
247 } else {
248 synchronized (queue) {
249 queue.addLast(event);
250 runnable = NOOP;
251 }
252 }
253
254 //
255 runnable.run();
256 }
257 catch (IOException e) {
258 log.error("Error when reading term", e);
259 }
260 finally {
261 synchronized (termLock) {
262 reading = false;
263 termLock.notifyAll();
264 }
265 }
266 }
267
268 void close() {
269 listeners.close();
270 }
271
272 public void addListener(Closeable listener) {
273 listeners.add(listener);
274 }
275
276 void write(CharReader reader) {
277 try {
278 term.write(reader);
279 }
280 catch (IOException e) {
281 log.error("Write to term failure", e);
282 }
283 }
284
285 void writePrompt() {
286 String prompt = shell.getPrompt();
287 try {
288 String p = prompt == null ? "% " : prompt;
289 CharReader cr = new CharReader().append("\r\n").append(p);
290 CharSequence buffer = term.getBuffer();
291 if (buffer != null) {
292 cr.append(buffer);
293 }
294 term.write(cr);
295 } catch (IOException e) {
296 e.printStackTrace();
297 }
298 }
299
300 private void complete(CharSequence prefix) {
301 log.debug("About to get completions for " + prefix);
302 CommandCompletion completion = shell.complete(prefix.toString());
303 ValueCompletion completions = completion.getValue();
304 log.debug("Completions for " + prefix + " are " + completions);
305
306 //
307 Delimiter delimiter = completion.getDelimiter();
308
309 try {
310 // Try to find the greatest prefix among all the results
311 if (completions.getSize() == 0) {
312 // Do nothing
313 } else if (completions.getSize() == 1) {
314 Map.Entry<String, Boolean> entry = completions.iterator().next();
315 Appendable buffer = term.getInsertBuffer();
316 String insert = entry.getKey();
317 delimiter.escape(insert, term.getInsertBuffer());
318 if (entry.getValue()) {
319 buffer.append(completion.getDelimiter().getValue());
320 }
321 } else {
322 String commonCompletion = Strings.findLongestCommonPrefix(completions.getSuffixes());
323 if (commonCompletion.length() > 0) {
324 delimiter.escape(commonCompletion, term.getInsertBuffer());
325 } else {
326 // Format stuff
327 int width = term.getWidth();
328
329 //
330 String completionPrefix = completions.getPrefix();
331
332 // Get the max length
333 int max = 0;
334 for (String suffix : completions.getSuffixes()) {
335 max = Math.max(max, completionPrefix.length() + suffix.length());
336 }
337
338 // Separator : use two whitespace like in BASH
339 max += 2;
340
341 //
342 StringBuilder sb = new StringBuilder().append('\n');
343 if (max < width) {
344 int columns = width / max;
345 int index = 0;
346 for (String suffix : completions.getSuffixes()) {
347 sb.append(completionPrefix).append(suffix);
348 for (int l = completionPrefix.length() + suffix.length();l < max;l++) {
349 sb.append(' ');
350 }
351 if (++index >= columns) {
352 index = 0;
353 sb.append('\n');
354 }
355 }
356 if (index > 0) {
357 sb.append('\n');
358 }
359 } else {
360 for (Iterator<String> i = completions.getSuffixes().iterator();i.hasNext();) {
361 String suffix = i.next();
362 sb.append(commonCompletion).append(suffix);
363 if (i.hasNext()) {
364 sb.append('\n');
365 }
366 }
367 sb.append('\n');
368 }
369
370 // We propose
371 term.write(new CharReader(sb.toString()));
372 writePrompt();
373 }
374 }
375 }
376 catch (IOException e) {
377 log.error("Could not write completion", e);
378 }
379 }
380 }