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