diff options
Diffstat (limited to 'libjava/classpath/javax/swing/text/AbstractDocument.java')
-rw-r--r-- | libjava/classpath/javax/swing/text/AbstractDocument.java | 2906 |
1 files changed, 2906 insertions, 0 deletions
diff --git a/libjava/classpath/javax/swing/text/AbstractDocument.java b/libjava/classpath/javax/swing/text/AbstractDocument.java new file mode 100644 index 000000000..25915bb5a --- /dev/null +++ b/libjava/classpath/javax/swing/text/AbstractDocument.java @@ -0,0 +1,2906 @@ +/* AbstractDocument.java -- + Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc. + +This file is part of GNU Classpath. + +GNU Classpath is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2, or (at your option) +any later version. + +GNU Classpath is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Classpath; see the file COPYING. If not, write to the +Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +02110-1301 USA. + +Linking this library statically or dynamically with other modules is +making a combined work based on this library. Thus, the terms and +conditions of the GNU General Public License cover the whole +combination. + +As a special exception, the copyright holders of this library give you +permission to link this library with independent modules to produce an +executable, regardless of the license terms of these independent +modules, and to copy and distribute the resulting executable under +terms of your choice, provided that you also meet, for each linked +independent module, the terms and conditions of the license of that +module. An independent module is a module which is not derived from +or based on this library. If you modify this library, you may extend +this exception to your version of the library, but you are not +obligated to do so. If you do not wish to do so, delete this +exception statement from your version. */ + + +package javax.swing.text; + +import gnu.java.lang.CPStringBuilder; + +import java.awt.font.TextAttribute; +import java.io.PrintStream; +import java.io.Serializable; +import java.text.Bidi; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Vector; + +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.EventListenerList; +import javax.swing.event.UndoableEditEvent; +import javax.swing.event.UndoableEditListener; +import javax.swing.text.DocumentFilter; +import javax.swing.tree.TreeNode; +import javax.swing.undo.AbstractUndoableEdit; +import javax.swing.undo.CompoundEdit; +import javax.swing.undo.UndoableEdit; + +/** + * An abstract base implementation for the {@link Document} interface. + * This class provides some common functionality for all <code>Element</code>s, + * most notably it implements a locking mechanism to make document modification + * thread-safe. + * + * @author original author unknown + * @author Roman Kennke (roman@kennke.org) + */ +public abstract class AbstractDocument implements Document, Serializable +{ + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = 6842927725919637215L; + + /** + * Standard error message to indicate a bad location. + */ + protected static final String BAD_LOCATION = "document location failure"; + + /** + * Standard name for unidirectional <code>Element</code>s. + */ + public static final String BidiElementName = "bidi level"; + + /** + * Standard name for content <code>Element</code>s. These are usually + * {@link LeafElement}s. + */ + public static final String ContentElementName = "content"; + + /** + * Standard name for paragraph <code>Element</code>s. These are usually + * {@link BranchElement}s. + */ + public static final String ParagraphElementName = "paragraph"; + + /** + * Standard name for section <code>Element</code>s. These are usually + * {@link DefaultStyledDocument.SectionElement}s. + */ + public static final String SectionElementName = "section"; + + /** + * Attribute key for storing the element name. + */ + public static final String ElementNameAttribute = "$ename"; + + /** + * Standard name for the bidi root element. + */ + private static final String BidiRootName = "bidi root"; + + /** + * Key for storing the asynchronous load priority. + */ + private static final String AsyncLoadPriority = "load priority"; + + /** + * Key for storing the I18N state. + */ + private static final String I18N = "i18n"; + + /** + * The actual content model of this <code>Document</code>. + */ + Content content; + + /** + * The AttributeContext for this <code>Document</code>. + */ + AttributeContext context; + + /** + * The currently installed <code>DocumentFilter</code>. + */ + DocumentFilter documentFilter; + + /** + * The documents properties. + */ + Dictionary properties; + + /** + * Manages event listeners for this <code>Document</code>. + */ + protected EventListenerList listenerList = new EventListenerList(); + + /** + * Stores the current writer thread. Used for locking. + */ + private Thread currentWriter = null; + + /** + * The number of readers. Used for locking. + */ + private int numReaders = 0; + + /** + * The number of current writers. If this is > 1 then the same thread entered + * the write lock more than once. + */ + private int numWriters = 0; + + /** An instance of a DocumentFilter.FilterBypass which allows calling + * the insert, remove and replace method without checking for an installed + * document filter. + */ + private DocumentFilter.FilterBypass bypass; + + /** + * The bidi root element. + */ + private BidiRootElement bidiRoot; + + /** + * True when we are currently notifying any listeners. This is used + * to detect illegal situations in writeLock(). + */ + private transient boolean notifyListeners; + + /** + * Creates a new <code>AbstractDocument</code> with the specified + * {@link Content} model. + * + * @param doc the <code>Content</code> model to be used in this + * <code>Document<code> + * + * @see GapContent + * @see StringContent + */ + protected AbstractDocument(Content doc) + { + this(doc, StyleContext.getDefaultStyleContext()); + } + + /** + * Creates a new <code>AbstractDocument</code> with the specified + * {@link Content} model and {@link AttributeContext}. + * + * @param doc the <code>Content</code> model to be used in this + * <code>Document<code> + * @param ctx the <code>AttributeContext</code> to use + * + * @see GapContent + * @see StringContent + */ + protected AbstractDocument(Content doc, AttributeContext ctx) + { + content = doc; + context = ctx; + + // FIXME: Fully implement bidi. + bidiRoot = new BidiRootElement(); + + // FIXME: This is determined using a Mauve test. Make the document + // actually use this. + putProperty(I18N, Boolean.FALSE); + + // Add one child to the bidi root. + writeLock(); + try + { + Element[] children = new Element[1]; + children[0] = new BidiElement(bidiRoot, 0, 1, 0); + bidiRoot.replace(0, 0, children); + } + finally + { + writeUnlock(); + } + } + + /** Returns the DocumentFilter.FilterBypass instance for this + * document and create it if it does not exist yet. + * + * @return This document's DocumentFilter.FilterBypass instance. + */ + private DocumentFilter.FilterBypass getBypass() + { + if (bypass == null) + bypass = new Bypass(); + + return bypass; + } + + /** + * Returns the paragraph {@link Element} that holds the specified position. + * + * @param pos the position for which to get the paragraph element + * + * @return the paragraph {@link Element} that holds the specified position + */ + public abstract Element getParagraphElement(int pos); + + /** + * Returns the default root {@link Element} of this <code>Document</code>. + * Usual <code>Document</code>s only have one root element and return this. + * However, there may be <code>Document</code> implementations that + * support multiple root elements, they have to return a default root element + * here. + * + * @return the default root {@link Element} of this <code>Document</code> + */ + public abstract Element getDefaultRootElement(); + + /** + * Creates and returns a branch element with the specified + * <code>parent</code> and <code>attributes</code>. Note that the new + * <code>Element</code> is linked to the parent <code>Element</code> + * through {@link Element#getParentElement}, but it is not yet added + * to the parent <code>Element</code> as child. + * + * @param parent the parent <code>Element</code> for the new branch element + * @param attributes the text attributes to be installed in the new element + * + * @return the new branch <code>Element</code> + * + * @see BranchElement + */ + protected Element createBranchElement(Element parent, + AttributeSet attributes) + { + return new BranchElement(parent, attributes); + } + + /** + * Creates and returns a leaf element with the specified + * <code>parent</code> and <code>attributes</code>. Note that the new + * <code>Element</code> is linked to the parent <code>Element</code> + * through {@link Element#getParentElement}, but it is not yet added + * to the parent <code>Element</code> as child. + * + * @param parent the parent <code>Element</code> for the new branch element + * @param attributes the text attributes to be installed in the new element + * + * @return the new branch <code>Element</code> + * + * @see LeafElement + */ + protected Element createLeafElement(Element parent, AttributeSet attributes, + int start, int end) + { + return new LeafElement(parent, attributes, start, end); + } + + /** + * Creates a {@link Position} that keeps track of the location at the + * specified <code>offset</code>. + * + * @param offset the location in the document to keep track by the new + * <code>Position</code> + * + * @return the newly created <code>Position</code> + * + * @throws BadLocationException if <code>offset</code> is not a valid + * location in the documents content model + */ + public synchronized Position createPosition(final int offset) + throws BadLocationException + { + return content.createPosition(offset); + } + + /** + * Notifies all registered listeners when the document model changes. + * + * @param event the <code>DocumentEvent</code> to be fired + */ + protected void fireChangedUpdate(DocumentEvent event) + { + notifyListeners = true; + try + { + DocumentListener[] listeners = getDocumentListeners(); + for (int index = 0; index < listeners.length; ++index) + listeners[index].changedUpdate(event); + } + finally + { + notifyListeners = false; + } + } + + /** + * Notifies all registered listeners when content is inserted in the document + * model. + * + * @param event the <code>DocumentEvent</code> to be fired + */ + protected void fireInsertUpdate(DocumentEvent event) + { + notifyListeners = true; + try + { + DocumentListener[] listeners = getDocumentListeners(); + for (int index = 0; index < listeners.length; ++index) + listeners[index].insertUpdate(event); + } + finally + { + notifyListeners = false; + } + } + + /** + * Notifies all registered listeners when content is removed from the + * document model. + * + * @param event the <code>DocumentEvent</code> to be fired + */ + protected void fireRemoveUpdate(DocumentEvent event) + { + notifyListeners = true; + try + { + DocumentListener[] listeners = getDocumentListeners(); + for (int index = 0; index < listeners.length; ++index) + listeners[index].removeUpdate(event); + } + finally + { + notifyListeners = false; + } + } + + /** + * Notifies all registered listeners when an <code>UndoableEdit</code> has + * been performed on this <code>Document</code>. + * + * @param event the <code>UndoableEditEvent</code> to be fired + */ + protected void fireUndoableEditUpdate(UndoableEditEvent event) + { + UndoableEditListener[] listeners = getUndoableEditListeners(); + + for (int index = 0; index < listeners.length; ++index) + listeners[index].undoableEditHappened(event); + } + + /** + * Returns the asynchronous loading priority. Returns <code>-1</code> if this + * document should not be loaded asynchronously. + * + * @return the asynchronous loading priority + */ + public int getAsynchronousLoadPriority() + { + Object val = getProperty(AsyncLoadPriority); + int prio = -1; + if (val != null) + prio = ((Integer) val).intValue(); + return prio; + } + + /** + * Returns the {@link AttributeContext} used in this <code>Document</code>. + * + * @return the {@link AttributeContext} used in this <code>Document</code> + */ + protected final AttributeContext getAttributeContext() + { + return context; + } + + /** + * Returns the root element for bidirectional content. + * + * @return the root element for bidirectional content + */ + public Element getBidiRootElement() + { + return bidiRoot; + } + + /** + * Returns the {@link Content} model for this <code>Document</code> + * + * @return the {@link Content} model for this <code>Document</code> + * + * @see GapContent + * @see StringContent + */ + protected final Content getContent() + { + return content; + } + + /** + * Returns the thread that currently modifies this <code>Document</code> + * if there is one, otherwise <code>null</code>. This can be used to + * distinguish between a method call that is part of an ongoing modification + * or if it is a separate modification for which a new lock must be aquired. + * + * @return the thread that currently modifies this <code>Document</code> + * if there is one, otherwise <code>null</code> + */ + protected final synchronized Thread getCurrentWriter() + { + return currentWriter; + } + + /** + * Returns the properties of this <code>Document</code>. + * + * @return the properties of this <code>Document</code> + */ + public Dictionary<Object, Object> getDocumentProperties() + { + // FIXME: make me thread-safe + if (properties == null) + properties = new Hashtable(); + + return properties; + } + + /** + * Returns a {@link Position} which will always mark the end of the + * <code>Document</code>. + * + * @return a {@link Position} which will always mark the end of the + * <code>Document</code> + */ + public final Position getEndPosition() + { + Position p; + try + { + p = createPosition(content.length()); + } + catch (BadLocationException ex) + { + // Shouldn't really happen. + p = null; + } + return p; + } + + /** + * Returns the length of this <code>Document</code>'s content. + * + * @return the length of this <code>Document</code>'s content + */ + public int getLength() + { + // We return Content.getLength() -1 here because there is always an + // implicit \n at the end of the Content which does count in Content + // but not in Document. + return content.length() - 1; + } + + /** + * Returns all registered listeners of a given listener type. + * + * @param listenerType the type of the listeners to be queried + * + * @return all registered listeners of the specified type + */ + public <T extends EventListener> T[] getListeners(Class<T> listenerType) + { + return listenerList.getListeners(listenerType); + } + + /** + * Returns a property from this <code>Document</code>'s property list. + * + * @param key the key of the property to be fetched + * + * @return the property for <code>key</code> or <code>null</code> if there + * is no such property stored + */ + public final Object getProperty(Object key) + { + // FIXME: make me thread-safe + Object value = null; + if (properties != null) + value = properties.get(key); + + return value; + } + + /** + * Returns all root elements of this <code>Document</code>. By default + * this just returns the single root element returned by + * {@link #getDefaultRootElement()}. <code>Document</code> implementations + * that support multiple roots must override this method and return all roots + * here. + * + * @return all root elements of this <code>Document</code> + */ + public Element[] getRootElements() + { + Element[] elements = new Element[2]; + elements[0] = getDefaultRootElement(); + elements[1] = getBidiRootElement(); + return elements; + } + + /** + * Returns a {@link Position} which will always mark the beginning of the + * <code>Document</code>. + * + * @return a {@link Position} which will always mark the beginning of the + * <code>Document</code> + */ + public final Position getStartPosition() + { + Position p; + try + { + p = createPosition(0); + } + catch (BadLocationException ex) + { + // Shouldn't really happen. + p = null; + } + return p; + } + + /** + * Returns a piece of this <code>Document</code>'s content. + * + * @param offset the start offset of the content + * @param length the length of the content + * + * @return the piece of content specified by <code>offset</code> and + * <code>length</code> + * + * @throws BadLocationException if <code>offset</code> or <code>offset + + * length</code> are invalid locations with this + * <code>Document</code> + */ + public String getText(int offset, int length) throws BadLocationException + { + return content.getString(offset, length); + } + + /** + * Fetches a piece of this <code>Document</code>'s content and stores + * it in the given {@link Segment}. + * + * @param offset the start offset of the content + * @param length the length of the content + * @param segment the <code>Segment</code> to store the content in + * + * @throws BadLocationException if <code>offset</code> or <code>offset + + * length</code> are invalid locations with this + * <code>Document</code> + */ + public void getText(int offset, int length, Segment segment) + throws BadLocationException + { + content.getChars(offset, length, segment); + } + + /** + * Inserts a String into this <code>Document</code> at the specified + * position and assigning the specified attributes to it. + * + * <p>If a {@link DocumentFilter} is installed in this document, the + * corresponding method of the filter object is called.</p> + * + * <p>The method has no effect when <code>text</code> is <code>null</code> + * or has a length of zero.</p> + * + * + * @param offset the location at which the string should be inserted + * @param text the content to be inserted + * @param attributes the text attributes to be assigned to that string + * + * @throws BadLocationException if <code>offset</code> is not a valid + * location in this <code>Document</code> + */ + public void insertString(int offset, String text, AttributeSet attributes) + throws BadLocationException + { + // Bail out if we have a bogus insertion (Behavior observed in RI). + if (text == null || text.length() == 0) + return; + + writeLock(); + try + { + if (documentFilter == null) + insertStringImpl(offset, text, attributes); + else + documentFilter.insertString(getBypass(), offset, text, attributes); + } + finally + { + writeUnlock(); + } + } + + void insertStringImpl(int offset, String text, AttributeSet attributes) + throws BadLocationException + { + // Just return when no text to insert was given. + if (text == null || text.length() == 0) + return; + DefaultDocumentEvent event = + new DefaultDocumentEvent(offset, text.length(), + DocumentEvent.EventType.INSERT); + + UndoableEdit undo = content.insertString(offset, text); + if (undo != null) + event.addEdit(undo); + + // Check if we need bidi layout. + if (getProperty(I18N).equals(Boolean.FALSE)) + { + Object dir = getProperty(TextAttribute.RUN_DIRECTION); + if (TextAttribute.RUN_DIRECTION_RTL.equals(dir)) + putProperty(I18N, Boolean.TRUE); + else + { + char[] chars = text.toCharArray(); + if (Bidi.requiresBidi(chars, 0, chars.length)) + putProperty(I18N, Boolean.TRUE); + } + } + + insertUpdate(event, attributes); + + fireInsertUpdate(event); + + if (undo != null) + fireUndoableEditUpdate(new UndoableEditEvent(this, undo)); + } + + /** + * Called to indicate that text has been inserted into this + * <code>Document</code>. The default implementation does nothing. + * This method is executed within a write lock. + * + * @param chng the <code>DefaultDocumentEvent</code> describing the change + * @param attr the attributes of the changed content + */ + protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) + { + if (Boolean.TRUE.equals(getProperty(I18N))) + updateBidi(chng); + } + + /** + * Called after some content has been removed from this + * <code>Document</code>. The default implementation does nothing. + * This method is executed within a write lock. + * + * @param chng the <code>DefaultDocumentEvent</code> describing the change + */ + protected void postRemoveUpdate(DefaultDocumentEvent chng) + { + if (Boolean.TRUE.equals(getProperty(I18N))) + updateBidi(chng); + } + + /** + * Stores a property in this <code>Document</code>'s property list. + * + * @param key the key of the property to be stored + * @param value the value of the property to be stored + */ + public final void putProperty(Object key, Object value) + { + // FIXME: make me thread-safe + if (properties == null) + properties = new Hashtable(); + + if (value == null) + properties.remove(key); + else + properties.put(key, value); + + // Update bidi structure if the RUN_DIRECTION is set. + if (TextAttribute.RUN_DIRECTION.equals(key)) + { + if (TextAttribute.RUN_DIRECTION_RTL.equals(value) + && Boolean.FALSE.equals(getProperty(I18N))) + putProperty(I18N, Boolean.TRUE); + + if (Boolean.TRUE.equals(getProperty(I18N))) + { + writeLock(); + try + { + DefaultDocumentEvent ev = + new DefaultDocumentEvent(0, getLength(), + DocumentEvent.EventType.INSERT); + updateBidi(ev); + } + finally + { + writeUnlock(); + } + } + } + } + + /** + * Updates the bidi element structure. + * + * @param ev the document event for the change + */ + private void updateBidi(DefaultDocumentEvent ev) + { + // Determine start and end offset of the paragraphs to be scanned. + int start = 0; + int end = 0; + DocumentEvent.EventType type = ev.getType(); + if (type == DocumentEvent.EventType.INSERT + || type == DocumentEvent.EventType.CHANGE) + { + int offs = ev.getOffset(); + int endOffs = offs + ev.getLength(); + start = getParagraphElement(offs).getStartOffset(); + end = getParagraphElement(endOffs).getEndOffset(); + } + else if (type == DocumentEvent.EventType.REMOVE) + { + Element par = getParagraphElement(ev.getOffset()); + start = par.getStartOffset(); + end = par.getEndOffset(); + } + else + assert false : "Unknown event type"; + + // Determine the bidi levels for the affected range. + Bidi[] bidis = getBidis(start, end); + + int removeFrom = 0; + int removeTo = 0; + + int offs = 0; + int lastRunStart = 0; + int lastRunEnd = 0; + int lastRunLevel = 0; + ArrayList newEls = new ArrayList(); + for (int i = 0; i < bidis.length; i++) + { + Bidi bidi = bidis[i]; + int numRuns = bidi.getRunCount(); + for (int r = 0; r < numRuns; r++) + { + if (r == 0 && i == 0) + { + if (start > 0) + { + // Try to merge with the previous element if it has the + // same bidi level as the first run. + int prevElIndex = bidiRoot.getElementIndex(start - 1); + removeFrom = prevElIndex; + Element prevEl = bidiRoot.getElement(prevElIndex); + AttributeSet atts = prevEl.getAttributes(); + int prevElLevel = StyleConstants.getBidiLevel(atts); + if (prevElLevel == bidi.getRunLevel(r)) + { + // Merge previous element with current run. + lastRunStart = prevEl.getStartOffset() - start; + lastRunEnd = bidi.getRunLimit(r); + lastRunLevel = bidi.getRunLevel(r); + } + else if (prevEl.getEndOffset() > start) + { + // Split previous element and replace by 2 new elements. + lastRunStart = 0; + lastRunEnd = bidi.getRunLimit(r); + lastRunLevel = bidi.getRunLevel(r); + newEls.add(new BidiElement(bidiRoot, + prevEl.getStartOffset(), + start, prevElLevel)); + } + else + { + // Simply start new run at start location. + lastRunStart = 0; + lastRunEnd = bidi.getRunLimit(r); + lastRunLevel = bidi.getRunLevel(r); + removeFrom++; + } + } + else + { + // Simply start new run at start location. + lastRunStart = 0; + lastRunEnd = bidi.getRunLimit(r); + lastRunLevel = bidi.getRunLevel(r); + removeFrom = 0; + } + } + if (i == bidis.length - 1 && r == numRuns - 1) + { + if (end <= getLength()) + { + // Try to merge last element with next element. + int nextIndex = bidiRoot.getElementIndex(end); + Element nextEl = bidiRoot.getElement(nextIndex); + AttributeSet atts = nextEl.getAttributes(); + int nextLevel = StyleConstants.getBidiLevel(atts); + int level = bidi.getRunLevel(r); + if (lastRunLevel == level && level == nextLevel) + { + // Merge runs together. + if (lastRunStart + start == nextEl.getStartOffset()) + removeTo = nextIndex - 1; + else + { + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + nextEl.getEndOffset(), level)); + removeTo = nextIndex; + } + } + else if (lastRunLevel == level) + { + // Merge current and last run. + int endOffs = offs + bidi.getRunLimit(r); + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + start + endOffs, level)); + if (start + endOffs == nextEl.getStartOffset()) + removeTo = nextIndex - 1; + else + { + newEls.add(new BidiElement(bidiRoot, start + endOffs, + nextEl.getEndOffset(), + nextLevel)); + removeTo = nextIndex; + } + } + else if (level == nextLevel) + { + // Merge current and next run. + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + start + lastRunEnd, + lastRunLevel)); + newEls.add(new BidiElement(bidiRoot, start + lastRunEnd, + nextEl.getEndOffset(), level)); + removeTo = nextIndex; + } + else + { + // Split next element. + int endOffs = offs + bidi.getRunLimit(r); + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + start + lastRunEnd, + lastRunLevel)); + newEls.add(new BidiElement(bidiRoot, start + lastRunEnd, + start + endOffs, level)); + newEls.add(new BidiElement(bidiRoot, start + endOffs, + nextEl.getEndOffset(), + nextLevel)); + removeTo = nextIndex; + } + } + else + { + removeTo = bidiRoot.getElementIndex(end); + int level = bidi.getRunLevel(r); + int runEnd = offs + bidi.getRunLimit(r); + + if (level == lastRunLevel) + { + // Merge with previous. + lastRunEnd = offs + runEnd; + newEls.add(new BidiElement(bidiRoot, + start + lastRunStart, + start + runEnd, level)); + } + else + { + // Create element for last run and current run. + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + start + lastRunEnd, + lastRunLevel)); + newEls.add(new BidiElement(bidiRoot, + start + lastRunEnd, + start + runEnd, + level)); + } + } + } + else + { + int level = bidi.getRunLevel(r); + int runEnd = bidi.getRunLimit(r); + + if (level == lastRunLevel) + { + // Merge with previous. + lastRunEnd = offs + runEnd; + } + else + { + // Create element for last run and update values for + // current run. + newEls.add(new BidiElement(bidiRoot, start + lastRunStart, + start + lastRunEnd, + lastRunLevel)); + lastRunStart = lastRunEnd; + lastRunEnd = offs + runEnd; + lastRunLevel = level; + } + } + } + offs += bidi.getLength(); + } + + // Determine the bidi elements which are to be removed. + int numRemoved = 0; + if (bidiRoot.getElementCount() > 0) + numRemoved = removeTo - removeFrom + 1; + Element[] removed = new Element[numRemoved]; + for (int i = 0; i < numRemoved; i++) + removed[i] = bidiRoot.getElement(removeFrom + i); + + Element[] added = new Element[newEls.size()]; + added = (Element[]) newEls.toArray(added); + + // Update the event. + ElementEdit edit = new ElementEdit(bidiRoot, removeFrom, removed, added); + ev.addEdit(edit); + + // Update the structure. + bidiRoot.replace(removeFrom, numRemoved, added); + } + + /** + * Determines the Bidi objects for the paragraphs in the specified range. + * + * @param start the start of the range + * @param end the end of the range + * + * @return the Bidi analysers for the paragraphs in the range + */ + private Bidi[] getBidis(int start, int end) + { + // Determine the default run direction from the document property. + Boolean defaultDir = null; + Object o = getProperty(TextAttribute.RUN_DIRECTION); + if (o instanceof Boolean) + defaultDir = (Boolean) o; + + // Scan paragraphs and add their level arrays to the overall levels array. + ArrayList bidis = new ArrayList(); + Segment s = new Segment(); + for (int i = start; i < end;) + { + Element par = getParagraphElement(i); + int pStart = par.getStartOffset(); + int pEnd = par.getEndOffset(); + + // Determine the default run direction of the paragraph. + Boolean dir = defaultDir; + o = par.getAttributes().getAttribute(TextAttribute.RUN_DIRECTION); + if (o instanceof Boolean) + dir = (Boolean) o; + + // Bidi over the paragraph. + try + { + getText(pStart, pEnd - pStart, s); + } + catch (BadLocationException ex) + { + assert false : "Must not happen"; + } + int flag = Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT; + if (dir != null) + { + if (TextAttribute.RUN_DIRECTION_LTR.equals(dir)) + flag = Bidi.DIRECTION_LEFT_TO_RIGHT; + else + flag = Bidi.DIRECTION_RIGHT_TO_LEFT; + } + Bidi bidi = new Bidi(s.array, s.offset, null, 0, s.count, flag); + bidis.add(bidi); + i = pEnd; + } + Bidi[] ret = new Bidi[bidis.size()]; + ret = (Bidi[]) bidis.toArray(ret); + return ret; + } + + /** + * Blocks until a read lock can be obtained. Must block if there is + * currently a writer modifying the <code>Document</code>. + */ + public final synchronized void readLock() + { + try + { + while (currentWriter != null) + { + if (currentWriter == Thread.currentThread()) + return; + wait(); + } + numReaders++; + } + catch (InterruptedException ex) + { + throw new Error("Interrupted during grab read lock"); + } + } + + /** + * Releases the read lock. If this was the only reader on this + * <code>Document</code>, writing may begin now. + */ + public final synchronized void readUnlock() + { + // Note we could have a problem here if readUnlock was called without a + // prior call to readLock but the specs simply warn users to ensure that + // balance by using a finally block: + // readLock() + // try + // { + // doSomethingHere + // } + // finally + // { + // readUnlock(); + // } + + // All that the JDK seems to check for is that you don't call unlock + // more times than you've previously called lock, but it doesn't make + // sure that the threads calling unlock were the same ones that called lock + + // If the current thread holds the write lock, and attempted to also obtain + // a readLock, then numReaders hasn't been incremented and we don't need + // to unlock it here. + if (currentWriter == Thread.currentThread()) + return; + + // FIXME: the reference implementation throws a + // javax.swing.text.StateInvariantError here + if (numReaders <= 0) + throw new IllegalStateException("document lock failure"); + + // If currentWriter is not null, the application code probably had a + // writeLock and then tried to obtain a readLock, in which case + // numReaders wasn't incremented + numReaders--; + notify(); + } + + /** + * Removes a piece of content from this <code>Document</code>. + * + * <p>If a {@link DocumentFilter} is installed in this document, the + * corresponding method of the filter object is called. The + * <code>DocumentFilter</code> is called even if <code>length</code> + * is zero. This is different from {@link #replace}.</p> + * + * <p>Note: When <code>length</code> is zero or below the call is not + * forwarded to the underlying {@link AbstractDocument.Content} instance + * of this document and no exception is thrown.</p> + * + * @param offset the start offset of the fragment to be removed + * @param length the length of the fragment to be removed + * + * @throws BadLocationException if <code>offset</code> or + * <code>offset + length</code> or invalid locations within this + * document + */ + public void remove(int offset, int length) throws BadLocationException + { + writeLock(); + try + { + DocumentFilter f = getDocumentFilter(); + if (f == null) + removeImpl(offset, length); + else + f.remove(getBypass(), offset, length); + } + finally + { + writeUnlock(); + } + } + + void removeImpl(int offset, int length) throws BadLocationException + { + // The RI silently ignores all requests that have a negative length. + // Don't ask my why, but that's how it is. + if (length > 0) + { + if (offset < 0 || offset > getLength()) + throw new BadLocationException("Invalid remove position", offset); + + if (offset + length > getLength()) + throw new BadLocationException("Invalid remove length", offset); + + DefaultDocumentEvent event = + new DefaultDocumentEvent(offset, length, + DocumentEvent.EventType.REMOVE); + + // The order of the operations below is critical! + removeUpdate(event); + UndoableEdit temp = content.remove(offset, length); + + postRemoveUpdate(event); + fireRemoveUpdate(event); + } + } + + /** + * Replaces a piece of content in this <code>Document</code> with + * another piece of content. + * + * <p>If a {@link DocumentFilter} is installed in this document, the + * corresponding method of the filter object is called.</p> + * + * <p>The method has no effect if <code>length</code> is zero (and + * only zero) and, at the same time, <code>text</code> is + * <code>null</code> or has zero length.</p> + * + * @param offset the start offset of the fragment to be removed + * @param length the length of the fragment to be removed + * @param text the text to replace the content with + * @param attributes the text attributes to assign to the new content + * + * @throws BadLocationException if <code>offset</code> or + * <code>offset + length</code> or invalid locations within this + * document + * + * @since 1.4 + */ + public void replace(int offset, int length, String text, + AttributeSet attributes) + throws BadLocationException + { + // Bail out if we have a bogus replacement (Behavior observed in RI). + if (length == 0 + && (text == null || text.length() == 0)) + return; + + writeLock(); + try + { + if (documentFilter == null) + { + // It is important to call the methods which again do the checks + // of the arguments and the DocumentFilter because subclasses may + // have overridden these methods and provide crucial behavior + // which would be skipped if we call the non-checking variants. + // An example for this is PlainDocument where insertString can + // provide a filtering of newlines. + remove(offset, length); + insertString(offset, text, attributes); + } + else + documentFilter.replace(getBypass(), offset, length, text, attributes); + } + finally + { + writeUnlock(); + } + } + + void replaceImpl(int offset, int length, String text, + AttributeSet attributes) + throws BadLocationException + { + removeImpl(offset, length); + insertStringImpl(offset, text, attributes); + } + + /** + * Adds a <code>DocumentListener</code> object to this document. + * + * @param listener the listener to add + */ + public void addDocumentListener(DocumentListener listener) + { + listenerList.add(DocumentListener.class, listener); + } + + /** + * Removes a <code>DocumentListener</code> object from this document. + * + * @param listener the listener to remove + */ + public void removeDocumentListener(DocumentListener listener) + { + listenerList.remove(DocumentListener.class, listener); + } + + /** + * Returns all registered <code>DocumentListener</code>s. + * + * @return all registered <code>DocumentListener</code>s + */ + public DocumentListener[] getDocumentListeners() + { + return (DocumentListener[]) getListeners(DocumentListener.class); + } + + /** + * Adds an {@link UndoableEditListener} to this <code>Document</code>. + * + * @param listener the listener to add + */ + public void addUndoableEditListener(UndoableEditListener listener) + { + listenerList.add(UndoableEditListener.class, listener); + } + + /** + * Removes an {@link UndoableEditListener} from this <code>Document</code>. + * + * @param listener the listener to remove + */ + public void removeUndoableEditListener(UndoableEditListener listener) + { + listenerList.remove(UndoableEditListener.class, listener); + } + + /** + * Returns all registered {@link UndoableEditListener}s. + * + * @return all registered {@link UndoableEditListener}s + */ + public UndoableEditListener[] getUndoableEditListeners() + { + return (UndoableEditListener[]) getListeners(UndoableEditListener.class); + } + + /** + * Called before some content gets removed from this <code>Document</code>. + * The default implementation does nothing but may be overridden by + * subclasses to modify the <code>Document</code> structure in response + * to a remove request. The method is executed within a write lock. + * + * @param chng the <code>DefaultDocumentEvent</code> describing the change + */ + protected void removeUpdate(DefaultDocumentEvent chng) + { + // Do nothing here. Subclasses may wish to override this. + } + + /** + * Called to render this <code>Document</code> visually. It obtains a read + * lock, ensuring that no changes will be made to the <code>document</code> + * during the rendering process. It then calls the {@link Runnable#run()} + * method on <code>runnable</code>. This method <em>must not</em> attempt + * to modifiy the <code>Document</code>, since a deadlock will occur if it + * tries to obtain a write lock. When the {@link Runnable#run()} method + * completes (either naturally or by throwing an exception), the read lock + * is released. Note that there is nothing in this method related to + * the actual rendering. It could be used to execute arbitrary code within + * a read lock. + * + * @param runnable the {@link Runnable} to execute + */ + public void render(Runnable runnable) + { + readLock(); + try + { + runnable.run(); + } + finally + { + readUnlock(); + } + } + + /** + * Sets the asynchronous loading priority for this <code>Document</code>. + * A value of <code>-1</code> indicates that this <code>Document</code> + * should be loaded synchronously. + * + * @param p the asynchronous loading priority to set + */ + public void setAsynchronousLoadPriority(int p) + { + Integer val = p >= 0 ? new Integer(p) : null; + putProperty(AsyncLoadPriority, val); + } + + /** + * Sets the properties of this <code>Document</code>. + * + * @param p the document properties to set + */ + public void setDocumentProperties(Dictionary<Object, Object> p) + { + // FIXME: make me thread-safe + properties = p; + } + + /** + * Blocks until a write lock can be obtained. Must wait if there are + * readers currently reading or another thread is currently writing. + */ + protected synchronized final void writeLock() + { + try + { + while (numReaders > 0 || currentWriter != null) + { + if (Thread.currentThread() == currentWriter) + { + if (notifyListeners) + throw new IllegalStateException("Mutation during notify"); + numWriters++; + return; + } + wait(); + } + currentWriter = Thread.currentThread(); + numWriters = 1; + } + catch (InterruptedException ex) + { + throw new Error("Interupted during grab write lock"); + } + } + + /** + * Releases the write lock. This allows waiting readers or writers to + * obtain the lock. + */ + protected final synchronized void writeUnlock() + { + if (--numWriters <= 0) + { + numWriters = 0; + currentWriter = null; + notifyAll(); + } + } + + /** + * Returns the currently installed {@link DocumentFilter} for this + * <code>Document</code>. + * + * @return the currently installed {@link DocumentFilter} for this + * <code>Document</code> + * + * @since 1.4 + */ + public DocumentFilter getDocumentFilter() + { + return documentFilter; + } + + /** + * Sets the {@link DocumentFilter} for this <code>Document</code>. + * + * @param filter the <code>DocumentFilter</code> to set + * + * @since 1.4 + */ + public void setDocumentFilter(DocumentFilter filter) + { + this.documentFilter = filter; + } + + /** + * Dumps diagnostic information to the specified <code>PrintStream</code>. + * + * @param out the stream to write the diagnostic information to + */ + public void dump(PrintStream out) + { + ((AbstractElement) getDefaultRootElement()).dump(out, 0); + ((AbstractElement) getBidiRootElement()).dump(out, 0); + } + + /** + * Defines a set of methods for managing text attributes for one or more + * <code>Document</code>s. + * + * Replicating {@link AttributeSet}s throughout a <code>Document</code> can + * be very expensive. Implementations of this interface are intended to + * provide intelligent management of <code>AttributeSet</code>s, eliminating + * costly duplication. + * + * @see StyleContext + */ + public interface AttributeContext + { + /** + * Returns an {@link AttributeSet} that contains the attributes + * of <code>old</code> plus the new attribute specified by + * <code>name</code> and <code>value</code>. + * + * @param old the attribute set to be merged with the new attribute + * @param name the name of the attribute to be added + * @param value the value of the attribute to be added + * + * @return the old attributes plus the new attribute + */ + AttributeSet addAttribute(AttributeSet old, Object name, Object value); + + /** + * Returns an {@link AttributeSet} that contains the attributes + * of <code>old</code> plus the new attributes in <code>attributes</code>. + * + * @param old the set of attributes where to add the new attributes + * @param attributes the attributes to be added + * + * @return an {@link AttributeSet} that contains the attributes + * of <code>old</code> plus the new attributes in + * <code>attributes</code> + */ + AttributeSet addAttributes(AttributeSet old, AttributeSet attributes); + + /** + * Returns an empty {@link AttributeSet}. + * + * @return an empty {@link AttributeSet} + */ + AttributeSet getEmptySet(); + + /** + * Called to indicate that the attributes in <code>attributes</code> are + * no longer used. + * + * @param attributes the attributes are no longer used + */ + void reclaim(AttributeSet attributes); + + /** + * Returns a {@link AttributeSet} that has the attribute with the specified + * <code>name</code> removed from <code>old</code>. + * + * @param old the attribute set from which an attribute is removed + * @param name the name of the attribute to be removed + * + * @return the attributes of <code>old</code> minus the attribute + * specified by <code>name</code> + */ + AttributeSet removeAttribute(AttributeSet old, Object name); + + /** + * Removes all attributes in <code>attributes</code> from <code>old</code> + * and returns the resulting <code>AttributeSet</code>. + * + * @param old the set of attributes from which to remove attributes + * @param attributes the attributes to be removed from <code>old</code> + * + * @return the attributes of <code>old</code> minus the attributes in + * <code>attributes</code> + */ + AttributeSet removeAttributes(AttributeSet old, AttributeSet attributes); + + /** + * Removes all attributes specified by <code>names</code> from + * <code>old</code> and returns the resulting <code>AttributeSet</code>. + * + * @param old the set of attributes from which to remove attributes + * @param names the names of the attributes to be removed from + * <code>old</code> + * + * @return the attributes of <code>old</code> minus the attributes in + * <code>attributes</code> + */ + AttributeSet removeAttributes(AttributeSet old, Enumeration<?> names); + } + + /** + * A sequence of data that can be edited. This is were the actual content + * in <code>AbstractDocument</code>'s is stored. + */ + public interface Content + { + /** + * Creates a {@link Position} that keeps track of the location at + * <code>offset</code>. + * + * @return a {@link Position} that keeps track of the location at + * <code>offset</code>. + * + * @throw BadLocationException if <code>offset</code> is not a valid + * location in this <code>Content</code> model + */ + Position createPosition(int offset) throws BadLocationException; + + /** + * Returns the length of the content. + * + * @return the length of the content + */ + int length(); + + /** + * Inserts a string into the content model. + * + * @param where the offset at which to insert the string + * @param str the string to be inserted + * + * @return an <code>UndoableEdit</code> or <code>null</code> if undo is + * not supported by this <code>Content</code> model + * + * @throws BadLocationException if <code>where</code> is not a valid + * location in this <code>Content</code> model + */ + UndoableEdit insertString(int where, String str) + throws BadLocationException; + + /** + * Removes a piece of content from the content model. + * + * @param where the offset at which to remove content + * @param nitems the number of characters to be removed + * + * @return an <code>UndoableEdit</code> or <code>null</code> if undo is + * not supported by this <code>Content</code> model + * + * @throws BadLocationException if <code>where</code> is not a valid + * location in this <code>Content</code> model + */ + UndoableEdit remove(int where, int nitems) throws BadLocationException; + + /** + * Returns a piece of content. + * + * @param where the start offset of the requested fragment + * @param len the length of the requested fragment + * + * @return the requested fragment + * @throws BadLocationException if <code>offset</code> or + * <code>offset + len</code>is not a valid + * location in this <code>Content</code> model + */ + String getString(int where, int len) throws BadLocationException; + + /** + * Fetches a piece of content and stores it in <code>txt</code>. + * + * @param where the start offset of the requested fragment + * @param len the length of the requested fragment + * @param txt the <code>Segment</code> where to fragment is stored into + * + * @throws BadLocationException if <code>offset</code> or + * <code>offset + len</code>is not a valid + * location in this <code>Content</code> model + */ + void getChars(int where, int len, Segment txt) throws BadLocationException; + } + + /** + * An abstract base implementation of the {@link Element} interface. + */ + public abstract class AbstractElement + implements Element, MutableAttributeSet, TreeNode, Serializable + { + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = 1712240033321461704L; + + /** The number of characters that this Element spans. */ + int count; + + /** The starting offset of this Element. */ + int offset; + + /** The attributes of this Element. */ + AttributeSet attributes; + + /** The parent element. */ + Element element_parent; + + /** The parent in the TreeNode interface. */ + TreeNode tree_parent; + + /** The children of this element. */ + Vector tree_children; + + /** + * Creates a new instance of <code>AbstractElement</code> with a + * specified parent <code>Element</code> and <code>AttributeSet</code>. + * + * @param p the parent of this <code>AbstractElement</code> + * @param s the attributes to be assigned to this + * <code>AbstractElement</code> + */ + public AbstractElement(Element p, AttributeSet s) + { + element_parent = p; + AttributeContext ctx = getAttributeContext(); + attributes = ctx.getEmptySet(); + if (s != null) + addAttributes(s); + } + + /** + * Returns the child nodes of this <code>Element</code> as an + * <code>Enumeration</code> of {@link TreeNode}s. + * + * @return the child nodes of this <code>Element</code> as an + * <code>Enumeration</code> of {@link TreeNode}s + */ + public abstract Enumeration children(); + + /** + * Returns <code>true</code> if this <code>AbstractElement</code> + * allows children. + * + * @return <code>true</code> if this <code>AbstractElement</code> + * allows children + */ + public abstract boolean getAllowsChildren(); + + /** + * Returns the child of this <code>AbstractElement</code> at + * <code>index</code>. + * + * @param index the position in the child list of the child element to + * be returned + * + * @return the child of this <code>AbstractElement</code> at + * <code>index</code> + */ + public TreeNode getChildAt(int index) + { + return (TreeNode) tree_children.get(index); + } + + /** + * Returns the number of children of this <code>AbstractElement</code>. + * + * @return the number of children of this <code>AbstractElement</code> + */ + public int getChildCount() + { + return tree_children.size(); + } + + /** + * Returns the index of a given child <code>TreeNode</code> or + * <code>-1</code> if <code>node</code> is not a child of this + * <code>AbstractElement</code>. + * + * @param node the node for which the index is requested + * + * @return the index of a given child <code>TreeNode</code> or + * <code>-1</code> if <code>node</code> is not a child of this + * <code>AbstractElement</code> + */ + public int getIndex(TreeNode node) + { + return tree_children.indexOf(node); + } + + /** + * Returns the parent <code>TreeNode</code> of this + * <code>AbstractElement</code> or <code>null</code> if this element + * has no parent. + * + * @return the parent <code>TreeNode</code> of this + * <code>AbstractElement</code> or <code>null</code> if this + * element has no parent + */ + public TreeNode getParent() + { + return tree_parent; + } + + /** + * Returns <code>true</code> if this <code>AbstractElement</code> is a + * leaf element, <code>false</code> otherwise. + * + * @return <code>true</code> if this <code>AbstractElement</code> is a + * leaf element, <code>false</code> otherwise + */ + public abstract boolean isLeaf(); + + /** + * Adds an attribute to this element. + * + * @param name the name of the attribute to be added + * @param value the value of the attribute to be added + */ + public void addAttribute(Object name, Object value) + { + attributes = getAttributeContext().addAttribute(attributes, name, value); + } + + /** + * Adds a set of attributes to this element. + * + * @param attrs the attributes to be added to this element + */ + public void addAttributes(AttributeSet attrs) + { + attributes = getAttributeContext().addAttributes(attributes, attrs); + } + + /** + * Removes an attribute from this element. + * + * @param name the name of the attribute to be removed + */ + public void removeAttribute(Object name) + { + attributes = getAttributeContext().removeAttribute(attributes, name); + } + + /** + * Removes a set of attributes from this element. + * + * @param attrs the attributes to be removed + */ + public void removeAttributes(AttributeSet attrs) + { + attributes = getAttributeContext().removeAttributes(attributes, attrs); + } + + /** + * Removes a set of attribute from this element. + * + * @param names the names of the attributes to be removed + */ + public void removeAttributes(Enumeration<?> names) + { + attributes = getAttributeContext().removeAttributes(attributes, names); + } + + /** + * Sets the parent attribute set against which the element can resolve + * attributes that are not defined in itself. + * + * @param parent the resolve parent to set + */ + public void setResolveParent(AttributeSet parent) + { + attributes = getAttributeContext().addAttribute(attributes, + ResolveAttribute, + parent); + } + + /** + * Returns <code>true</code> if this element contains the specified + * attribute. + * + * @param name the name of the attribute to check + * @param value the value of the attribute to check + * + * @return <code>true</code> if this element contains the specified + * attribute + */ + public boolean containsAttribute(Object name, Object value) + { + return attributes.containsAttribute(name, value); + } + + /** + * Returns <code>true</code> if this element contains all of the + * specified attributes. + * + * @param attrs the attributes to check + * + * @return <code>true</code> if this element contains all of the + * specified attributes + */ + public boolean containsAttributes(AttributeSet attrs) + { + return attributes.containsAttributes(attrs); + } + + /** + * Returns a copy of the attributes of this element. + * + * @return a copy of the attributes of this element + */ + public AttributeSet copyAttributes() + { + return attributes.copyAttributes(); + } + + /** + * Returns the attribute value with the specified key. If this attribute + * is not defined in this element and this element has a resolving + * parent, the search goes upward to the resolve parent chain. + * + * @param key the key of the requested attribute + * + * @return the attribute value for <code>key</code> of <code>null</code> + * if <code>key</code> is not found locally and cannot be resolved + * in this element's resolve parents + */ + public Object getAttribute(Object key) + { + Object result = attributes.getAttribute(key); + if (result == null) + { + AttributeSet resParent = getResolveParent(); + if (resParent != null) + result = resParent.getAttribute(key); + } + return result; + } + + /** + * Returns the number of defined attributes in this element. + * + * @return the number of defined attributes in this element + */ + public int getAttributeCount() + { + return attributes.getAttributeCount(); + } + + /** + * Returns the names of the attributes of this element. + * + * @return the names of the attributes of this element + */ + public Enumeration<?> getAttributeNames() + { + return attributes.getAttributeNames(); + } + + /** + * Returns the resolve parent of this element. + * This is taken from the AttributeSet, but if this is null, + * this method instead returns the Element's parent's + * AttributeSet + * + * @return the resolve parent of this element + * + * @see #setResolveParent(AttributeSet) + */ + public AttributeSet getResolveParent() + { + return attributes.getResolveParent(); + } + + /** + * Returns <code>true</code> if an attribute with the specified name + * is defined in this element, <code>false</code> otherwise. + * + * @param attrName the name of the requested attributes + * + * @return <code>true</code> if an attribute with the specified name + * is defined in this element, <code>false</code> otherwise + */ + public boolean isDefined(Object attrName) + { + return attributes.isDefined(attrName); + } + + /** + * Returns <code>true</code> if the specified <code>AttributeSet</code> + * is equal to this element's <code>AttributeSet</code>, <code>false</code> + * otherwise. + * + * @param attrs the attributes to compare this element to + * + * @return <code>true</code> if the specified <code>AttributeSet</code> + * is equal to this element's <code>AttributeSet</code>, + * <code>false</code> otherwise + */ + public boolean isEqual(AttributeSet attrs) + { + return attributes.isEqual(attrs); + } + + /** + * Returns the attributes of this element. + * + * @return the attributes of this element + */ + public AttributeSet getAttributes() + { + return this; + } + + /** + * Returns the {@link Document} to which this element belongs. + * + * @return the {@link Document} to which this element belongs + */ + public Document getDocument() + { + return AbstractDocument.this; + } + + /** + * Returns the child element at the specified <code>index</code>. + * + * @param index the index of the requested child element + * + * @return the requested element + */ + public abstract Element getElement(int index); + + /** + * Returns the name of this element. + * + * @return the name of this element + */ + public String getName() + { + return (String) attributes.getAttribute(ElementNameAttribute); + } + + /** + * Returns the parent element of this element. + * + * @return the parent element of this element + */ + public Element getParentElement() + { + return element_parent; + } + + /** + * Returns the offset inside the document model that is after the last + * character of this element. + * + * @return the offset inside the document model that is after the last + * character of this element + */ + public abstract int getEndOffset(); + + /** + * Returns the number of child elements of this element. + * + * @return the number of child elements of this element + */ + public abstract int getElementCount(); + + /** + * Returns the index of the child element that spans the specified + * offset in the document model. + * + * @param offset the offset for which the responsible element is searched + * + * @return the index of the child element that spans the specified + * offset in the document model + */ + public abstract int getElementIndex(int offset); + + /** + * Returns the start offset if this element inside the document model. + * + * @return the start offset if this element inside the document model + */ + public abstract int getStartOffset(); + + /** + * Prints diagnostic output to the specified stream. + * + * @param stream the stream to write to + * @param indent the indentation level + */ + public void dump(PrintStream stream, int indent) + { + CPStringBuilder b = new CPStringBuilder(); + for (int i = 0; i < indent; ++i) + b.append(' '); + b.append('<'); + b.append(getName()); + // Dump attributes if there are any. + if (getAttributeCount() > 0) + { + b.append('\n'); + Enumeration attNames = getAttributeNames(); + while (attNames.hasMoreElements()) + { + for (int i = 0; i < indent + 2; ++i) + b.append(' '); + Object attName = attNames.nextElement(); + b.append(attName); + b.append('='); + Object attribute = getAttribute(attName); + b.append(attribute); + b.append('\n'); + } + } + if (getAttributeCount() > 0) + { + for (int i = 0; i < indent; ++i) + b.append(' '); + } + b.append(">\n"); + + // Dump element content for leaf elements. + if (isLeaf()) + { + for (int i = 0; i < indent + 2; ++i) + b.append(' '); + int start = getStartOffset(); + int end = getEndOffset(); + b.append('['); + b.append(start); + b.append(','); + b.append(end); + b.append("]["); + try + { + b.append(getDocument().getText(start, end - start)); + } + catch (BadLocationException ex) + { + AssertionError err = new AssertionError("BadLocationException " + + "must not be thrown " + + "here."); + err.initCause(ex); + throw err; + } + b.append("]\n"); + } + stream.print(b.toString()); + + // Dump child elements if any. + int count = getElementCount(); + for (int i = 0; i < count; ++i) + { + Element el = getElement(i); + if (el instanceof AbstractElement) + ((AbstractElement) el).dump(stream, indent + 2); + } + } + } + + /** + * An implementation of {@link Element} to represent composite + * <code>Element</code>s that contain other <code>Element</code>s. + */ + public class BranchElement extends AbstractElement + { + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = -6037216547466333183L; + + /** + * The child elements of this BranchElement. + */ + private Element[] children; + + /** + * The number of children in the branch element. + */ + private int numChildren; + + /** + * The last found index in getElementIndex(). Used for faster searching. + */ + private int lastIndex; + + /** + * Creates a new <code>BranchElement</code> with the specified + * parent and attributes. + * + * @param parent the parent element of this <code>BranchElement</code> + * @param attributes the attributes to set on this + * <code>BranchElement</code> + */ + public BranchElement(Element parent, AttributeSet attributes) + { + super(parent, attributes); + children = new Element[1]; + numChildren = 0; + lastIndex = -1; + } + + /** + * Returns the children of this <code>BranchElement</code>. + * + * @return the children of this <code>BranchElement</code> + */ + public Enumeration children() + { + if (numChildren == 0) + return null; + + Vector tmp = new Vector(); + + for (int index = 0; index < numChildren; ++index) + tmp.add(children[index]); + + return tmp.elements(); + } + + /** + * Returns <code>true</code> since <code>BranchElements</code> allow + * child elements. + * + * @return <code>true</code> since <code>BranchElements</code> allow + * child elements + */ + public boolean getAllowsChildren() + { + return true; + } + + /** + * Returns the child element at the specified <code>index</code>. + * + * @param index the index of the requested child element + * + * @return the requested element + */ + public Element getElement(int index) + { + if (index < 0 || index >= numChildren) + return null; + + return children[index]; + } + + /** + * Returns the number of child elements of this element. + * + * @return the number of child elements of this element + */ + public int getElementCount() + { + return numChildren; + } + + /** + * Returns the index of the child element that spans the specified + * offset in the document model. + * + * @param offset the offset for which the responsible element is searched + * + * @return the index of the child element that spans the specified + * offset in the document model + */ + public int getElementIndex(int offset) + { + // Implemented using an improved linear search. + // This makes use of the fact that searches are not random but often + // close to the previous search. So we try to start the binary + // search at the last found index. + + int i0 = 0; // The lower bounds. + int i1 = numChildren - 1; // The upper bounds. + int index = -1; // The found index. + + int p0 = getStartOffset(); + int p1; // Start and end offset local variables. + + if (numChildren == 0) + index = 0; + else if (offset >= getEndOffset()) + index = numChildren - 1; + else + { + // Try lastIndex. + if (lastIndex >= i0 && lastIndex <= i1) + { + Element last = getElement(lastIndex); + p0 = last.getStartOffset(); + p1 = last.getEndOffset(); + if (offset >= p0 && offset < p1) + index = lastIndex; + else + { + // Narrow the search bounds using the lastIndex, even + // if it hasn't been a hit. + if (offset < p0) + i1 = lastIndex; + else + i0 = lastIndex; + } + } + // The actual search. + int i = 0; + while (i0 <= i1 && index == -1) + { + i = i0 + (i1 - i0) / 2; + Element el = getElement(i); + p0 = el.getStartOffset(); + p1 = el.getEndOffset(); + if (offset >= p0 && offset < p1) + { + // Found it! + index = i; + } + else if (offset < p0) + i1 = i - 1; + else + i0 = i + 1; + } + + if (index == -1) + { + // Didn't find it. Return the boundary index. + if (offset < p0) + index = i; + else + index = i + 1; + } + + lastIndex = index; + } + return index; + } + + /** + * Returns the offset inside the document model that is after the last + * character of this element. + * This is the end offset of the last child element. If this element + * has no children, this method throws a <code>NullPointerException</code>. + * + * @return the offset inside the document model that is after the last + * character of this element + * + * @throws NullPointerException if this branch element has no children + */ + public int getEndOffset() + { + // This might accss one cached element or trigger an NPE for + // numChildren == 0. This is checked by a Mauve test. + Element child = numChildren > 0 ? children[numChildren - 1] + : children[0]; + return child.getEndOffset(); + } + + /** + * Returns the name of this element. This is {@link #ParagraphElementName} + * in this case. + * + * @return the name of this element + */ + public String getName() + { + return ParagraphElementName; + } + + /** + * Returns the start offset of this element inside the document model. + * This is the start offset of the first child element. If this element + * has no children, this method throws a <code>NullPointerException</code>. + * + * @return the start offset of this element inside the document model + * + * @throws NullPointerException if this branch element has no children and + * no startOffset value has been cached + */ + public int getStartOffset() + { + // Do not explicitly throw an NPE here. If the first element is null + // then the NPE gets thrown anyway. If it isn't, then it either + // holds a real value (for numChildren > 0) or a cached value + // (for numChildren == 0) as we don't fully remove elements in replace() + // when removing single elements. + // This is checked by a Mauve test. + return children[0].getStartOffset(); + } + + /** + * Returns <code>false</code> since <code>BranchElement</code> are no + * leafes. + * + * @return <code>false</code> since <code>BranchElement</code> are no + * leafes + */ + public boolean isLeaf() + { + return false; + } + + /** + * Returns the <code>Element</code> at the specified <code>Document</code> + * offset. + * + * @return the <code>Element</code> at the specified <code>Document</code> + * offset + * + * @see #getElementIndex(int) + */ + public Element positionToElement(int position) + { + // XXX: There is surely a better algorithm + // as beginning from first element each time. + for (int index = 0; index < numChildren; ++index) + { + Element elem = children[index]; + + if ((elem.getStartOffset() <= position) + && (position < elem.getEndOffset())) + return elem; + } + + return null; + } + + /** + * Replaces a set of child elements with a new set of child elemens. + * + * @param offset the start index of the elements to be removed + * @param length the number of elements to be removed + * @param elements the new elements to be inserted + */ + public void replace(int offset, int length, Element[] elements) + { + int delta = elements.length - length; + int copyFrom = offset + length; // From where to copy. + int copyTo = copyFrom + delta; // Where to copy to. + int numMove = numChildren - copyFrom; // How many elements are moved. + if (numChildren + delta > children.length) + { + // Gotta grow the array. + int newSize = Math.max(2 * children.length, numChildren + delta); + Element[] target = new Element[newSize]; + System.arraycopy(children, 0, target, 0, offset); + System.arraycopy(elements, 0, target, offset, elements.length); + System.arraycopy(children, copyFrom, target, copyTo, numMove); + children = target; + } + else + { + System.arraycopy(children, copyFrom, children, copyTo, numMove); + System.arraycopy(elements, 0, children, offset, elements.length); + } + numChildren += delta; + } + + /** + * Returns a string representation of this element. + * + * @return a string representation of this element + */ + public String toString() + { + return ("BranchElement(" + getName() + ") " + + getStartOffset() + "," + getEndOffset() + "\n"); + } + } + + /** + * Stores the changes when a <code>Document</code> is beeing modified. + */ + public class DefaultDocumentEvent extends CompoundEdit + implements DocumentEvent + { + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = 5230037221564563284L; + + /** + * The threshold that indicates when we switch to using a Hashtable. + */ + private static final int THRESHOLD = 10; + + /** The starting offset of the change. */ + private int offset; + + /** The length of the change. */ + private int length; + + /** The type of change. */ + private DocumentEvent.EventType type; + + /** + * Maps <code>Element</code> to their change records. This is only + * used when the changes array gets too big. We can use an + * (unsync'ed) HashMap here, since changes to this are (should) always + * be performed inside a write lock. + */ + private HashMap changes; + + /** + * Indicates if this event has been modified or not. This is used to + * determine if this event is thrown. + */ + private boolean modified; + + /** + * Creates a new <code>DefaultDocumentEvent</code>. + * + * @param offset the starting offset of the change + * @param length the length of the change + * @param type the type of change + */ + public DefaultDocumentEvent(int offset, int length, + DocumentEvent.EventType type) + { + this.offset = offset; + this.length = length; + this.type = type; + modified = false; + } + + /** + * Adds an UndoableEdit to this <code>DocumentEvent</code>. If this + * edit is an instance of {@link ElementEdit}, then this record can + * later be fetched by calling {@link #getChange}. + * + * @param edit the undoable edit to add + */ + public boolean addEdit(UndoableEdit edit) + { + // Start using Hashtable when we pass a certain threshold. This + // gives a good memory/performance compromise. + if (changes == null && edits.size() > THRESHOLD) + { + changes = new HashMap(); + int count = edits.size(); + for (int i = 0; i < count; i++) + { + Object o = edits.elementAt(i); + if (o instanceof ElementChange) + { + ElementChange ec = (ElementChange) o; + changes.put(ec.getElement(), ec); + } + } + } + + if (changes != null && edit instanceof ElementChange) + { + ElementChange elEdit = (ElementChange) edit; + changes.put(elEdit.getElement(), elEdit); + } + return super.addEdit(edit); + } + + /** + * Returns the document that has been modified. + * + * @return the document that has been modified + */ + public Document getDocument() + { + return AbstractDocument.this; + } + + /** + * Returns the length of the modification. + * + * @return the length of the modification + */ + public int getLength() + { + return length; + } + + /** + * Returns the start offset of the modification. + * + * @return the start offset of the modification + */ + public int getOffset() + { + return offset; + } + + /** + * Returns the type of the modification. + * + * @return the type of the modification + */ + public DocumentEvent.EventType getType() + { + return type; + } + + /** + * Returns the changes for an element. + * + * @param elem the element for which the changes are requested + * + * @return the changes for <code>elem</code> or <code>null</code> if + * <code>elem</code> has not been changed + */ + public ElementChange getChange(Element elem) + { + ElementChange change = null; + if (changes != null) + { + change = (ElementChange) changes.get(elem); + } + else + { + int count = edits.size(); + for (int i = 0; i < count && change == null; i++) + { + Object o = edits.get(i); + if (o instanceof ElementChange) + { + ElementChange ec = (ElementChange) o; + if (elem.equals(ec.getElement())) + change = ec; + } + } + } + return change; + } + + /** + * Returns a String description of the change event. This returns the + * toString method of the Vector of edits. + */ + public String toString() + { + return edits.toString(); + } + } + + /** + * An implementation of {@link DocumentEvent.ElementChange} to be added + * to {@link DefaultDocumentEvent}s. + */ + public static class ElementEdit extends AbstractUndoableEdit + implements DocumentEvent.ElementChange + { + /** The serial version UID of ElementEdit. */ + private static final long serialVersionUID = -1216620962142928304L; + + /** + * The changed element. + */ + private Element elem; + + /** + * The index of the change. + */ + private int index; + + /** + * The removed elements. + */ + private Element[] removed; + + /** + * The added elements. + */ + private Element[] added; + + /** + * Creates a new <code>ElementEdit</code>. + * + * @param elem the changed element + * @param index the index of the change + * @param removed the removed elements + * @param added the added elements + */ + public ElementEdit(Element elem, int index, + Element[] removed, Element[] added) + { + this.elem = elem; + this.index = index; + this.removed = removed; + this.added = added; + } + + /** + * Returns the added elements. + * + * @return the added elements + */ + public Element[] getChildrenAdded() + { + return added; + } + + /** + * Returns the removed elements. + * + * @return the removed elements + */ + public Element[] getChildrenRemoved() + { + return removed; + } + + /** + * Returns the changed element. + * + * @return the changed element + */ + public Element getElement() + { + return elem; + } + + /** + * Returns the index of the change. + * + * @return the index of the change + */ + public int getIndex() + { + return index; + } + } + + /** + * An implementation of {@link Element} that represents a leaf in the + * document structure. This is used to actually store content. + */ + public class LeafElement extends AbstractElement + { + /** The serialization UID (compatible with JDK1.5). */ + private static final long serialVersionUID = -8906306331347768017L; + + /** + * Manages the start offset of this element. + */ + private Position startPos; + + /** + * Manages the end offset of this element. + */ + private Position endPos; + + /** + * Creates a new <code>LeafElement</code>. + * + * @param parent the parent of this <code>LeafElement</code> + * @param attributes the attributes to be set + * @param start the start index of this element inside the document model + * @param end the end index of this element inside the document model + */ + public LeafElement(Element parent, AttributeSet attributes, int start, + int end) + { + super(parent, attributes); + try + { + startPos = createPosition(start); + endPos = createPosition(end); + } + catch (BadLocationException ex) + { + AssertionError as; + as = new AssertionError("BadLocationException thrown " + + "here. start=" + start + + ", end=" + end + + ", length=" + getLength()); + as.initCause(ex); + throw as; + } + } + + /** + * Returns <code>null</code> since <code>LeafElement</code>s cannot have + * children. + * + * @return <code>null</code> since <code>LeafElement</code>s cannot have + * children + */ + public Enumeration children() + { + return null; + } + + /** + * Returns <code>false</code> since <code>LeafElement</code>s cannot have + * children. + * + * @return <code>false</code> since <code>LeafElement</code>s cannot have + * children + */ + public boolean getAllowsChildren() + { + return false; + } + + /** + * Returns <code>null</code> since <code>LeafElement</code>s cannot have + * children. + * + * @return <code>null</code> since <code>LeafElement</code>s cannot have + * children + */ + public Element getElement(int index) + { + return null; + } + + /** + * Returns <code>0</code> since <code>LeafElement</code>s cannot have + * children. + * + * @return <code>0</code> since <code>LeafElement</code>s cannot have + * children + */ + public int getElementCount() + { + return 0; + } + + /** + * Returns <code>-1</code> since <code>LeafElement</code>s cannot have + * children. + * + * @return <code>-1</code> since <code>LeafElement</code>s cannot have + * children + */ + public int getElementIndex(int offset) + { + return -1; + } + + /** + * Returns the end offset of this <code>Element</code> inside the + * document. + * + * @return the end offset of this <code>Element</code> inside the + * document + */ + public int getEndOffset() + { + return endPos.getOffset(); + } + + /** + * Returns the name of this <code>Element</code>. This is + * {@link #ContentElementName} in this case. + * + * @return the name of this <code>Element</code> + */ + public String getName() + { + String name = super.getName(); + if (name == null) + name = ContentElementName; + return name; + } + + /** + * Returns the start offset of this <code>Element</code> inside the + * document. + * + * @return the start offset of this <code>Element</code> inside the + * document + */ + public int getStartOffset() + { + return startPos.getOffset(); + } + + /** + * Returns <code>true</code>. + * + * @return <code>true</code> + */ + public boolean isLeaf() + { + return true; + } + + /** + * Returns a string representation of this <code>Element</code>. + * + * @return a string representation of this <code>Element</code> + */ + public String toString() + { + return ("LeafElement(" + getName() + ") " + + getStartOffset() + "," + getEndOffset() + "\n"); + } + } + + /** + * The root element for bidirectional text. + */ + private class BidiRootElement + extends BranchElement + { + /** + * Creates a new bidi root element. + */ + BidiRootElement() + { + super(null, null); + } + + /** + * Returns the name of the element. + * + * @return the name of the element + */ + public String getName() + { + return BidiRootName; + } + } + + /** + * A leaf element for the bidi structure. + */ + private class BidiElement + extends LeafElement + { + /** + * Creates a new BidiElement. + * + * @param parent the parent element + * @param start the start offset + * @param end the end offset + * @param level the bidi level + */ + BidiElement(Element parent, int start, int end, int level) + { + super(parent, new SimpleAttributeSet(), start, end); + addAttribute(StyleConstants.BidiLevel, new Integer(level)); + } + + /** + * Returns the name of the element. + * + * @return the name of the element + */ + public String getName() + { + return BidiElementName; + } + } + + /** A class whose methods delegate to the insert, remove and replace methods + * of this document which do not check for an installed DocumentFilter. + */ + class Bypass extends DocumentFilter.FilterBypass + { + + public Document getDocument() + { + return AbstractDocument.this; + } + + public void insertString(int offset, String string, AttributeSet attr) + throws BadLocationException + { + AbstractDocument.this.insertStringImpl(offset, string, attr); + } + + public void remove(int offset, int length) + throws BadLocationException + { + AbstractDocument.this.removeImpl(offset, length); + } + + public void replace(int offset, int length, String string, + AttributeSet attrs) + throws BadLocationException + { + AbstractDocument.this.replaceImpl(offset, length, string, attrs); + } + + } + +} |