1 /*
   2  * Copyright (c) 1997, 2010, 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 
  29 /**
  30  * A plain document that maintains no character attributes.  The
  31  * default element structure for this document is a map of the lines in
  32  * the text.  The Element returned by getDefaultRootElement is
  33  * a map of the lines, and each child element represents a line.
  34  * This model does not maintain any character level attributes,
  35  * but each line can be tagged with an arbitrary set of attributes.
  36  * Line to offset, and offset to line translations can be quickly
  37  * performed using the default root element.  The structure information
  38  * of the DocumentEvent's fired by edits will indicate the line
  39  * structure changes.
  40  * <p>
  41  * The default content storage management is performed by a
  42  * gapped buffer implementation (GapContent).  It supports
  43  * editing reasonably large documents with good efficiency when
  44  * the edits are contiguous or clustered, as is typical.
  45  * <p>
  46  * <strong>Warning:</strong>
  47  * Serialized objects of this class will not be compatible with
  48  * future Swing releases. The current serialization support is
  49  * appropriate for short term storage or RMI between applications running
  50  * the same version of Swing.  As of 1.4, support for long term storage
  51  * of all JavaBeans<sup><font size="-2">TM</font></sup>
  52  * has been added to the <code>java.beans</code> package.
  53  * Please see {@link java.beans.XMLEncoder}.
  54  *
  55  * @author  Timothy Prinzing
  56  * @see     Document
  57  * @see     AbstractDocument
  58  */
  59 public class PlainDocument extends AbstractDocument {
  60 
  61     /**
  62      * Name of the attribute that specifies the tab
  63      * size for tabs contained in the content.  The
  64      * type for the value is Integer.
  65      */
  66     public static final String tabSizeAttribute = "tabSize";
  67 
  68     /**
  69      * Name of the attribute that specifies the maximum
  70      * length of a line, if there is a maximum length.
  71      * The type for the value is Integer.
  72      */
  73     public static final String lineLimitAttribute = "lineLimit";
  74 
  75     /**
  76      * Constructs a plain text document.  A default model using
  77      * <code>GapContent</code> is constructed and set.
  78      */
  79     public PlainDocument() {
  80         this(new GapContent());
  81     }
  82 
  83     /**
  84      * Constructs a plain text document.  A default root element is created,
  85      * and the tab size set to 8.
  86      *
  87      * @param c  the container for the content
  88      */
  89     public PlainDocument(Content c) {
  90         super(c);
  91         putProperty(tabSizeAttribute, Integer.valueOf(8));
  92         defaultRoot = createDefaultRoot();
  93     }
  94 
  95     /**
  96      * Inserts some content into the document.
  97      * Inserting content causes a write lock to be held while the
  98      * actual changes are taking place, followed by notification
  99      * to the observers on the thread that grabbed the write lock.
 100      * <p>
 101      * This method is thread safe, although most Swing methods
 102      * are not. Please see
 103      * <A HREF="http://download.oracle.com/javase/tutorial/uiswing/concurrency/index.html">Concurrency
 104      * in Swing</A> for more information.
 105      *
 106      * @param offs the starting offset &gt;= 0
 107      * @param str the string to insert; does nothing with null/empty strings
 108      * @param a the attributes for the inserted content
 109      * @exception BadLocationException  the given insert position is not a valid
 110      *   position within the document
 111      * @see Document#insertString
 112      */
 113     public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
 114         // fields don't want to have multiple lines.  We may provide a field-specific
 115         // model in the future in which case the filtering logic here will no longer
 116         // be needed.
 117         Object filterNewlines = getProperty("filterNewlines");
 118         if ((filterNewlines instanceof Boolean) && filterNewlines.equals(Boolean.TRUE)) {
 119             if ((str != null) && (str.indexOf('\n') >= 0)) {
 120                 StringBuilder filtered = new StringBuilder(str);
 121                 int n = filtered.length();
 122                 for (int i = 0; i < n; i++) {
 123                     if (filtered.charAt(i) == '\n') {
 124                         filtered.setCharAt(i, ' ');
 125                     }
 126                 }
 127                 str = filtered.toString();
 128             }
 129         }
 130         super.insertString(offs, str, a);
 131     }
 132 
 133     /**
 134      * Gets the default root element for the document model.
 135      *
 136      * @return the root
 137      * @see Document#getDefaultRootElement
 138      */
 139     public Element getDefaultRootElement() {
 140         return defaultRoot;
 141     }
 142 
 143     /**
 144      * Creates the root element to be used to represent the
 145      * default document structure.
 146      *
 147      * @return the element base
 148      */
 149     protected AbstractElement createDefaultRoot() {
 150         BranchElement map = (BranchElement) createBranchElement(null, null);
 151         Element line = createLeafElement(map, null, 0, 1);
 152         Element[] lines = new Element[1];
 153         lines[0] = line;
 154         map.replace(0, 0, lines);
 155         return map;
 156     }
 157 
 158     /**
 159      * Get the paragraph element containing the given position.  Since this
 160      * document only models lines, it returns the line instead.
 161      */
 162     public Element getParagraphElement(int pos){
 163         Element lineMap = getDefaultRootElement();
 164         return lineMap.getElement( lineMap.getElementIndex( pos ) );
 165     }
 166 
 167     /**
 168      * Updates document structure as a result of text insertion.  This
 169      * will happen within a write lock.  Since this document simply
 170      * maps out lines, we refresh the line map.
 171      *
 172      * @param chng the change event describing the dit
 173      * @param attr the set of attributes for the inserted text
 174      */
 175     protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
 176         removed.removeAllElements();
 177         added.removeAllElements();
 178         BranchElement lineMap = (BranchElement) getDefaultRootElement();
 179         int offset = chng.getOffset();
 180         int length = chng.getLength();
 181         if (offset > 0) {
 182           offset -= 1;
 183           length += 1;
 184         }
 185         int index = lineMap.getElementIndex(offset);
 186         Element rmCandidate = lineMap.getElement(index);
 187         int rmOffs0 = rmCandidate.getStartOffset();
 188         int rmOffs1 = rmCandidate.getEndOffset();
 189         int lastOffset = rmOffs0;
 190         try {
 191             if (s == null) {
 192                 s = new Segment();
 193             }
 194             getContent().getChars(offset, length, s);
 195             boolean hasBreaks = false;
 196             for (int i = 0; i < length; i++) {
 197                 char c = s.array[s.offset + i];
 198                 if (c == '\n') {
 199                     int breakOffset = offset + i + 1;
 200                     added.addElement(createLeafElement(lineMap, null, lastOffset, breakOffset));
 201                     lastOffset = breakOffset;
 202                     hasBreaks = true;
 203                 }
 204             }
 205             if (hasBreaks) {
 206                 removed.addElement(rmCandidate);
 207                 if ((offset + length == rmOffs1) && (lastOffset != rmOffs1) &&
 208                     ((index+1) < lineMap.getElementCount())) {
 209                     Element e = lineMap.getElement(index+1);
 210                     removed.addElement(e);
 211                     rmOffs1 = e.getEndOffset();
 212                 }
 213                 if (lastOffset < rmOffs1) {
 214                     added.addElement(createLeafElement(lineMap, null, lastOffset, rmOffs1));
 215                 }
 216 
 217                 Element[] aelems = new Element[added.size()];
 218                 added.copyInto(aelems);
 219                 Element[] relems = new Element[removed.size()];
 220                 removed.copyInto(relems);
 221                 ElementEdit ee = new ElementEdit(lineMap, index, relems, aelems);
 222                 chng.addEdit(ee);
 223                 lineMap.replace(index, relems.length, aelems);
 224             }
 225             if (Utilities.isComposedTextAttributeDefined(attr)) {
 226                 insertComposedTextUpdate(chng, attr);
 227             }
 228         } catch (BadLocationException e) {
 229             throw new Error("Internal error: " + e.toString());
 230         }
 231         super.insertUpdate(chng, attr);
 232     }
 233 
 234     /**
 235      * Updates any document structure as a result of text removal.
 236      * This will happen within a write lock. Since the structure
 237      * represents a line map, this just checks to see if the
 238      * removal spans lines.  If it does, the two lines outside
 239      * of the removal area are joined together.
 240      *
 241      * @param chng the change event describing the edit
 242      */
 243     protected void removeUpdate(DefaultDocumentEvent chng) {
 244         removed.removeAllElements();
 245         BranchElement map = (BranchElement) getDefaultRootElement();
 246         int offset = chng.getOffset();
 247         int length = chng.getLength();
 248         int line0 = map.getElementIndex(offset);
 249         int line1 = map.getElementIndex(offset + length);
 250         if (line0 != line1) {
 251             // a line was removed
 252             for (int i = line0; i <= line1; i++) {
 253                 removed.addElement(map.getElement(i));
 254             }
 255             int p0 = map.getElement(line0).getStartOffset();
 256             int p1 = map.getElement(line1).getEndOffset();
 257             Element[] aelems = new Element[1];
 258             aelems[0] = createLeafElement(map, null, p0, p1);
 259             Element[] relems = new Element[removed.size()];
 260             removed.copyInto(relems);
 261             ElementEdit ee = new ElementEdit(map, line0, relems, aelems);
 262             chng.addEdit(ee);
 263             map.replace(line0, relems.length, aelems);
 264         } else {
 265             //Check for the composed text element
 266             Element line = map.getElement(line0);
 267             if (!line.isLeaf()) {
 268                 Element leaf = line.getElement(line.getElementIndex(offset));
 269                 if (Utilities.isComposedTextElement(leaf)) {
 270                     Element[] aelem = new Element[1];
 271                     aelem[0] = createLeafElement(map, null,
 272                         line.getStartOffset(), line.getEndOffset());
 273                     Element[] relem = new Element[1];
 274                     relem[0] = line;
 275                     ElementEdit ee = new ElementEdit(map, line0, relem, aelem);
 276                     chng.addEdit(ee);
 277                     map.replace(line0, 1, aelem);
 278                 }
 279             }
 280         }
 281         super.removeUpdate(chng);
 282     }
 283 
 284     //
 285     // Inserts the composed text of an input method. The line element
 286     // where the composed text is inserted into becomes an branch element
 287     // which contains leaf elements of the composed text and the text
 288     // backing store.
 289     //
 290     private void insertComposedTextUpdate(DefaultDocumentEvent chng, AttributeSet attr) {
 291         added.removeAllElements();
 292         BranchElement lineMap = (BranchElement) getDefaultRootElement();
 293         int offset = chng.getOffset();
 294         int length = chng.getLength();
 295         int index = lineMap.getElementIndex(offset);
 296         Element elem = lineMap.getElement(index);
 297         int elemStart = elem.getStartOffset();
 298         int elemEnd = elem.getEndOffset();
 299         BranchElement[] abelem = new BranchElement[1];
 300         abelem[0] = (BranchElement) createBranchElement(lineMap, null);
 301         Element[] relem = new Element[1];
 302         relem[0] = elem;
 303         if (elemStart != offset)
 304             added.addElement(createLeafElement(abelem[0], null, elemStart, offset));
 305         added.addElement(createLeafElement(abelem[0], attr, offset, offset+length));
 306         if (elemEnd != offset+length)
 307             added.addElement(createLeafElement(abelem[0], null, offset+length, elemEnd));
 308         Element[] alelem = new Element[added.size()];
 309         added.copyInto(alelem);
 310         ElementEdit ee = new ElementEdit(lineMap, index, relem, abelem);
 311         chng.addEdit(ee);
 312 
 313         abelem[0].replace(0, 0, alelem);
 314         lineMap.replace(index, 1, abelem);
 315     }
 316 
 317     private AbstractElement defaultRoot;
 318     private Vector<Element> added = new Vector<Element>();
 319     private Vector<Element> removed = new Vector<Element>();
 320     private transient Segment s;
 321 }