[jdom-interest] Child-Parent patch - Ideas.

Bradley S. Huffman hip at cs.okstate.edu
Thu Feb 5 18:24:03 PST 2004


Jason Hunter writes:

> Rolf sent in this change a while back, and I just integrated it with the 
> latest CVS code but haven't checked it in.  I'm curious what other 
> people think of it as an approach.  Rolf's description below explains 
> its justification.
> 
> I invite people to compare the CVS HEAD (get it from CVS or at 
> http://jdom.org/head.zip) versus the source at http://jdom.org/rolf.zip. 
>   Comments welcome.  Which way should we go?

Honestly, I've grown to really dislike ContentList and AttributeList (and I
wrote a large portion of them). They are large and difficult to understand,
so anything to clean them up, or better yet elminate them, would be a good
thing IMHO. At the bottom is a abstract implementation of Parent that I
had for a while that gets rid of ContentList. Element (not included) is
similar in it's treatment of attributes).

One of the other goals I was working on is a good subclassing policy. For
example methods that access content should be in a abstract class and
based on getContent(int), addContent(int, Child), removeContent(int).
Also I wonder if it would be usefull to allow subclassers to provide
any implementation of List for content, attributes, and namespace
declarations?  As seen in AbstractParent below it's doable, but is it
usefull? More stuff for people to consider :)

> Rolf Lear wrote:
> 
> > I have had a look at the Child/Parent thing.
> > 
> > Personally, I don't think the Idea has been taken far enough, so I 
> > played around with the concept, and "normalised" some of the redundancies.
> > 
> > Firstly, I converted the Child interface into an Abstract class that 
> > deals with ALL Parent/child relationships for the Child role, including 
> > detaching, cloning, set/getParent (and holds the parent instance field).
> > 
> > I also implemented getDocument at the Child Class level (all children 
> > have the same mechanism for getting the Document).
> > 
> > Next, I added the method "getDocument" to the Parent Interface... and 
> > parent can getDocument, including the Document class which has "return 
> > this;" in the getDocument().

Here's a version I've had sitting on my hard, but it's pretty boring.
AbstractParent below is more interesting.

abstract class AbstractChild implements Child {

    /** Parent of this child, or null if none. */
    protected Parent parent;

    /** Additional properties on this child. */
    protected Map properties;

    /**
     * Protected constructor is provided in order to support a subclass
     * that wants full control over variable initialization. It intentionally
     * leaves all instance variables null, allowing a lightweight subclass
     * implementation. The subclass is responsible for ensuring all the
     * get and set methods on Element behave as documented.
     * <p>
     * When implementing a subclass which doesn't require full control over
     * variable initialization, be aware that simply calling super() (or
     * letting the compiler add the implicit super() call) will not initialize
     * the instance variables which will cause many of the methods to throw a
     * NullPointerException. Therefore, the constructor for these subclasses
     * should call one of the public constructors so variable initialization
     * is handled automatically.
     * </p>
     */
    protected AbstractChild() { }

    public final Child detach() {
        Parent ancestor = getParent();
        if (ancestor != null) {
            ancestor.removeContent(this);
        }
        return this;
    }

    public Document getDocument() {
        Parent ancestor = getParent();
        if (ancestor instanceof Element) {
            return ((Element) ancestor).getDocument();
        }
        else if (ancestor instanceof Document) {
            return (Document) ancestor;
        }
        return null;
    }

    public Parent getParent() {
        return parent;
    }

    public Object clone() {
        AbstractChild child = null;

        try {
            child = (AbstractChild) super.clone();
        }
        catch (CloneNotSupportedException exception) {
            // Can't happen
        }

        child.setParent(null);
        return child;
    }

    public final boolean equals(Object obj) {
        return (this == obj);
    }

    public int hashCode() {
        return super.hashCode();
    }

    protected Child setParent(Parent parent) {
        this.parent = parent;
        return this;
    }

    public void setProperty(String name, Object value) {
        if (properties == null) {
            properties = new HashMap();
        }
        properties.put(name, value);
    }

    public Object getProperty(String name) {
        return properties.get(name);
    }
}

