/* AbstractDocument.java -- Copyright (C) 2002, 2004, 2005 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 java.io.PrintStream; import java.io.Serializable; import java.util.Dictionary; import java.util.Enumeration; import java.util.EventListener; 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 Elements, * 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 Elements. */ public static final String BidiElementName = "bidi level"; /** * Standard name for content Elements. These are usually * {@link LeafElement}s. */ public static final String ContentElementName = "content"; /** * Standard name for paragraph Elements. These are usually * {@link BranchElement}s. */ public static final String ParagraphElementName = "paragraph"; /** * Standard name for section Elements. 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"; /** * The actual content model of this Document. */ Content content; /** * The AttributeContext for this Document. */ AttributeContext context; /** * The currently installed DocumentFilter. */ DocumentFilter documentFilter; /** * The documents properties. */ Dictionary properties; /** * Manages event listeners for this Document. */ 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; /** * Tells if there are one or more writers waiting. */ private int numWritersWaiting = 0; /** * A condition variable that readers and writers wait on. */ private Object documentCV = new Object(); /** 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 Element bidiRoot; /** * Creates a new AbstractDocument with the specified * {@link Content} model. * * @param doc the Content model to be used in this * Document * * @see GapContent * @see StringContent */ protected AbstractDocument(Content doc) { this(doc, StyleContext.getDefaultStyleContext()); } /** * Creates a new AbstractDocument with the specified * {@link Content} model and {@link AttributeContext}. * * @param doc the Content model to be used in this * Document * @param ctx the AttributeContext to use * * @see GapContent * @see StringContent */ protected AbstractDocument(Content doc, AttributeContext ctx) { content = doc; context = ctx; // FIXME: This is determined using a Mauve test. Make the document // actually use this. putProperty("i18n", Boolean.FALSE); // FIXME: Fully implement bidi. bidiRoot = new BranchElement(null, null); } /** 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 Document. * Usual Documents only have one root element and return this. * However, there may be Document implementations that * support multiple root elements, they have to return a default root element * here. * * @return the default root {@link Element} of this Document */ public abstract Element getDefaultRootElement(); /** * Creates and returns a branch element with the specified * parent and attributes. Note that the new * Element is linked to the parent Element * through {@link Element#getParentElement}, but it is not yet added * to the parent Element as child. * * @param parent the parent Element for the new branch element * @param attributes the text attributes to be installed in the new element * * @return the new branch Element * * @see BranchElement */ protected Element createBranchElement(Element parent, AttributeSet attributes) { return new BranchElement(parent, attributes); } /** * Creates and returns a leaf element with the specified * parent and attributes. Note that the new * Element is linked to the parent Element * through {@link Element#getParentElement}, but it is not yet added * to the parent Element as child. * * @param parent the parent Element for the new branch element * @param attributes the text attributes to be installed in the new element * * @return the new branch Element * * @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 offset. * * @param offset the location in the document to keep track by the new * Position * * @return the newly created Position * * @throws BadLocationException if offset is not a valid * location in the documents content model */ public Position createPosition(final int offset) throws BadLocationException { return content.createPosition(offset); } /** * Notifies all registered listeners when the document model changes. * * @param event the DocumentEvent to be fired */ protected void fireChangedUpdate(DocumentEvent event) { DocumentListener[] listeners = getDocumentListeners(); for (int index = 0; index < listeners.length; ++index) listeners[index].changedUpdate(event); } /** * Notifies all registered listeners when content is inserted in the document * model. * * @param event the DocumentEvent to be fired */ protected void fireInsertUpdate(DocumentEvent event) { DocumentListener[] listeners = getDocumentListeners(); for (int index = 0; index < listeners.length; ++index) listeners[index].insertUpdate(event); } /** * Notifies all registered listeners when content is removed from the * document model. * * @param event the DocumentEvent to be fired */ protected void fireRemoveUpdate(DocumentEvent event) { DocumentListener[] listeners = getDocumentListeners(); for (int index = 0; index < listeners.length; ++index) listeners[index].removeUpdate(event); } /** * Notifies all registered listeners when an UndoableEdit has * been performed on this Document. * * @param event the UndoableEditEvent 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 -1 if this * document should not be loaded asynchronously. * * @return the asynchronous loading priority */ public int getAsynchronousLoadPriority() { return 0; } /** * Returns the {@link AttributeContext} used in this Document. * * @return the {@link AttributeContext} used in this Document */ 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 Document * * @return the {@link Content} model for this Document * * @see GapContent * @see StringContent */ protected final Content getContent() { return content; } /** * Returns the thread that currently modifies this Document * if there is one, otherwise null. 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 Document * if there is one, otherwise null */ protected final Thread getCurrentWriter() { return currentWriter; } /** * Returns the properties of this Document. * * @return the properties of this Document */ public Dictionary 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 * Document. * * @return a {@link Position} which will always mark the end of the * Document */ public final Position getEndPosition() { // FIXME: Properly implement this by calling Content.createPosition(). return new Position() { public int getOffset() { return getLength(); } }; } /** * Returns the length of this Document's content. * * @return the length of this Document'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[] getListeners(Class listenerType) { return listenerList.getListeners(listenerType); } /** * Returns a property from this Document's property list. * * @param key the key of the property to be fetched * * @return the property for key or null 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 Document. By default * this just returns the single root element returned by * {@link #getDefaultRootElement()}. Document implementations * that support multiple roots must override this method and return all roots * here. * * @return all root elements of this Document */ 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 * Document. * * @return a {@link Position} which will always mark the beginning of the * Document */ public final Position getStartPosition() { // FIXME: Properly implement this using Content.createPosition(). return new Position() { public int getOffset() { return 0; } }; } /** * Returns a piece of this Document's content. * * @param offset the start offset of the content * @param length the length of the content * * @return the piece of content specified by offset and * length * * @throws BadLocationException if offset or offset + * length are invalid locations with this * Document */ public String getText(int offset, int length) throws BadLocationException { return content.getString(offset, length); } /** * Fetches a piece of this Document'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 Segment to store the content in * * @throws BadLocationException if offset or offset + * length are invalid locations with this * Document */ public void getText(int offset, int length, Segment segment) throws BadLocationException { content.getChars(offset, length, segment); } /** * Inserts a String into this Document at the specified * position and assigning the specified attributes to it. * *

If a {@link DocumentFilter} is installed in this document, the * corresponding method of the filter object is called.

* *

The method has no effect when text is null * or has a length of zero.

* * * @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 offset is not a valid * location in this Document */ 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; if (documentFilter == null) insertStringImpl(offset, text, attributes); else documentFilter.insertString(getBypass(), offset, text, attributes); } 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); try { writeLock(); UndoableEdit undo = content.insertString(offset, text); if (undo != null) event.addEdit(undo); insertUpdate(event, attributes); fireInsertUpdate(event); if (undo != null) fireUndoableEditUpdate(new UndoableEditEvent(this, undo)); } finally { writeUnlock(); } } /** * Called to indicate that text has been inserted into this * Document. The default implementation does nothing. * This method is executed within a write lock. * * @param chng the DefaultDocumentEvent describing the change * @param attr the attributes of the changed content */ protected void insertUpdate(DefaultDocumentEvent chng, AttributeSet attr) { // Do nothing here. Subclasses may want to override this. } /** * Called after some content has been removed from this * Document. The default implementation does nothing. * This method is executed within a write lock. * * @param chng the DefaultDocumentEvent describing the change */ protected void postRemoveUpdate(DefaultDocumentEvent chng) { // Do nothing here. Subclasses may want to override this. } /** * Stores a property in this Document'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(); properties.put(key, value); } /** * Blocks until a read lock can be obtained. Must block if there is * currently a writer modifying the Document. */ public final void readLock() { if (currentWriter != null && currentWriter.equals(Thread.currentThread())) return; synchronized (documentCV) { while (currentWriter != null || numWritersWaiting > 0) { try { documentCV.wait(); } catch (InterruptedException ie) { throw new Error("interrupted trying to get a readLock"); } } numReaders++; } } /** * Releases the read lock. If this was the only reader on this * Document, writing may begin now. */ public final 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"); synchronized (documentCV) { // 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 if (currentWriter == null) { numReaders --; if (numReaders == 0 && numWritersWaiting != 0) documentCV.notify(); } } } /** * Removes a piece of content from this Document. * *

If a {@link DocumentFilter} is installed in this document, the * corresponding method of the filter object is called. The * DocumentFilter is called even if length * is zero. This is different from {@link #replace}.

* *

Note: When length is zero or below the call is not * forwarded to the underlying {@link AbstractDocument.Content} instance * of this document and no exception is thrown.

* * @param offset the start offset of the fragment to be removed * @param length the length of the fragment to be removed * * @throws BadLocationException if offset or * offset + length or invalid locations within this * document */ public void remove(int offset, int length) throws BadLocationException { if (documentFilter == null) removeImpl(offset, length); else documentFilter.remove(getBypass(), offset, length); } 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); try { writeLock(); // The order of the operations below is critical! removeUpdate(event); UndoableEdit temp = content.remove(offset, length); postRemoveUpdate(event); fireRemoveUpdate(event); } finally { writeUnlock(); } } } /** * Replaces a piece of content in this Document with * another piece of content. * *

If a {@link DocumentFilter} is installed in this document, the * corresponding method of the filter object is called.

* *

The method has no effect if length is zero (and * only zero) and, at the same time, text is * null or has zero length.

* * @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 offset or * offset + length 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; 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); } void replaceImpl(int offset, int length, String text, AttributeSet attributes) throws BadLocationException { removeImpl(offset, length); insertStringImpl(offset, text, attributes); } /** * Adds a DocumentListener object to this document. * * @param listener the listener to add */ public void addDocumentListener(DocumentListener listener) { listenerList.add(DocumentListener.class, listener); } /** * Removes a DocumentListener object from this document. * * @param listener the listener to remove */ public void removeDocumentListener(DocumentListener listener) { listenerList.remove(DocumentListener.class, listener); } /** * Returns all registered DocumentListeners. * * @return all registered DocumentListeners */ public DocumentListener[] getDocumentListeners() { return (DocumentListener[]) getListeners(DocumentListener.class); } /** * Adds an {@link UndoableEditListener} to this Document. * * @param listener the listener to add */ public void addUndoableEditListener(UndoableEditListener listener) { listenerList.add(UndoableEditListener.class, listener); } /** * Removes an {@link UndoableEditListener} from this Document. * * @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 Document. * The default implementation does nothing but may be overridden by * subclasses to modify the Document structure in response * to a remove request. The method is executed within a write lock. * * @param chng the DefaultDocumentEvent describing the change */ protected void removeUpdate(DefaultDocumentEvent chng) { // Do nothing here. Subclasses may wish to override this. } /** * Called to render this Document visually. It obtains a read * lock, ensuring that no changes will be made to the document * during the rendering process. It then calls the {@link Runnable#run()} * method on runnable. This method must not attempt * to modifiy the Document, 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 Document. * A value of -1 indicates that this Document * should be loaded synchronously. * * @param p the asynchronous loading priority to set */ public void setAsynchronousLoadPriority(int p) { // TODO: Implement this properly. } /** * Sets the properties of this Document. * * @param p the document properties to set */ public void setDocumentProperties(Dictionary 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 final void writeLock() { if (currentWriter != null && currentWriter.equals(Thread.currentThread())) return; synchronized (documentCV) { numWritersWaiting++; while (numReaders > 0) { try { documentCV.wait(); } catch (InterruptedException ie) { throw new Error("interruped while trying to obtain write lock"); } } numWritersWaiting --; currentWriter = Thread.currentThread(); } } /** * Releases the write lock. This allows waiting readers or writers to * obtain the lock. */ protected final void writeUnlock() { synchronized (documentCV) { if (Thread.currentThread().equals(currentWriter)) { currentWriter = null; documentCV.notifyAll(); } } } /** * Returns the currently installed {@link DocumentFilter} for this * Document. * * @return the currently installed {@link DocumentFilter} for this * Document * * @since 1.4 */ public DocumentFilter getDocumentFilter() { return documentFilter; } /** * Sets the {@link DocumentFilter} for this Document. * * @param filter the DocumentFilter to set * * @since 1.4 */ public void setDocumentFilter(DocumentFilter filter) { this.documentFilter = filter; } /** * Dumps diagnostic information to the specified PrintStream. * * @param out the stream to write the diagnostic information to */ public void dump(PrintStream out) { ((AbstractElement) getDefaultRootElement()).dump(out, 0); } /** * Defines a set of methods for managing text attributes for one or more * Documents. * * Replicating {@link AttributeSet}s throughout a Document can * be very expensive. Implementations of this interface are intended to * provide intelligent management of AttributeSets, eliminating * costly duplication. * * @see StyleContext */ public interface AttributeContext { /** * Returns an {@link AttributeSet} that contains the attributes * of old plus the new attribute specified by * name and value. * * @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 old plus the new attributes in attributes. * * @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 old plus the new attributes in * attributes */ 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 attributes 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 * name removed from old. * * @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 old minus the attribute * specified by name */ AttributeSet removeAttribute(AttributeSet old, Object name); /** * Removes all attributes in attributes from old * and returns the resulting AttributeSet. * * @param old the set of attributes from which to remove attributes * @param attributes the attributes to be removed from old * * @return the attributes of old minus the attributes in * attributes */ AttributeSet removeAttributes(AttributeSet old, AttributeSet attributes); /** * Removes all attributes specified by names from * old and returns the resulting AttributeSet. * * @param old the set of attributes from which to remove attributes * @param names the names of the attributes to be removed from * old * * @return the attributes of old minus the attributes in * attributes */ AttributeSet removeAttributes(AttributeSet old, Enumeration names); } /** * A sequence of data that can be edited. This is were the actual content * in AbstractDocument's is stored. */ public interface Content { /** * Creates a {@link Position} that keeps track of the location at * offset. * * @return a {@link Position} that keeps track of the location at * offset. * * @throw BadLocationException if offset is not a valid * location in this Content 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 UndoableEdit or null if undo is * not supported by this Content model * * @throws BadLocationException if where is not a valid * location in this Content 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 UndoableEdit or null if undo is * not supported by this Content model * * @throws BadLocationException if where is not a valid * location in this Content 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 offset or * offset + lenis not a valid * location in this Content model */ String getString(int where, int len) throws BadLocationException; /** * Fetches a piece of content and stores it in txt. * * @param where the start offset of the requested fragment * @param len the length of the requested fragment * @param txt the Segment where to fragment is stored into * * @throws BadLocationException if offset or * offset + lenis not a valid * location in this Content 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 AbstractElement with a * specified parent Element and AttributeSet. * * @param p the parent of this AbstractElement * @param s the attributes to be assigned to this * AbstractElement */ public AbstractElement(Element p, AttributeSet s) { element_parent = p; AttributeContext ctx = getAttributeContext(); attributes = ctx.getEmptySet(); if (s != null) attributes = ctx.addAttributes(attributes, s); } /** * Returns the child nodes of this Element as an * Enumeration of {@link TreeNode}s. * * @return the child nodes of this Element as an * Enumeration of {@link TreeNode}s */ public abstract Enumeration children(); /** * Returns true if this AbstractElement * allows children. * * @return true if this AbstractElement * allows children */ public abstract boolean getAllowsChildren(); /** * Returns the child of this AbstractElement at * index. * * @param index the position in the child list of the child element to * be returned * * @return the child of this AbstractElement at * index */ public TreeNode getChildAt(int index) { return (TreeNode) tree_children.get(index); } /** * Returns the number of children of this AbstractElement. * * @return the number of children of this AbstractElement */ public int getChildCount() { return tree_children.size(); } /** * Returns the index of a given child TreeNode or * -1 if node is not a child of this * AbstractElement. * * @param node the node for which the index is requested * * @return the index of a given child TreeNode or * -1 if node is not a child of this * AbstractElement */ public int getIndex(TreeNode node) { return tree_children.indexOf(node); } /** * Returns the parent TreeNode of this * AbstractElement or null if this element * has no parent. * * @return the parent TreeNode of this * AbstractElement or null if this * element has no parent */ public TreeNode getParent() { return tree_parent; } /** * Returns true if this AbstractElement is a * leaf element, false otherwise. * * @return true if this AbstractElement is a * leaf element, false 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 true 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 true if this element contains the specified * attribute */ public boolean containsAttribute(Object name, Object value) { return attributes.containsAttribute(name, value); } /** * Returns true if this element contains all of the * specified attributes. * * @param attrs the attributes to check * * @return true 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 key of null * if key 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 true if an attribute with the specified name * is defined in this element, false otherwise. * * @param attrName the name of the requested attributes * * @return true if an attribute with the specified name * is defined in this element, false otherwise */ public boolean isDefined(Object attrName) { return attributes.isDefined(attrName); } /** * Returns true if the specified AttributeSet * is equal to this element's AttributeSet, false * otherwise. * * @param attrs the attributes to compare this element to * * @return true if the specified AttributeSet * is equal to this element's AttributeSet, * false 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 index. * * @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) getAttribute(NameAttribute); } /** * 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) { StringBuffer b = new StringBuffer(); 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'); } } 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 * Elements that contain other Elements. */ 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; /** * Creates a new BranchElement with the specified * parent and attributes. * * @param parent the parent element of this BranchElement * @param attributes the attributes to set on this * BranchElement */ public BranchElement(Element parent, AttributeSet attributes) { super(parent, attributes); children = new Element[1]; numChildren = 0; } /** * Returns the children of this BranchElement. * * @return the children of this BranchElement */ public Enumeration children() { if (children.length == 0) return null; Vector tmp = new Vector(); for (int index = 0; index < numChildren; ++index) tmp.add(children[index]); return tmp.elements(); } /** * Returns true since BranchElements allow * child elements. * * @return true since BranchElements allow * child elements */ public boolean getAllowsChildren() { return true; } /** * Returns the child element at the specified index. * * @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) { // If offset is less than the start offset of our first child, // return 0 if (offset < getStartOffset()) return 0; // XXX: There is surely a better algorithm // as beginning from first element each time. for (int index = 0; index < numChildren - 1; ++index) { Element elem = children[index]; if ((elem.getStartOffset() <= offset) && (offset < elem.getEndOffset())) return index; // If the next element's start offset is greater than offset // then we have to return the closest Element, since no Elements // will contain the offset if (children[index + 1].getStartOffset() > offset) { if ((offset - elem.getEndOffset()) > (children[index + 1].getStartOffset() - offset)) return index + 1; else return index; } } // If offset is greater than the index of the last element, return // the index of the last element. return getElementCount() - 1; } /** * 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 NullPointerException. * * @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 NullPointerException. * * @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 false since BranchElement are no * leafes. * * @return false since BranchElement are no * leafes */ public boolean isLeaf() { return false; } /** * Returns the Element at the specified Document * offset. * * @return the Element at the specified Document * 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 Document is beeing modified. */ public class DefaultDocumentEvent extends CompoundEdit implements DocumentEvent { /** The serialization UID (compatible with JDK1.5). */ private static final long serialVersionUID = 5230037221564563284L; /** 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 Element to their change records. */ Hashtable changes; /** * Indicates if this event has been modified or not. This is used to * determine if this event is thrown. */ boolean modified; /** * Creates a new DefaultDocumentEvent. * * @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; changes = new Hashtable(); modified = false; } /** * Adds an UndoableEdit to this DocumentEvent. 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) { // XXX - Fully qualify ElementChange to work around gcj bug #2499. if (edit instanceof DocumentEvent.ElementChange) { modified = true; DocumentEvent.ElementChange elEdit = (DocumentEvent.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 elem or null if * elem has not been changed */ public DocumentEvent.ElementChange getChange(Element elem) { // XXX - Fully qualify ElementChange to work around gcj bug #2499. return (DocumentEvent.ElementChange) changes.get(elem); } /** * 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 ElementEdit. * * @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 LeafElement. * * @param parent the parent of this LeafElement * @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 null since LeafElements cannot have * children. * * @return null since LeafElements cannot have * children */ public Enumeration children() { return null; } /** * Returns false since LeafElements cannot have * children. * * @return false since LeafElements cannot have * children */ public boolean getAllowsChildren() { return false; } /** * Returns null since LeafElements cannot have * children. * * @return null since LeafElements cannot have * children */ public Element getElement(int index) { return null; } /** * Returns 0 since LeafElements cannot have * children. * * @return 0 since LeafElements cannot have * children */ public int getElementCount() { return 0; } /** * Returns -1 since LeafElements cannot have * children. * * @return -1 since LeafElements cannot have * children */ public int getElementIndex(int offset) { return -1; } /** * Returns the end offset of this Element inside the * document. * * @return the end offset of this Element inside the * document */ public int getEndOffset() { return endPos.getOffset(); } /** * Returns the name of this Element. This is * {@link #ContentElementName} in this case. * * @return the name of this Element */ public String getName() { String name = super.getName(); if (name == null) name = ContentElementName; return name; } /** * Returns the start offset of this Element inside the * document. * * @return the start offset of this Element inside the * document */ public int getStartOffset() { return startPos.getOffset(); } /** * Returns true. * * @return true */ public boolean isLeaf() { return true; } /** * Returns a string representation of this Element. * * @return a string representation of this Element */ public String toString() { return ("LeafElement(" + getName() + ") " + getStartOffset() + "," + getEndOffset() + "\n"); } } /** 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); } } }