1 /*
   2  * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text;
  26 
  27 import java.util.Vector;
  28 import java.io.Serializable;
  29 import javax.swing.undo.*;
  30 import javax.swing.SwingUtilities;
  31 
  32 /**
  33  * An implementation of the AbstractDocument.Content interface that is
  34  * a brute force implementation that is useful for relatively small
  35  * documents and/or debugging.  It manages the character content
  36  * as a simple character array.  It is also quite inefficient.
  37  * <p>
  38  * It is generally recommended that the gap buffer or piece table
  39  * implementations be used instead.  This buffer does not scale up
  40  * to large sizes.
  41  * <p>
  42  * <strong>Warning:</strong>
  43  * Serialized objects of this class will not be compatible with
  44  * future Swing releases. The current serialization support is
  45  * appropriate for short term storage or RMI between applications running
  46  * the same version of Swing.  As of 1.4, support for long term storage
  47  * of all JavaBeans&trade;
  48  * has been added to the <code>java.beans</code> package.
  49  * Please see {@link java.beans.XMLEncoder}.
  50  *
  51  * @author  Timothy Prinzing
  52  */
  53 @SuppressWarnings("serial") // Same-version serialization only
  54 public final class StringContent implements AbstractDocument.Content, Serializable {
  55 
  56     /**
  57      * Creates a new StringContent object.  Initial size defaults to 10.
  58      */
  59     public StringContent() {
  60         this(10);
  61     }
  62 
  63     /**
  64      * Creates a new StringContent object, with the initial
  65      * size specified.  If the length is &lt; 1, a size of 1 is used.
  66      *
  67      * @param initialLength the initial size
  68      */
  69     public StringContent(int initialLength) {
  70         if (initialLength < 1) {
  71             initialLength = 1;
  72         }
  73         data = new char[initialLength];
  74         data[0] = '\n';
  75         count = 1;
  76     }
  77 
  78     /**
  79      * Returns the length of the content.
  80      *
  81      * @return the length &gt;= 1
  82      * @see AbstractDocument.Content#length
  83      */
  84     public int length() {
  85         return count;
  86     }
  87 
  88     /**
  89      * Inserts a string into the content.
  90      *
  91      * @param where the starting position &gt;= 0 &amp;&amp; &lt; length()
  92      * @param str the non-null string to insert
  93      * @return an UndoableEdit object for undoing
  94      * @exception BadLocationException if the specified position is invalid
  95      * @see AbstractDocument.Content#insertString
  96      */
  97     public UndoableEdit insertString(int where, String str) throws BadLocationException {
  98         if (where >= count || where < 0) {
  99             throw new BadLocationException("Invalid location", count);
 100         }
 101         char[] chars = str.toCharArray();
 102         replace(where, 0, chars, 0, chars.length);
 103         if (marks != null) {
 104             updateMarksForInsert(where, str.length());
 105         }
 106         return new InsertUndo(where, str.length());
 107     }
 108 
 109     /**
 110      * Removes part of the content.  where + nitems must be &lt; length().
 111      *
 112      * @param where the starting position &gt;= 0
 113      * @param nitems the number of characters to remove &gt;= 0
 114      * @return an UndoableEdit object for undoing
 115      * @exception BadLocationException if the specified position is invalid
 116      * @see AbstractDocument.Content#remove
 117      */
 118     public UndoableEdit remove(int where, int nitems) throws BadLocationException {
 119         if (where + nitems >= count) {
 120             throw new BadLocationException("Invalid range", count);
 121         }
 122         String removedString = getString(where, nitems);
 123         UndoableEdit edit = new RemoveUndo(where, removedString);
 124         replace(where, nitems, empty, 0, 0);
 125         if (marks != null) {
 126             updateMarksForRemove(where, nitems);
 127         }
 128         return edit;
 129 
 130     }
 131 
 132     /**
 133      * Retrieves a portion of the content.  where + len must be &lt;= length().
 134      *
 135      * @param where the starting position &gt;= 0
 136      * @param len the length to retrieve &gt;= 0
 137      * @return a string representing the content; may be empty
 138      * @exception BadLocationException if the specified position is invalid
 139      * @see AbstractDocument.Content#getString
 140      */
 141     public String getString(int where, int len) throws BadLocationException {
 142         if (where + len > count) {
 143             throw new BadLocationException("Invalid range", count);
 144         }
 145         return new String(data, where, len);
 146     }
 147 
 148     /**
 149      * Retrieves a portion of the content.  where + len must be &lt;= length()
 150      *
 151      * @param where the starting position &gt;= 0
 152      * @param len the number of characters to retrieve &gt;= 0
 153      * @param chars the Segment object to return the characters in
 154      * @exception BadLocationException if the specified position is invalid
 155      * @see AbstractDocument.Content#getChars
 156      */
 157     public void getChars(int where, int len, Segment chars) throws BadLocationException {
 158         if (where + len > count) {
 159             throw new BadLocationException("Invalid location", count);
 160         }
 161         chars.array = data;
 162         chars.offset = where;
 163         chars.count = len;
 164     }
 165 
 166     /**
 167      * Creates a position within the content that will
 168      * track change as the content is mutated.
 169      *
 170      * @param offset the offset to create a position for &gt;= 0
 171      * @return the position
 172      * @exception BadLocationException if the specified position is invalid
 173      */
 174     public Position createPosition(int offset) throws BadLocationException {
 175         // some small documents won't have any sticky positions
 176         // at all, so the buffer is created lazily.
 177         if (marks == null) {
 178             marks = new Vector<PosRec>();
 179         }
 180         return new StickyPosition(offset);
 181     }
 182 
 183     // --- local methods ---------------------------------------
 184 
 185     /**
 186      * Replaces some of the characters in the array
 187      * @param offset  offset into the array to start the replace
 188      * @param length  number of characters to remove
 189      * @param replArray replacement array
 190      * @param replOffset offset into the replacement array
 191      * @param replLength number of character to use from the
 192      *   replacement array.
 193      */
 194     void replace(int offset, int length,
 195                  char[] replArray, int replOffset, int replLength) {
 196         int delta = replLength - length;
 197         int src = offset + length;
 198         int nmove = count - src;
 199         int dest = src + delta;
 200         if ((count + delta) >= data.length) {
 201             // need to grow the array
 202             int newLength = Math.max(2*data.length, count + delta);
 203             char[] newData = new char[newLength];
 204             System.arraycopy(data, 0, newData, 0, offset);
 205             System.arraycopy(replArray, replOffset, newData, offset, replLength);
 206             System.arraycopy(data, src, newData, dest, nmove);
 207             data = newData;
 208         } else {
 209             // patch the existing array
 210             System.arraycopy(data, src, data, dest, nmove);
 211             System.arraycopy(replArray, replOffset, data, offset, replLength);
 212         }
 213         count = count + delta;
 214     }
 215 
 216     void resize(int ncount) {
 217         char[] ndata = new char[ncount];
 218         System.arraycopy(data, 0, ndata, 0, Math.min(ncount, count));
 219         data = ndata;
 220     }
 221 
 222     synchronized void updateMarksForInsert(int offset, int length) {
 223         if (offset == 0) {
 224             // zero is a special case where we update only
 225             // marks after it.
 226             offset = 1;
 227         }
 228         int n = marks.size();
 229         for (int i = 0; i < n; i++) {
 230             PosRec mark = marks.elementAt(i);
 231             if (mark.unused) {
 232                 // this record is no longer used, get rid of it
 233                 marks.removeElementAt(i);
 234                 i -= 1;
 235                 n -= 1;
 236             } else if (mark.offset >= offset) {
 237                 mark.offset += length;
 238             }
 239         }
 240     }
 241 
 242     synchronized void updateMarksForRemove(int offset, int length) {
 243         int n = marks.size();
 244         for (int i = 0; i < n; i++) {
 245             PosRec mark = marks.elementAt(i);
 246             if (mark.unused) {
 247                 // this record is no longer used, get rid of it
 248                 marks.removeElementAt(i);
 249                 i -= 1;
 250                 n -= 1;
 251             } else if (mark.offset >= (offset + length)) {
 252                 mark.offset -= length;
 253             } else if (mark.offset >= offset) {
 254                 mark.offset = offset;
 255             }
 256         }
 257     }
 258 
 259     /**
 260      * Returns a Vector containing instances of UndoPosRef for the
 261      * Positions in the range
 262      * <code>offset</code> to <code>offset</code> + <code>length</code>.
 263      * If <code>v</code> is not null the matching Positions are placed in
 264      * there. The vector with the resulting Positions are returned.
 265      * <p>
 266      * This is meant for internal usage, and is generally not of interest
 267      * to subclasses.
 268      *
 269      * @param v the Vector to use, with a new one created on null
 270      * @param offset the starting offset &gt;= 0
 271      * @param length the length &gt;= 0
 272      * @return the set of instances
 273      */
 274     @SuppressWarnings({"rawtypes", "unchecked"}) // UndoPosRef type cannot be exposed
 275     protected Vector getPositionsInRange(Vector v, int offset,
 276                                          int length) {
 277         int n = marks.size();
 278         int end = offset + length;
 279         Vector placeIn = (v == null) ? new Vector() : v;
 280         for (int i = 0; i < n; i++) {
 281             PosRec mark = marks.elementAt(i);
 282             if (mark.unused) {
 283                 // this record is no longer used, get rid of it
 284                 marks.removeElementAt(i);
 285                 i -= 1;
 286                 n -= 1;
 287             } else if(mark.offset >= offset && mark.offset <= end)
 288                 placeIn.addElement(new UndoPosRef(mark));
 289         }
 290         return placeIn;
 291     }
 292 
 293     /**
 294      * Resets the location for all the UndoPosRef instances
 295      * in <code>positions</code>.
 296      * <p>
 297      * This is meant for internal usage, and is generally not of interest
 298      * to subclasses.
 299      *
 300      * @param positions the positions of the instances
 301      */
 302     @SuppressWarnings("rawtypes") // UndoPosRef type cannot be exposed
 303     protected void updateUndoPositions(Vector positions) {
 304         for(int counter = positions.size() - 1; counter >= 0; counter--) {
 305             UndoPosRef ref = (UndoPosRef) positions.elementAt(counter);
 306             // Check if the Position is still valid.
 307             if(ref.rec.unused) {
 308                 positions.removeElementAt(counter);
 309             }
 310             else
 311                 ref.resetLocation();
 312         }
 313     }
 314 
 315     private static final char[] empty = new char[0];
 316     private char[] data;
 317     private int count;
 318     transient Vector<PosRec> marks;
 319 
 320     /**
 321      * holds the data for a mark... separately from
 322      * the real mark so that the real mark can be
 323      * collected if there are no more references to
 324      * it.... the update table holds only a reference
 325      * to this grungy thing.
 326      */
 327     final class PosRec {
 328 
 329         PosRec(int offset) {
 330             this.offset = offset;
 331         }
 332 
 333         int offset;
 334         boolean unused;
 335     }
 336 
 337     /**
 338      * This really wants to be a weak reference but
 339      * in 1.1 we don't have a 100% pure solution for
 340      * this... so this class trys to hack a solution
 341      * to causing the marks to be collected.
 342      */
 343     final class StickyPosition implements Position {
 344 
 345         StickyPosition(int offset) {
 346             rec = new PosRec(offset);
 347             marks.addElement(rec);
 348         }
 349 
 350         public int getOffset() {
 351             return rec.offset;
 352         }
 353 
 354         @SuppressWarnings("deprecation")
 355         protected void finalize() throws Throwable {
 356             // schedule the record to be removed later
 357             // on another thread.
 358             rec.unused = true;
 359         }
 360 
 361         public String toString() {
 362             return Integer.toString(getOffset());
 363         }
 364 
 365         PosRec rec;
 366     }
 367 
 368     /**
 369      * Used to hold a reference to a Position that is being reset as the
 370      * result of removing from the content.
 371      */
 372     final class UndoPosRef {
 373         UndoPosRef(PosRec rec) {
 374             this.rec = rec;
 375             this.undoLocation = rec.offset;
 376         }
 377 
 378         /**
 379          * Resets the location of the Position to the offset when the
 380          * receiver was instantiated.
 381          */
 382         protected void resetLocation() {
 383             rec.offset = undoLocation;
 384         }
 385 
 386         /** Location to reset to when resetLocatino is invoked. */
 387         protected int undoLocation;
 388         /** Position to reset offset. */
 389         protected PosRec rec;
 390     }
 391 
 392     /**
 393      * UnoableEdit created for inserts.
 394      */
 395     class InsertUndo extends AbstractUndoableEdit {
 396         protected InsertUndo(int offset, int length) {
 397             super();
 398             this.offset = offset;
 399             this.length = length;
 400         }
 401 
 402         public void undo() throws CannotUndoException {
 403             super.undo();
 404             try {
 405                 synchronized(StringContent.this) {
 406                     // Get the Positions in the range being removed.
 407                     if(marks != null)
 408                         posRefs = getPositionsInRange(null, offset, length);
 409                     string = getString(offset, length);
 410                     remove(offset, length);
 411                 }
 412             } catch (BadLocationException bl) {
 413               throw new CannotUndoException();
 414             }
 415         }
 416 
 417         public void redo() throws CannotRedoException {
 418             super.redo();
 419             try {
 420                 synchronized(StringContent.this) {
 421                     insertString(offset, string);
 422                     string = null;
 423                     // Update the Positions that were in the range removed.
 424                     if(posRefs != null) {
 425                         updateUndoPositions(posRefs);
 426                         posRefs = null;
 427                     }
 428               }
 429             } catch (BadLocationException bl) {
 430               throw new CannotRedoException();
 431             }
 432         }
 433 
 434         // Where the string goes.
 435         protected int offset;
 436         // Length of the string.
 437         protected int length;
 438         // The string that was inserted. To cut down on space needed this
 439         // will only be valid after an undo.
 440         protected String string;
 441         // An array of instances of UndoPosRef for the Positions in the
 442         // range that was removed, valid after undo.
 443         @SuppressWarnings("rawtypes") // UndoPosRef type cannot be exposed
 444         protected Vector posRefs;
 445     }
 446 
 447 
 448     /**
 449      * UndoableEdit created for removes.
 450      */
 451     class RemoveUndo extends AbstractUndoableEdit {
 452         @SuppressWarnings("unchecked")
 453         protected RemoveUndo(int offset, String string) {
 454             super();
 455             this.offset = offset;
 456             this.string = string;
 457             this.length = string.length();
 458             if(marks != null)
 459                 posRefs = getPositionsInRange(null, offset, length);
 460         }
 461 
 462         public void undo() throws CannotUndoException {
 463             super.undo();
 464             try {
 465                 synchronized(StringContent.this) {
 466                     insertString(offset, string);
 467                     // Update the Positions that were in the range removed.
 468                     if(posRefs != null) {
 469                         updateUndoPositions(posRefs);
 470                         posRefs = null;
 471                     }
 472                     string = null;
 473                 }
 474             } catch (BadLocationException bl) {
 475               throw new CannotUndoException();
 476             }
 477         }
 478 
 479         @SuppressWarnings("unchecked")
 480         public void redo() throws CannotRedoException {
 481             super.redo();
 482             try {
 483                 synchronized(StringContent.this) {
 484                     string = getString(offset, length);
 485                     // Get the Positions in the range being removed.
 486                     if(marks != null)
 487                         posRefs = getPositionsInRange(null, offset, length);
 488                     remove(offset, length);
 489                 }
 490             } catch (BadLocationException bl) {
 491               throw new CannotRedoException();
 492             }
 493         }
 494 
 495         // Where the string goes.
 496         protected int offset;
 497         // Length of the string.
 498         protected int length;
 499         // The string that was inserted. This will be null after an undo.
 500         protected String string;
 501         // An array of instances of UndoPosRef for the Positions in the
 502         // range that was removed, valid before undo.
 503         protected Vector<UndoPosRef> posRefs;
 504     }
 505 }