> > Finally, I changed the ContentList class substantially. elementData is 
> > now called childData, and is of type Child[] instead of Object[]. The 
> > ContentList owner is now of type Parent instead of Object. I have added 
> > a method canContain to the Parent interface, and thus the actual Parent 
> > object determines what content is allowed in the contentlist, so instead 
> > of add(int,Text), add(int,Element), add(int,CDATA), 
> > add(int,ProcessingInstruction), etc, there is just add(int, Child).
> > 
> > In doing all of the above, I have cut large amounts of 
> > redundant/duplicated code, simplified the relationships, and thrown away 
> > "special cases". The only downside I can see in terms of "functionality" 
> > is that some of the exceptions are thrown with less specialised messages...
> > 
> > Please have a look, and comment on the concept. Note, that this is only 
> > possible by converting Child to an abstract class instead of an interface.

Here's a AbstractParent to consider.

abstract class AbstractParent implements Parent {

    private static final String CVS_ID =
    "@(#) $RCSfile: $ $Revision: $ $Date: $ $Name: $";

    /** Content of this parent. */
    protected List content;

    // Wrapped version of content.
    private transient List wrappedList;

    /** Additional properties on this parent. */
    protected Map properties;

    /**
     * Protected constructor is provided in order to support a subclass
     * that wants full control over variable initialization. It intentionally
     * leaves all instance variables null, allowing a lightweight subclass
     * implementation. The subclass is responsible for ensuring all the
     * get and set methods on Element behave as documented.
     * <p>
     * When implementing a subclass which doesn't require full control over
     * variable initialization, be aware that simply calling super() (or
     * letting the compiler add the implicit super() call) will not initialize
     * the instance variables which will cause many of the methods to throw a
     * NullPointerException. Therefore, the constructor for these subclasses
     * should call one of the public constructors so variable initialization
     * is handled automatically.
     * </p>
     */
    protected AbstractParent() { }

    public int getContentSize() {
        return (content == null) ? 0 : content.size();
    }

    public int indexOf(Child child) {
        return (content == null) ? -1 : content.indexOf(child);
    }

    public Parent addContent(int index, Child child) {
        int size = getContentSize();
        if ((index < 0) || (index > size)) {
            throw new IndexOutOfBoundsException("Index: " + index +
                                                "Size: " + size);
        }

        if (child == null) {
            throw new IllegalAddException("Null content not allowed");
        }

        if (child.getParent() != null) {
            throw new IllegalAddException("Child already attached to " +
                                          "another parent");
        }

        
        if (content == null) {
            content = createContentList();
        }

        content.add(index, child);
        setParentage(child, this);

        return this;
    }

    public Parent addContent(Child child) {
        return addContent(getContentSize(), child);
    }

    public Parent addContent(Collection collection) {
        return addContent(getContentSize(), collection);
    }

    public Parent addContent(int index, Collection collection) {
        if (collection != null) {
            int count = 0;
            try {
                Iterator i = collection.iterator();
                while (i.hasNext()) {
                    addContent(index + count, i.next());
                    count++;
                }
            }
            catch (RuntimeException exception) {
                for (int i = 0; i < count; i++) {
                    AbstractParent.this.removeContent(index);
                }
                throw exception;
            }
        }
        return this;
    }

    public List cloneContent() {
        List copy = new ArrayList(getContentSize());
        int size = getContentSize();
        for (int i = 0; i < size; i++) {
            Child child = getContent(i);
            copy.add(child.clone());
        }
        return copy;
    }

    public Child getContent(int index) {
        if (content == null) {
            throw new IndexOutOfBoundsException("Size: 0 Index: " + index);
        }
        return (Child) content.get(index);
    }

    public List getContent() {
        if (wrappedList == null) {
            wrappedList = new ContentList();
        }
        return wrappedList;
    }

    public List getContent(Filter filter) {
        return new FilterContentList(filter);
    }

    public boolean removeContent(Child child) {
        int index = indexOf(child);
        if (index < 0) {
            return false;
        }
        removeContent(index);
        return true;
    }

    public Child removeContent(int index) {
        if (content == null) {
            throw new IndexOutOfBoundsException("Size: 0 Index: " + index);
        }

        Child child = (Child) content.remove(index);
        setParentage(child, null);

        return child;
    }

    public List removeContent() {
        //XXX Put in try/catch
        List old = new ArrayList(getContentSize());
        int size = getContentSize();
        for (int i = 0; i < size; i++) {
            Child child = removeContent(0);
            old.add(child);
        }
        return old;
    }

    public List removeContent(Filter filter) {
        //XXX violates JDOMs restore policy
        List old = new ArrayList();
        Iterator i = new FilterContentIterator(filter);
        while(i.hasNext()) {
            old.add(i.next());
            i.remove();
        }
        return old;
    }

