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 }