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
020package org.crsh.telnet.term.console;
021
022import org.crsh.telnet.term.spi.TermIO;
023
024import java.io.IOException;
025import java.util.Iterator;
026import java.util.LinkedList;
027import java.util.NoSuchElementException;
028
029public final class TermIOBuffer implements Appendable, Iterator<CharSequence> {
030
031  /** . */
032  private char[] buffer;
033
034  /** . */
035  private int size;
036
037  /** Cursor Position, always equal to {@link #size} unless the underlying *.IO class supports editing. */
038  private int curAt;
039
040  /** . */
041  private LinkedList<CharSequence> lines;
042
043  /** Do we have a issued a CR previously? */
044  private boolean previousCR;
045
046  /** Whether or not we do echoing. */
047  private boolean echoing;
048
049  /** . */
050  private final TermIO io;
051
052  public TermIOBuffer(TermIO io) {
053    this.buffer = new char[128];
054    this.size = 0;
055    this.curAt = 0;
056    this.lines = new LinkedList<CharSequence>();
057    this.previousCR = false;
058    this.echoing = true;
059    this.io = io;
060  }
061
062  /**
063   * Clears the buffer without doing any echoing.
064   */
065  public void clear() {
066    this.previousCR = false;
067    this.curAt = 0;
068    this.size = 0;
069  }
070
071  /**
072   * Returns the total number of chars in the buffer, independently of the cursor position.
073   *
074   * @return the number of chars
075   */
076  public int getSize() {
077    return size;
078  }
079
080  /**
081   * Returns the current cursor position.
082   *
083   * @return the cursor position
084   */
085  int getCursor() {
086    return curAt;
087  }
088
089  /**
090   * Returns a character at a specified index in the buffer.
091   *
092   * @param index the index
093   * @return the char
094   * @throws IndexOutOfBoundsException if the index is negative or larget than the size
095   */
096  char charAt(int index) throws IndexOutOfBoundsException {
097    if (index < 0) {
098      throw new IndexOutOfBoundsException("No negative position accepted");
099    }
100    if (index >= size) {
101      throw new IndexOutOfBoundsException("Cannot accept position greater than size:" + index + " >= " + size);
102    }
103    return buffer[index];
104  }
105
106  CharSequence getBufferToCursor() {
107    return new String(buffer, 0, curAt);
108  }
109
110  boolean isEchoing() {
111    return echoing;
112  }
113
114  void setEchoing(boolean echoing) {
115    this.echoing = echoing;
116  }
117
118  // Iterator<CharSequence> implementation *****************************************************************************
119
120  public boolean hasNext() {
121    return lines.size() > 0;
122  }
123
124  public CharSequence next() {
125    if (lines.size() > 0) {
126      return lines.removeFirst();
127    } else {
128      throw new NoSuchElementException();
129    }
130  }
131
132  public void remove() {
133    throw new UnsupportedOperationException();
134  }
135
136  // Appendable implementation *****************************************************************************************
137
138  public TermIOBuffer append(char c) throws IOException {
139    if (appendData(c)) {
140      io.flush();
141    }
142    return this;
143  }
144
145  public TermIOBuffer append(CharSequence s) throws IOException {
146    return append(s, 0, s.length());
147  }
148
149  public TermIOBuffer append(CharSequence csq, int start, int end) throws IOException {
150    if (appendData(csq, start, end)) {
151      io.flush();
152    }
153    return this;
154  }
155
156  // Protected methods *************************************************************************************************
157
158  /**
159   * Replace all the characters before the cursor by the provided char sequence.
160   *
161   * @param s the new char sequence
162   * @return the l
163   * @throws IOException any IOException
164   */
165  CharSequence replace(CharSequence s) throws IOException {
166    StringBuilder builder = new StringBuilder();
167    boolean flush = false;
168    for (int i = appendDel();i != -1;i = appendDel()) {
169      builder.append((char)i);
170      flush = true;
171    }
172    flush |= appendData(s, 0, s.length());
173    if (flush) {
174      io.flush();
175    }
176    return builder.reverse().toString();
177  }
178
179  public boolean moveRight() throws IOException {
180    return moveRight(1) == 1;
181  }
182
183  public boolean moveLeft() throws IOException {
184    return moveLeft(1) == 1;
185  }
186
187  public int moveRight(int count) throws IOException, IllegalArgumentException {
188    if (count < 0) {
189      throw new IllegalArgumentException("Cannot move with negative count " + count);
190    }
191    int delta = 0;
192    while (delta < count) {
193      if (curAt + delta < size && io.moveRight(buffer[curAt + delta])) {
194        delta++;
195      } else {
196        break;
197      }
198    }
199    if (delta > 0) {
200      io.flush();
201      curAt += delta;
202    }
203    return delta;
204  }
205
206  int moveLeft(int count) throws IOException, IllegalArgumentException {
207    if (count < 0) {
208      throw new IllegalArgumentException("Cannot move with negative count " + count);
209    }
210    int delta = 0;
211    while (delta < count) {
212      if (delta < curAt && io.moveLeft()) {
213        delta++;
214      } else {
215        break;
216      }
217    }
218    if (delta > 0) {
219      io.flush();
220      curAt -= delta;
221    }
222    return delta;
223  }
224
225  /**
226   * Delete the char under the cursor or return -1 if no char was deleted.
227   *
228   * @return the deleted char
229   * @throws IOException any IOException
230   */
231  public int del() throws IOException {
232    int ret = appendDel();
233    if (ret != -1) {
234      io.flush();
235    }
236    return ret;
237  }
238
239  private boolean appendData(CharSequence s, int start, int end) throws IOException {
240    if (start < 0) {
241      throw new IndexOutOfBoundsException("No negative start");
242    }
243    if (end < 0) {
244      throw new IndexOutOfBoundsException("No negative end");
245    }
246    if (end > s.length()) {
247      throw new IndexOutOfBoundsException("End cannot be greater than sequence length");
248    }
249    if (end < start) {
250      throw new IndexOutOfBoundsException("Start cannot be greater than end");
251    }
252    boolean flush = false;
253    for (int i = start;i < end;i++) {
254      flush |= appendData(s.charAt(i));
255    }
256    return flush;
257  }
258
259  /**
260   * Append a char at the current cursor position and increment the cursor position.
261   *
262   * @param c the char to append
263   * @return true if flush is required
264   * @throws IOException any IOException
265   */
266  private boolean appendData(char c) throws IOException {
267    if (previousCR && c == '\n') {
268      previousCR = false;
269      return false;
270    } else if (c == '\r' || c == '\n') {
271      previousCR = c == '\r';
272      String line = new String(buffer, 0, size);
273      lines.add(line);
274      size = 0;
275      curAt = size;
276      return echoCRLF();
277    } else {
278      if (push(c)) {
279        return echo(c);
280      } else {
281        String disp = new String(buffer, curAt, size - curAt);
282        io.write(disp);
283        int amount = size - curAt - 1;
284        curAt++;
285        while (amount > 0) {
286          io.moveLeft();
287          amount--;
288        }
289        return true;
290      }
291    }
292  }
293
294  /**
295   * Delete the char before the cursor.
296   *
297   * @return the removed char value or -1 if no char was removed
298   * @throws IOException any IOException
299   */
300  private int appendDel() throws IOException {
301
302    // If the cursor is at the most right position (i.e no more chars after)
303    if (curAt == size){
304      int popped = pop();
305
306      //
307      if (popped != -1) {
308        echoDel();
309        // We do not care about the return value of echoDel, but we will return a value that indcates
310        // that a flush is required although it may not
311        // to properly carry out the status we should have two things to return
312        // 1/ the popped char
313        // 2/ the boolean indicating if flush is required
314      }
315
316      //
317      return popped;
318    } else {
319      // We are editing the line
320
321      // Shift all the chars after the cursor
322      int popped = pop();
323
324      //
325      if (popped != -1) {
326
327        // We move the cursor to left
328        if (io.moveLeft()) {
329          StringBuilder disp = new StringBuilder();
330          disp.append(buffer, curAt, size - curAt);
331          disp.append(' ');
332          io.write(disp);
333          int amount = size - curAt + 1;
334          while (amount > 0) {
335            io.moveLeft();
336            amount--;
337          }
338        } else {
339          throw new UnsupportedOperationException("not implemented");
340        }
341      }
342
343      //
344      return popped;
345    }
346  }
347
348  private boolean echo(char c) throws IOException {
349    if (echoing) {
350      io.write(c);
351      return true;
352    } else {
353      return false;
354    }
355  }
356
357  private void echo(String s) throws IOException {
358    if (echoing) {
359      io.write(s);
360      io.flush();
361    }
362  }
363
364  private boolean echoDel() throws IOException {
365    if (echoing) {
366      io.writeDel();
367      return true;
368    } else {
369      return false;
370    }
371  }
372
373  private boolean echoCRLF() throws IOException {
374    if (echoing) {
375      io.writeCRLF();
376      return true;
377    } else {
378      return false;
379    }
380  }
381
382  /**
383   * Popup one char from buffer at the current cursor position.
384   *
385   * @return the popped char or -1 if none was removed
386   */
387  private int pop() {
388    if (curAt > 0) {
389      char popped = buffer[curAt - 1];
390      if (curAt == size) {
391        buffer[curAt] = 0;
392        size = --curAt;
393        return popped;
394      } else {
395        for (int i = curAt;i < size;i++) {
396          buffer[i - 1] = buffer[i];
397        }
398        buffer[--size] = 0;
399        curAt--;
400      }
401      return popped;
402    } else {
403      return -1;
404    }
405  }
406
407  /**
408   * Push  one char in the buffer at the current cursor position. This operation ensures that the buffer
409   * is large enough and it may increase the buffer capacity when required. The cursor position is incremented
410   * when a char is appended at the last position, otherwise the cursor position remains unchanged.
411   *
412   * @param c the char to push
413   * @return true if the cursor position was incremented
414   */
415  private boolean push(char c) {
416    if (size >= buffer.length) {
417      char[] tmp = new char[buffer.length * 2 + 1];
418      System.arraycopy(buffer, 0, tmp, 0, buffer.length);
419      TermIOBuffer.this.buffer = tmp;
420    }
421    if (curAt == size) {
422      buffer[size++] = c;
423      curAt++;
424      return true;
425    } else {
426      for (int i = size - 1;i > curAt - 1;i--) {
427        buffer[i + 1] = buffer[i];
428      }
429      buffer[curAt] = c;
430      ++size;
431      return false;
432    }
433  }
434}