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
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      */
StringContent()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      */
StringContent(int initialLength)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      */
length()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      */
insertString(int where, String str)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      */
remove(int where, int nitems)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      */
getString(int where, int len)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      */
getChars(int where, int len, Segment chars)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      */
createPosition(int offset)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      */
replace(int offset, int length, char[] replArray, int replOffset, int replLength)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 
resize(int ncount)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 
updateMarksForInsert(int offset, int length)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 
updateMarksForRemove(int offset, int length)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
getPositionsInRange(Vector v, int offset, int length)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
updateUndoPositions(Vector positions)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 
PosRec(int offset)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 
StickyPosition(int offset)345         StickyPosition(int offset) {
346             rec = new PosRec(offset);
347             marks.addElement(rec);
348         }
349 
getOffset()350         public int getOffset() {
351             return rec.offset;
352         }
353 
354         @SuppressWarnings("deprecation")
finalize()355         protected void finalize() throws Throwable {
356             // schedule the record to be removed later
357             // on another thread.
358             rec.unused = true;
359         }
360 
toString()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 {
UndoPosRef(PosRec rec)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          */
resetLocation()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 {
InsertUndo(int offset, int length)396         protected InsertUndo(int offset, int length) {
397             super();
398             this.offset = offset;
399             this.length = length;
400         }
401 
undo()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 
redo()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")
RemoveUndo(int offset, String string)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 
undo()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")
redo()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 }
506