    public Parent setContent(Child child) {
        List old = removeContent();
        try {
            addContent(child);
        }
        catch(RuntimeException exception) {
            restoreContent(old);
            throw exception;
        }
        return this;
    }

    public Parent setContent(Collection collection) {
        List old = removeContent();
        try {
            addContent(collection);
        }
        catch(RuntimeException exception) {
            // Reset parentage of any children in the collection
            // that have been add to content list.
            setParentage(content, null);
            // Now restore old content.
            restoreContent(old);
        }
        return this;
    }

    public Parent setContent(int index, Child child) {
        //XXX could throw a exception
        Child old = removeContent(index);
        try {
            addContent(index, child);
        }
        catch(RuntimeException exception) {
            if (old != null) {
                content.add(index, old);
                setParentage(old, this);
            }
            throw exception;
        }
        return this;
    }

    public Parent setContent(int index, Collection collection) {
        //XXX could throw a exception
        Child old = removeContent(index);
        try {
            addContent(index, collection);
        }
        catch(RuntimeException exception) {
            if (old != null) {
                content.add(index, old);
                setParentage(old, this);
            }
            throw exception;
        }
        return this;
    }

    public Iterator getDescendants() {
        return new DescendantIterator(this);
    }

    public Iterator getDescendants(Filter filter) {
        return new FilterIterator(new DescendantIterator(this), filter);
    }

    public void setProperty(String name, Object value) {
        if (properties == null) {
            properties = new HashMap();
        }
        properties.put(name, value);
    }

    public Object getProperty(String name) {
        return properties.get(name);
    }

    public Object clone() {
        AbstractParent parent = null;

        try {
            parent = (AbstractParent) super.clone();
        } catch (CloneNotSupportedException exception) {
            // Can't happen
        }

        // The clone has a reference to this object's content list, so
        // overwrite and add the cloned content to clone.
        if (content != null) {
            Iterator i = content.iterator();
            while(i.hasNext()) {
                Child child = (Child) ((Child) i.next()).clone();
                parent.addContent(child);
            }
        }

        return parent;
    }

    public final boolean equal(Object obj) {
        return (this == obj);
    }

    public int hashCode() {
        return super.hashCode();
    }

    /**
     * Create a new {@link List} for holding JDOM {@link Child} objects.
     */
    protected List createContentList() {
        return new ArrayList(5);
    }

    // Set parentage of every JDOM object in given collection to
    // supplied parent.
    void setParentage(Collection collection, Parent parent) {
        if (collection != null) {
            Iterator i = collection.iterator();
            while(i.hasNext()) {
                setParentage(i.next(), parent);
            }
        }
    }

    // Set parent of given object to supplied parent.
    void setParentage(Object obj, Parent parent) {
        if (obj instanceof Element) {
            ((Element) obj).setParent(parent);
        }
        else if (obj instanceof Text) {
            ((Text) obj).setParent((Element) parent);
        }
        else if (obj instanceof Attribute) {
            ((Attribute) obj).setParent((Element) parent);
        }
        else if (obj instanceof EntityRef) {
            ((EntityRef) obj).setParent((Element) parent);
        }
        else if (obj instanceof Comment) {
            ((Comment) obj).setParent(parent);
        }
        else if (obj instanceof ProcessingInstruction) {
            ((ProcessingInstruction) obj).setParent(parent);
        }
        else if (obj instanceof DocType) {
            ((DocType) obj).setParent((Document) parent);
        }
        else {
            throw new IllegalStateException("Unknown child " +
                                             obj.getClass().getName());
        }
    }

    // Convenice method for implementing List inner classes below.
    private Parent addContent(int index, Object obj) {
        if (obj instanceof Child) {
            addContent(index, (Child) obj);
        }
        else if (obj == null) {
            throw new NullPointerException("Null child not allowed");
        }
        else {
            throw new IllegalAddException(obj.getClass().getName() +
                      " not instance of org.jdom.Child");
        }
        return this;
    }

    // Convenice method for restore the content list to the children
    // in the supplied collection.  Note: don't use addContent in case
    // a subclass is using it to gather statistics.
    void restoreContent(Collection collection) {
        Iterator i = collection.iterator();
        while(i.hasNext()) {
            Object obj = i.next();
            content.add(obj);
            setParentage(obj, this);
        }
    }

