/* 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); } } }