001    /* StringContent.java --
002       Copyright (C) 2005, 2006 Free Software Foundation, Inc.
003    
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing.text;
040    
041    import java.io.Serializable;
042    import java.lang.ref.Reference;
043    import java.lang.ref.ReferenceQueue;
044    import java.lang.ref.WeakReference;
045    import java.util.Iterator;
046    import java.util.Vector;
047    
048    import javax.swing.undo.AbstractUndoableEdit;
049    import javax.swing.undo.CannotRedoException;
050    import javax.swing.undo.CannotUndoException;
051    import javax.swing.undo.UndoableEdit;
052    
053    /**
054     * An implementation of the <code>AbstractDocument.Content</code>
055     * interface useful for small documents or debugging. The character
056     * content is a simple character array. It's not really efficient.
057     * 
058     * <p>Do not use this class for large size.</p>
059     */
060    public final class StringContent 
061      implements AbstractDocument.Content, Serializable
062    {
063      /**
064       * Stores a reference to a mark that can be resetted to the original value
065       * after a mark has been moved. This is used for undoing actions. 
066       */
067      private class UndoPosRef
068      {
069        /**
070         * The mark that might need to be reset.
071         */
072        private Mark mark;
073    
074        /**
075         * The original offset to reset the mark to.
076         */
077        private int undoOffset;
078    
079        /**
080         * Creates a new UndoPosRef.
081         *
082         * @param m the mark
083         */
084        UndoPosRef(Mark m)
085        {
086          mark = m;
087          undoOffset = mark.mark;
088        }
089    
090        /**
091         * Resets the position of the mark to the value that it had when
092         * creating this UndoPosRef.
093         */
094        void reset()
095        {
096          mark.mark = undoOffset;
097        }
098      }
099    
100      /**
101       * Holds a mark into the buffer that is used by StickyPosition to find
102       * the actual offset of the position. This is pulled out of the
103       * GapContentPosition object so that the mark and position can be handled
104       * independently, and most important, so that the StickyPosition can
105       * be garbage collected while we still hold a reference to the Mark object. 
106       */
107      private class Mark
108      {
109        /**
110         * The actual mark into the buffer.
111         */
112        int mark;
113    
114    
115        /**
116         * The number of GapContentPosition object that reference this mark. If
117         * it reaches zero, it get's deleted by
118         * {@link StringContent#garbageCollect()}.
119         */
120        int refCount;
121    
122        /**
123         * Creates a new Mark object for the specified offset.
124         *
125         * @param offset the offset
126         */
127        Mark(int offset)
128        {
129          mark = offset;
130        }
131      }
132    
133      /** The serialization UID (compatible with JDK1.5). */
134      private static final long serialVersionUID = 4755994433709540381L;
135    
136      // This is package-private to avoid an accessor method.
137      char[] content;
138    
139      private int count;
140    
141      /**
142       * Holds the marks for the positions.
143       *
144       * This is package private to avoid accessor methods.
145       */
146      Vector marks;
147    
148      private class InsertUndo extends AbstractUndoableEdit
149      {
150        private int start;
151        
152        private int length;
153    
154        private String redoContent;
155    
156        private Vector positions;
157    
158        public InsertUndo(int start, int length)
159        {
160          super();
161          this.start = start;
162          this.length = length;
163        }
164    
165        public void undo()
166        {
167          super.undo();
168          try
169            {
170              if (marks != null)
171                positions = getPositionsInRange(null, start, length);
172              redoContent = getString(start, length);
173              remove(start, length);
174            }
175          catch (BadLocationException b)
176            {
177              throw new CannotUndoException();
178            }
179        }
180        
181        public void redo()
182        {
183          super.redo();
184          try
185            {
186              insertString(start, redoContent);
187              redoContent = null;
188              if (positions != null)
189                {
190                  updateUndoPositions(positions);
191                  positions = null;
192                }
193            }
194          catch (BadLocationException b)
195            {
196              throw new CannotRedoException();
197            }
198        }
199      }
200    
201      private class RemoveUndo extends AbstractUndoableEdit
202      {
203        private int start;
204        private int len;
205        private String undoString;
206    
207        Vector positions;
208    
209        public RemoveUndo(int start, String str)
210        {
211          super();
212          this.start = start;
213          len = str.length();
214          this.undoString = str;
215          if (marks != null)
216            positions = getPositionsInRange(null, start, str.length());
217        }
218    
219        public void undo()
220        {
221          super.undo();
222          try
223            {
224              StringContent.this.insertString(this.start, this.undoString);
225              if (positions != null)
226                {
227                  updateUndoPositions(positions);
228                  positions = null;
229                }
230              undoString = null;
231            }
232          catch (BadLocationException bad)
233            {
234              throw new CannotUndoException();
235            }
236        }
237    
238        public void redo()
239        {
240          super.redo();
241          try
242            {
243              undoString = getString(start, len);
244              if (marks != null)
245                positions = getPositionsInRange(null, start, len);
246              remove(this.start, len);
247            }
248          catch (BadLocationException bad)
249            {
250              throw new CannotRedoException();
251            }
252        }
253      }
254    
255      private class StickyPosition implements Position
256      {
257        Mark mark;
258    
259        public StickyPosition(int offset)
260        {
261          // Try to make space.
262          garbageCollect();
263    
264          mark = new Mark(offset);
265          mark.refCount++;
266          marks.add(mark);
267    
268          new WeakReference(this, queueOfDeath);
269        }
270    
271        /**
272         * Should be >=0.
273         */
274        public int getOffset()
275        {
276          return mark.mark;
277        }
278      }
279    
280      /**
281       * Used in {@link #remove(int,int)}.
282       */
283      private static final char[] EMPTY = new char[0];
284    
285      /**
286       * Queues all references to GapContentPositions that are about to be
287       * GC'ed. This is used to remove the corresponding marks from the
288       * positionMarks array if the number of references to that mark reaches zero.
289       *
290       * This is package private to avoid accessor synthetic methods.
291       */
292      ReferenceQueue queueOfDeath;
293    
294      /**
295       * Creates a new instance containing the string "\n".  This is equivalent
296       * to calling {@link #StringContent(int)} with an <code>initialLength</code>
297       * of 10.
298       */
299      public StringContent()
300      {
301        this(10);
302      }
303    
304      /**
305       * Creates a new instance containing the string "\n".
306       * 
307       * @param initialLength  the initial length of the underlying character 
308       *                       array used to store the content.
309       */
310      public StringContent(int initialLength)
311      {
312        super();
313        queueOfDeath = new ReferenceQueue();
314        if (initialLength < 1)
315          initialLength = 1;
316        this.content = new char[initialLength];
317        this.content[0] = '\n';
318        this.count = 1;
319      }
320    
321      protected Vector getPositionsInRange(Vector v,
322                                           int offset,
323                                           int length)
324      {
325        Vector refPos = v == null ? new Vector() : v;
326        Iterator iter = marks.iterator();
327        while(iter.hasNext())
328          {
329            Mark m = (Mark) iter.next();
330            if (offset <= m.mark && m.mark <= offset + length)
331              refPos.add(new UndoPosRef(m));
332          }
333        return refPos;
334      }
335    
336      /**
337       * Creates a position reference for the character at the given offset.  The
338       * position offset will be automatically updated when new characters are
339       * inserted into or removed from the content.
340       * 
341       * @param offset  the character offset.
342       * 
343       * @throws BadLocationException if offset is outside the bounds of the 
344       *         content.
345       */
346      public Position createPosition(int offset) throws BadLocationException
347      {
348        // Lazily create marks vector.
349        if (marks == null)
350          marks = new Vector();
351        StickyPosition sp = new StickyPosition(offset);
352        return sp;
353      }
354      
355      /**
356       * Returns the length of the string content, including the '\n' character at
357       * the end.
358       * 
359       * @return The length of the string content.
360       */
361      public int length()
362      {
363        return count;
364      }
365      
366      /**
367       * Inserts <code>str</code> at the given position and returns an 
368       * {@link UndoableEdit} that enables undo/redo support.
369       * 
370       * @param where  the insertion point (must be less than 
371       *               <code>length()</code>).
372       * @param str  the string to insert (<code>null</code> not permitted).
373       * 
374       * @return An object that can undo the insertion.
375       */
376      public UndoableEdit insertString(int where, String str)
377        throws BadLocationException
378      {
379        checkLocation(where, 0);
380        if (where == this.count)
381          throw new BadLocationException("Invalid location", 1);
382        if (str == null)
383          throw new NullPointerException();
384        char[] insert = str.toCharArray();
385        replace(where, 0, insert);
386    
387        // Move all the positions.
388        if (marks != null)
389          {
390            Iterator iter = marks.iterator();
391            int start = where;
392            if (start == 0)
393              start = 1;
394            while (iter.hasNext())
395              {
396                Mark m = (Mark) iter.next();
397                if (m.mark >= start)
398                  m.mark += str.length();
399              }
400          }
401    
402        InsertUndo iundo = new InsertUndo(where, insert.length);
403        return iundo;
404      }
405      
406      /**
407       * Removes the specified range of characters and returns an 
408       * {@link UndoableEdit} that enables undo/redo support.
409       * 
410       * @param where  the starting index.
411       * @param nitems  the number of characters.
412       * 
413       * @return An object that can undo the removal.
414       * 
415       * @throws BadLocationException if the character range extends outside the
416       *         bounds of the content OR includes the last character.
417       */
418      public UndoableEdit remove(int where, int nitems) throws BadLocationException
419      {
420        checkLocation(where, nitems + 1);
421        RemoveUndo rundo = new RemoveUndo(where, new String(this.content, where, 
422            nitems));
423    
424        replace(where, nitems, EMPTY);
425        // Move all the positions.
426        if (marks != null)
427          {
428            Iterator iter = marks.iterator();
429            while (iter.hasNext())
430              {
431                Mark m = (Mark) iter.next();
432                if (m.mark >= where + nitems)
433                  m.mark -= nitems;
434                else if (m.mark >= where)
435                  m.mark = where;
436              }
437          }
438        return rundo;
439      }
440    
441      private void replace(int offs, int numRemove, char[] insert)
442      {
443        int insertLength = insert.length;
444        int delta = insertLength - numRemove;
445        int src = offs + numRemove;
446        int numMove = count - src;
447        int dest = src + delta;
448        if (count + delta >= content.length)
449          {
450            // Grow data array.
451            int newLength = Math.max(2 * content.length, count + delta);
452            char[] newContent = new char[newLength];
453            System.arraycopy(content, 0, newContent, 0, offs);
454            System.arraycopy(insert, 0, newContent, offs, insertLength);
455            System.arraycopy(content, src, newContent, dest, numMove);
456            content = newContent;
457          }
458        else
459          {
460            System.arraycopy(content, src, content, dest, numMove);
461            System.arraycopy(insert, 0, content, offs, insertLength);
462          }
463        count += delta;
464      }
465    
466      /**
467       * Returns a new <code>String</code> containing the characters in the 
468       * specified range.
469       * 
470       * @param where  the start index.
471       * @param len  the number of characters.
472       * 
473       * @return A string.
474       * 
475       * @throws BadLocationException if the requested range of characters extends 
476       *         outside the bounds of the content.
477       */
478      public String getString(int where, int len) throws BadLocationException
479      {
480        // The RI throws a StringIndexOutOfBoundsException here, which
481        // smells like a bug. We throw a BadLocationException instead.
482        checkLocation(where, len);
483        return new String(this.content, where, len);
484      }
485      
486      /**
487       * Updates <code>txt</code> to contain a direct reference to the underlying 
488       * character array.
489       * 
490       * @param where  the index of the first character.
491       * @param len  the number of characters.
492       * @param txt  a carrier for the return result (<code>null</code> not 
493       *             permitted).
494       *             
495       * @throws BadLocationException if the requested character range is not 
496       *                              within the bounds of the content.
497       * @throws NullPointerException if <code>txt</code> is <code>null</code>.
498       */
499      public void getChars(int where, int len, Segment txt) 
500        throws BadLocationException
501      {
502        if (where + len > count)
503          throw new BadLocationException("Invalid location", where + len);
504        txt.array = content;
505        txt.offset = where;
506        txt.count = len;
507      }
508    
509    
510      /**
511       * Resets the positions in the specified vector to their original offset
512       * after a undo operation is performed. For example, after removing some
513       * content, the positions in the removed range will all be set to one
514       * offset. This method restores the positions to their original offsets
515       * after an undo.
516       */
517      protected void updateUndoPositions(Vector positions)
518      {
519        for (Iterator i = positions.iterator(); i.hasNext();)
520          {
521            UndoPosRef pos = (UndoPosRef) i.next();
522            pos.reset();
523          }
524      }
525    
526      /** 
527       * A utility method that checks the validity of the specified character
528       * range.
529       * 
530       * @param where  the first character in the range.
531       * @param len  the number of characters in the range.
532       * 
533       * @throws BadLocationException if the specified range is not within the
534       *         bounds of the content.
535       */
536      void checkLocation(int where, int len) throws BadLocationException
537      {
538        if (where < 0)
539          throw new BadLocationException("Invalid location", 1);
540        else if (where > this.count)
541          throw new BadLocationException("Invalid location", this.count);
542        else if ((where + len) > this.count)
543          throw new BadLocationException("Invalid range", this.count);
544      }
545    
546      /**
547       * Polls the queue of death for GapContentPositions, updates the
548       * corresponding reference count and removes the corresponding mark
549       * if the refcount reaches zero.
550       *
551       * This is package private to avoid accessor synthetic methods.
552       */
553      void garbageCollect()
554      {
555        Reference ref = queueOfDeath.poll();
556        while (ref != null)
557          {
558            if (ref != null)
559              {
560                StickyPosition pos = (StickyPosition) ref.get();
561                Mark m = pos.mark;
562                m.refCount--;
563                if (m.refCount == 0)
564                  marks.remove(m);
565              }
566            ref = queueOfDeath.poll();
567          }
568      }
569    }
570