    // Convenice method for implementing List inner classes below.
    private Parent setContent(int index, Object obj) {
        if (obj instanceof Child) {
            setContent(index, (Child) obj);
        }
        else if (obj == null) {
            throw new NullPointerException("Null child not allowed");
        }
        else {
            throw new IllegalAddException(obj.getClass().getName() +
                      " not instance of org.jdom.Child");
        }
        return this;
    }

    // Convenice method for implementing List inner classes below.
    // Return the real index of the nth child that matches the supplied
    // filter, or content size if less than n content exist.
    private int findRealIndex(Filter filter, int n) {
        int count = 0; // Count of objects matching filter in content.
        int size  = 0; // Count of all objects in content.
        if (content != null) {
            Iterator i = content.iterator();
            while(i.hasNext()) {
                if (filter.matches(i.next())) {
                    if (count == n) {
                        return size;
                    }
                    count++;
                }
                size++;
            }
        }
        return (count == n) ? size : size + 1;
    }
 
    ////////////////////////////////////////////////
    // Implementation of inner class ContentList //
    ////////////////////////////////////////////////

    /**
     * Provides a java.util.List view of this parent's content.
     */
    final class ContentList extends AbstractList {

        public ContentList() { }

        public void add(int index, Object obj) {
            addContent(index, obj);
        }

        public boolean addAll(Collection collection) {
            int oldSize = getContentSize();
            addContent(collection);
            return oldSize != getContentSize();
        }

        public boolean addAll(int index, Collection collection) {
            int oldSize = getContentSize();
            addContent(index, collection);
            return oldSize != getContentSize();
        }

        public void clear() {
            removeContent();
        }

        public Object get(int index) {
            return getContent(index);
        }

        public Object remove(int index) {
            return removeContent(index);
        }

        public Object set(int index, Object obj) {
            return setContent(index, obj);
        }

        public int size() {
            return getContentSize();
        }
    } /* ContentList */

    /////////////////////////////////////////////////////
    // Implementation of inner class FilterContentList //
    /////////////////////////////////////////////////////

    /**
     * Provides a filtered java.util.List view of this parent's content.
     */
    final class FilterContentList extends AbstractSequentialList {

        /** The Filter that applies to this list. */
        protected Filter filter;

        /**
         * Create a new instance of the FilterContentList with
         * the specified Filter.
         */
        protected FilterContentList(Filter filter) {
            this.filter = filter;
        }

        public void add(int index, Object obj) {
            //XXX as long as the supplied object is a valid child for this
            //XXX parent do we really care if it matches our filter
            if ( !filter.matches(obj)) {
                throw new IllegalAddException("Filter won't allow " +
                          (obj.getClass()).getName() +
                          " to be added at index " + index);
            }

            addContent(findRealIndex(filter, index), obj);
        }

        public boolean addAll(Collection collection) {
            int oldSize = getContentSize();
            //XXX do we need to check if every JDOM child
            //XXX in collection matches our filter
            addContent(collection);
            return oldSize != getContentSize();
        }

        public boolean addAll(int index, Collection collection) {
            int oldSize = getContentSize();
            //XXX do we need to check if every JDOM child
            //XXX in collection matches our filter
            addContent(findRealIndex(filter, index), collection);
            return oldSize != getContentSize();
        }

        public void clear() {
            removeContent(filter);
        }

        public Object get(int index) {
            return getContent(findRealIndex(filter, index));
        }

        public Object remove(int index) {
            return removeContent(findRealIndex(filter, index));
        }

        public Object set(int index, Object obj) {
            //XXX as long as the supplied object is a valid child for this
            //XXX parent do we really care if it matches our filter
            if ( !filter.matches(obj)) {
                throw new IllegalAddException("Filter won't allow " +
                          (obj.getClass()).getName() +
                          " to be set at index " + index);
            }

            return setContent(findRealIndex(filter, index), obj);
        }

        public int size() {
            int count = 0;
            Iterator i = new FilterContentIterator(filter, 0);
            while(i.hasNext()) {
                i.next();
                count++;
            }
            return count;
        }

        public Iterator iterator() {
            return new FilterContentIterator(filter, 0);
        }

        public ListIterator listIterator() {
            return new FilterContentIterator(filter, 0);
        }

        public ListIterator listIterator(int index) {
            return new FilterContentIterator(filter,  index);
        }
    } /* FilterContentList */

