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.telnet.term.console;
021    
022    import org.crsh.telnet.term.spi.TermIO;
023    
024    import java.io.IOException;
025    import java.util.Iterator;
026    import java.util.LinkedList;
027    import java.util.NoSuchElementException;
028    
029    public 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    }