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