    /////////////////////////////////////////////////////////
    // Implementation of inner class FilterContentIterator //
    /////////////////////////////////////////////////////////

    /**
     * Implementation of java.util.ListIterator backed by
     * this  {@link AbstractParent} and filtered using the
     * supplied {@link Filter}.
     */
    final class FilterContentIterator implements ListIterator {

        // Filter to apply.
        private Filter filter;

        // Real index in this.content of previous child.
        private int previous = -1;

        // Real index in this.content of next child.
        private int next = 0;
    
        // Real index in this.content of last child returned. Set to
        // to -1 if child is removed.
        private int last = -1;
    
        // Next index based on filtering.
        private int filteredIndex;

        public FilterContentIterator(Filter filter) {
            this(filter, 0);
        }

        public FilterContentIterator(Filter filter, int start) {
            if (start < 0) {
                throw new IndexOutOfBoundsException("Index: " + start);
            }

            this.filter = filter;
            this.previous = -1;
            this.next = nextIndex(0, filter);

            int size = AbstractParent.this.getContentSize();
            while((next >= 0) && (next < size)) {
                if (filteredIndex == start) {
                    return;
                }
                previous = next;
                next = nextIndex(next + 1, filter);
                filteredIndex++;
            }

            if (start > next) {
                throw new IndexOutOfBoundsException("Index: " + start +
                                                    " Size: " + next);
            }
        }

        public boolean hasPrevious() {
            return (previous >= 0) &&
                   (previous < AbstractParent.this.getContentSize());
        }
    
        public Object previous() {
            if ((previous < 0) ||
                (previous >= AbstractParent.this.getContentSize())) {
                     throw new IllegalStateException("previous=" + previous);
            }

            last = previous;
            next = previous;
            previous = previousIndex(previous - 1, filter);
            --filteredIndex;
            return AbstractParent.this.getContent(last);
        }
    
        public boolean hasNext() {
            return (next >= 0) &&
                   (next < AbstractParent.this.getContentSize());
        }
    
        public Object next() {
            if ((next < 0) ||
                (next >= AbstractParent.this.getContentSize())) {
                    throw new IllegalStateException("next=" + next);
            }

            previous = next;
            last = next;
            next = nextIndex(next + 1, filter);
            filteredIndex++;
            return AbstractParent.this.getContent(last);
        }
    
        public int previousIndex() {
            return filteredIndex - 1;
        }
    
        public int nextIndex() {
            return filteredIndex;
        }
    
        public void add(Object obj) {
            if ( !filter.matches(obj)) {
                throw new IllegalAddException("Filter won't allow " +
                                              (obj.getClass()).getName() +
                                              " to be added");
            }
            AbstractParent.this.addContent(next, obj);
            next++;
            filteredIndex++;
            last = -1;
        }
    
        public void remove() {
            if ((last < 0) ||
                (last >= AbstractParent.this.getContentSize())) {
                    throw new IllegalStateException("last=" + last);
            }

            AbstractParent.this.removeContent(last);
            previous = previousIndex(last - 1, filter);
            --next;
            --filteredIndex;
            last = -1;
        }
    
        public void set(Object obj) {
            if ((last < 0) ||
                (last >= AbstractParent.this.getContentSize())) {
                    throw new IllegalStateException("last=" + last);
            }

            if ( !filter.matches(obj)) {
                throw new IllegalAddException("Filter won't allow " +
                                              (obj.getClass()).getName() +
                                              " to be added");
            }

            setContent(last, obj);
            --filteredIndex;
        }

        // Starting at the given index (inclusive) return the index of
        // previous child that matches the supplied filter, or -1
        // if not such child exists.
        private int previousIndex(int index, Filter filter) {
            int size = AbstractParent.this.getContentSize();
            while((index >= 0) && (index < size)) {
                Child child = AbstractParent.this.getContent(index);
                if (filter.matches(child)) {
                    return index;
                }
                --index;
            }
            return -1;
        }

        // Starting at the given index (inclusive) return the index of
        // next child that matches the supplied filter, or the content
        // size if not such child exists.
        private int nextIndex(int index, Filter filter) {
            int size = AbstractParent.this.getContentSize();
            while((index >= 0) && (index < size)) {
                Child child = AbstractParent.this.getContent(index);
                if (filter.matches(child)) {
                    return index;
                }
                index++;
            }
            return size;
        }
    } /* FilterContentIterator */
}



More information about the jdom-interest mailing list