You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by jk...@apache.org on 2006/12/11 22:34:23 UTC

svn commit: r485882 - in /tapestry/tapestry4/trunk/tapestry-framework/src: java/org/apache/tapestry/ java/org/apache/tapestry/callback/ java/org/apache/tapestry/engine/ java/org/apache/tapestry/markup/ test/org/apache/tapestry/markup/

Author: jkuhnert
Date: Mon Dec 11 13:34:22 2006
New Revision: 485882

URL: http://svn.apache.org/viewvc?view=rev&rev=485882
Log:
Resolves TAPESTRY-550 and TAPESTRY-975. 

Refactored IMakupWriter to be able to handle overwriting existing attributes as well as the slew of new options 
available when attributes are cached while a tag is open - like append/has/remove/clear.

Added:
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/Attribute.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/DefaultAttribute.java
Modified:
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/AbstractPage.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IExternalPage.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IMarkupWriter.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IPage.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/callback/ExternalCallback.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/ExternalService.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/NullWriter.java
    tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/MarkupWriterImpl.java
    tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/markup/TestMarkupWriter.java

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/AbstractPage.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/AbstractPage.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/AbstractPage.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/AbstractPage.java Mon Dec 11 13:34:22 2006
@@ -494,10 +494,10 @@
     {
         if (_listenerList == null)
             return;
-
+        
         PageEvent event = null;
         Object[] listeners = _listenerList.getListenerList();
-
+        
         for (int i = 0; i < listeners.length; i += 2)
         {
             if (listeners[i] == PageValidateListener.class)

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IExternalPage.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IExternalPage.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IExternalPage.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IExternalPage.java Mon Dec 11 13:34:22 2006
@@ -17,7 +17,7 @@
 /**
  * Defines a page which may be referenced externally via a URL using the
  * {@link org.apache.tapestry.engine.ExternalService}. External pages may be bookmarked via their
- * URL for latter display. See the {@link org.apache.tapestry.link.ExternalLink}for details on how
+ * URL for latter display. See the {@link org.apache.tapestry.link.ExternalLink} for details on how
  * to invoke <tt>IExternalPage</tt>s.
  * 
  * @see org.apache.tapestry.callback.ExternalCallback

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IMarkupWriter.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IMarkupWriter.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IMarkupWriter.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IMarkupWriter.java Mon Dec 11 13:34:22 2006
@@ -14,6 +14,8 @@
 
 package org.apache.tapestry;
 
+import org.apache.tapestry.markup.Attribute;
+
 /**
  * Defines an object that can write markup (XML, HTML, XHTML) style output. A
  * <code>IMarkupWriter</code> handles translation from unicode to the markup language (escaping
@@ -66,7 +68,96 @@
      */
 
     void attributeRaw(String name, String value);
+    
+    /**
+     * Appends an integer attribute to the current attribute with a matching 
+     * <code>name</code> key, if one exists. 
+     * 
+     * @throws IllegalStateException
+     *             if there is no open tag.
+     */
+
+    void appendAttribute(String name, int value);
+
+    /**
+     * Appends a boolean attribute into the currently open tag.
+     * 
+     * @throws IllegalStateException
+     *             if there is no open tag.
+     * @since 3.0
+     */
+
+    void appendAttribute(String name, boolean value);
+
+    /**
+     * Appends an attribute into the most recently opened tag. This must be called after
+     * {@link #begin(String)} and before any other kind of writing (which closes the tag).
+     * <p>
+     * The value may be null.
+     * 
+     * @throws IllegalStateException
+     *             if there is no open tag.
+     */
+
+    void appendAttribute(String name, String value);
 
+    /**
+     * Similar to {@link #attribute(String, String)} but no escaping of invalid elements is done for
+     * the value.
+     * 
+     * @throws IllegalStateException
+     *             if there is no open tag.
+     * @since 3.0
+     */
+
+    void appendAttributeRaw(String name, String value);
+    
+    /**
+     * Checks if the current tag has an attribute keyed off of <code>name</code>.
+     * 
+     * @param name
+     *          The name of the attribute to check for existance of.
+     * @return 
+     *          True if the attribute exists, false otherwise.
+     * @throws IllegalStateException
+     *             If there is no open tag.
+     */
+    boolean hasAttribute(String name);
+    
+    /**
+     * Gets the attribute matching <code>name</code> from the current open
+     * tag, if it exists.
+     * 
+     * @param name
+     *          The attribute to get the value of by name.
+     * @return 
+     *          The attribute value, or null if it doesn't exist.
+     * @throws IllegalStateException
+     *             If there is no open tag.
+     */
+    Attribute getAttribute(String name);
+    
+    /**
+     * Removes the attribute specified with a matching <code>name</code> if 
+     * one exists.
+     * 
+     * @param name
+     *          The attribute to remove.
+     * @return
+     *          The removed attribute, null if one didn't exist.
+     * @throws IllegalStateException
+     *             If there is no open tag.
+     */
+    Attribute removeAttribute(String name);
+    
+    /**
+     * Removes all current attributes on the open tag, if any.
+     * 
+     * @throws IllegalStateException
+     *             If there is no open tag.
+     */
+    void clearAttributes();
+    
     /**
      * Closes any existing tag then starts a new element. The new element is pushed onto the active
      * element stack.

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IPage.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IPage.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IPage.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/IPage.java Mon Dec 11 13:34:22 2006
@@ -183,7 +183,7 @@
      * redirect the user to an appropriate part of the system (such as, a login page).
      * <p>
      * Since 3.0, it is easiest to not override this method, but to implement the
-     * {@link PageValidateListener}interface instead.
+     * {@link PageValidateListener} interface instead.
      */
 
     void validate(IRequestCycle cycle);

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/callback/ExternalCallback.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/callback/ExternalCallback.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/callback/ExternalCallback.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/callback/ExternalCallback.java Mon Dec 11 13:34:22 2006
@@ -32,17 +32,6 @@
  * the user and then invokes {@link ICallback#performCallback(IRequestCycle)} to the External page.
  * 
  * <pre>
- * 
- *  
- *   
- *    
- *     
- *      
- *       
- *        
- *         
- *          
- *           
  *             public class External extends BasePage implements IExternalPage {
  *            
  *                 private Integer _itemId;
@@ -87,17 +76,6 @@
  *                     }
  *                 }
  *             }    
- *             
- *           
- *          
- *         
- *        
- *       
- *      
- *     
- *    
- *   
- *  
  * </pre>
  * 
  * @see org.apache.tapestry.IExternalPage
@@ -153,9 +131,9 @@
         try
         {
             IExternalPage page = (IExternalPage) cycle.getPage(_pageName);
-
+            
             cycle.activate(page);
-
+            
             page.activateExternalPage(_parameters, cycle);
         }
         catch (ClassCastException ex)

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/ExternalService.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/ExternalService.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/ExternalService.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/ExternalService.java Mon Dec 11 13:34:22 2006
@@ -151,15 +151,14 @@
                     rawPage,
                     IExternalPage.class), rawPage, null, ex);
         }
-
+        
         Object[] parameters = _linkFactory.extractListenerParameters(cycle);
-
+        
         cycle.setListenerParameters(parameters);
-
+        
         cycle.activate(page);
-
-        page.activateExternalPage(parameters, cycle);
         
+        page.activateExternalPage(parameters, cycle);
         
         _responseRenderer.renderResponse(cycle);
     }

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/NullWriter.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/NullWriter.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/NullWriter.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/engine/NullWriter.java Mon Dec 11 13:34:22 2006
@@ -16,6 +16,7 @@
 
 import org.apache.tapestry.IMarkupWriter;
 import org.apache.tapestry.NestedMarkupWriter;
+import org.apache.tapestry.markup.Attribute;
 
 /**
  * A {@link IMarkupWriter}that does absolutely <em>nothing</em>; this is used during the rewind
@@ -133,25 +134,50 @@
     public void attribute(String name, String value)
     {
     }
-
-    /**
-     * @see org.apache.tapestry.IMarkupWriter#attribute(java.lang.String, boolean)
-     * @since 3.0
-     */
-
+    
     public void attribute(String name, boolean value)
     {
     }
-
-    /**
-     * @see org.apache.tapestry.IMarkupWriter#attributeRaw(java.lang.String, java.lang.String)
-     * @since 3.0
-     */
-
+    
     public void attributeRaw(String name, String value)
     {
     }
-
+    
+    public void appendAttribute(String name, boolean value)
+    {
+    }
+    
+    public void appendAttribute(String name, int value)
+    {
+    }
+    
+    public void appendAttribute(String name, String value)
+    {
+    }
+    
+    public void appendAttributeRaw(String name, String value)
+    {
+    }
+    
+    public Attribute getAttribute(String name)
+    {
+        return null;
+    }
+    
+    public boolean hasAttribute(String name)
+    {
+        return false;
+    }
+    
+    public Attribute removeAttribute(String name)
+    {
+        return null;
+    }
+    
+    public void clearAttributes()
+    {
+    }
+    
     public void print(char[] data, int offset, int length, boolean raw)
     {
     }

Added: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/Attribute.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/Attribute.java?view=auto&rev=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/Attribute.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/Attribute.java Mon Dec 11 13:34:22 2006
@@ -0,0 +1,41 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry.markup;
+
+import org.apache.tapestry.IMarkupWriter;
+
+
+/**
+ * Represents a DOM style attribute that is used by {@link IMarkupWriter} to 
+ * manage rendering attributes.
+ * 
+ * @author jkuhnert
+ */
+public interface Attribute
+{
+    /**
+     * Retrieves the current value for the attribute.
+     * 
+     * @return The current value for the attribute.
+     */
+    Object getValue();
+    
+    /**
+     * Whether or not this attribute should be written out in raw form 
+     * as specified by {@link IMarkupWriter#attribute(String, boolean)} .
+     * 
+     * @return True if content will be written in raw form, false otherwise.
+     */
+    boolean isRaw();
+}

Added: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/DefaultAttribute.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/DefaultAttribute.java?view=auto&rev=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/DefaultAttribute.java (added)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/DefaultAttribute.java Mon Dec 11 13:34:22 2006
@@ -0,0 +1,81 @@
+// Copyright 2004, 2005 The Apache Software Foundation
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package org.apache.tapestry.markup;
+
+import java.io.PrintWriter;
+
+
+/**
+ * Used to hold markup attribute data for writing in a specific format.
+ * 
+ * @author jkuhnert
+ */
+public class DefaultAttribute implements Attribute
+{
+    private String _value;
+    private boolean _raw;
+    
+    public DefaultAttribute(String value, boolean raw)
+    {
+        _value = value;
+        _raw = raw;
+    }
+    
+    public String getValue()
+    {
+        return _value;
+    }
+    
+    void append(Object value)
+    {
+        if (value == null)
+            return;
+        
+        _value += " " + value;
+    }
+    
+    void setRaw(boolean raw)
+    {
+        _raw = raw;
+    }
+    
+    public boolean isRaw()
+    {
+        return _raw;
+    }
+    
+    void print(String name, PrintWriter writer, MarkupFilter filter)
+    {
+        writer.print(' ');
+        writer.print(name);
+        writer.print("=\"");
+        
+        if (_raw && _value != null) {
+            
+            writer.write(_value);
+            
+        } else if (_value != null) {
+            
+            char[] data = _value.toCharArray();
+            filter.print(writer, data, 0, data.length, true);
+        }
+        
+        writer.print('"');
+    }
+    
+    public String toString()
+    {
+        return _value;
+    }
+}

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/MarkupWriterImpl.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/MarkupWriterImpl.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/MarkupWriterImpl.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/java/org/apache/tapestry/markup/MarkupWriterImpl.java Mon Dec 11 13:34:22 2006
@@ -16,7 +16,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.hivemind.ApplicationRuntimeException;
 import org.apache.hivemind.util.Defense;
@@ -61,9 +64,9 @@
      */
 
     private boolean _emptyTag = false;
-
+    
     private String _contentType;
-
+    
     /**
      * A Stack of Strings used to track the active tag elements. Elements are active until the
      * corresponding close tag is written. The {@link #push(String)}method adds elements to the
@@ -71,7 +74,14 @@
      */
 
     private List _activeElementStack;
-
+    
+    /**
+     *  Attributes are stored in a map until an open tag is closed. The linked hashmap ensures that
+     *  ordering remains constant.
+     */
+    
+    private final Map _attrMap = new LinkedHashMap();
+    
     public MarkupWriterImpl(String contentType, PrintWriter writer, MarkupFilter filter)
     {
         Defense.notNull(contentType, "contentType");
@@ -86,23 +96,15 @@
     public void attribute(String name, int value)
     {
         checkTagOpen();
-
-        _writer.print(' ');
-        _writer.print(name);
-        _writer.print("=\"");
-        _writer.print(value);
-        _writer.print('"');
+        
+        _attrMap.put(name, new DefaultAttribute(String.valueOf(value), false));
     }
 
     public void attribute(String name, boolean value)
     {
         checkTagOpen();
-
-        _writer.print(' ');
-        _writer.print(name);
-        _writer.print("=\"");
-        _writer.print(value);
-        _writer.print('"');
+        
+        _attrMap.put(name, new DefaultAttribute(String.valueOf(value), false));
     }
 
     public void attribute(String name, String value)
@@ -113,29 +115,88 @@
     public void attribute(String name, String value, boolean raw)
     {
         checkTagOpen();
-
-        _writer.print(' ');
-
-        // Could use a check here that name contains only valid characters
-
-        _writer.print(name);
-        _writer.print("=\"");
-
-        if (value != null)
-        {
-            char[] data = value.toCharArray();
-            maybePrintFiltered(data, 0, data.length, raw, true);
+        
+        _attrMap.put(name, new DefaultAttribute(value, raw));
+    }
+    
+    public void appendAttribute(String name, boolean value)
+    {
+        checkTagOpen();
+        
+        appendAttribute(name, String.valueOf(value));
+    }
+    
+    public void appendAttribute(String name, int value)
+    {
+        checkTagOpen();
+        
+        appendAttribute(name, String.valueOf(value));
+    }
+    
+    public void appendAttribute(String name, String value)
+    {
+        checkTagOpen();
+        
+        DefaultAttribute attr = (DefaultAttribute)_attrMap.get(name);
+        
+        if (attr == null) {
+            attr = new DefaultAttribute(value, false);
+            _attrMap.put(name, attr);
+            return;
         }
+        
+        attr.append(value);
+    }
 
-        _writer.print('"');
+    public void appendAttributeRaw(String name, String value)
+    {
+        checkTagOpen();
+        
+        DefaultAttribute attr = (DefaultAttribute)_attrMap.get(name);
+        
+        if (attr == null) {
+            attr = new DefaultAttribute(value, true);
+            _attrMap.put(name, attr);
+            return;
+        }
+        
+        attr.setRaw(true);
+        attr.append(value);
     }
 
+    public Attribute getAttribute(String name)
+    {
+        checkTagOpen();
+        
+        return (Attribute)_attrMap.get(name);
+    }
+    
+    public boolean hasAttribute(String name)
+    {
+        checkTagOpen();
+        
+        return _attrMap.containsKey(name);
+    }
+    
+    public void clearAttributes()
+    {
+        checkTagOpen();
+        
+        _attrMap.clear();
+    }
+    
+    public Attribute removeAttribute(String name)
+    {
+        checkTagOpen();
+        
+        return (Attribute)_attrMap.remove(name);
+    }
+    
     /**
      * Prints the value, if non-null. May pass it through the filter, unless raw is true.
      */
 
-    private void maybePrintFiltered(char[] data, int offset, int length, boolean raw,
-            boolean isAttribute)
+    private void maybePrintFiltered(char[] data, int offset, int length, boolean raw, boolean isAttribute)
     {
         if (data == null || length <= 0)
             return;
@@ -208,6 +269,8 @@
 
     public void closeTag()
     {
+        flushAttributes();
+        
         if (_emptyTag)
             _writer.print('/');
 
@@ -216,7 +279,29 @@
         _openTag = false;
         _emptyTag = false;
     }
-
+    
+    /**
+     * Causes any pending attributes on the current open tag
+     * to be written out to the writer.
+     */
+    void flushAttributes()
+    {
+        if (_attrMap.size() > 0) {
+            
+            Iterator it = _attrMap.keySet().iterator();
+            while (it.hasNext()) {
+                
+                String key = (String)it.next();
+                DefaultAttribute attr = (DefaultAttribute)_attrMap.get(key);
+                
+                attr.print(key, _writer, _filter);
+            }
+            
+            _attrMap.clear();
+        }
+        
+    }
+    
     public void comment(String value)
     {
         if (_openTag)

Modified: tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/markup/TestMarkupWriter.java
URL: http://svn.apache.org/viewvc/tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/markup/TestMarkupWriter.java?view=diff&rev=485882&r1=485881&r2=485882
==============================================================================
--- tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/markup/TestMarkupWriter.java (original)
+++ tapestry/tapestry4/trunk/tapestry-framework/src/test/org/apache/tapestry/markup/TestMarkupWriter.java Mon Dec 11 13:34:22 2006
@@ -20,7 +20,7 @@
 import org.apache.hivemind.ApplicationRuntimeException;
 import org.apache.tapestry.BaseComponentTestCase;
 import org.apache.tapestry.IMarkupWriter;
-import org.testng.annotations.Configuration;
+import org.testng.annotations.AfterClass;
 import org.testng.annotations.Test;
 
 /**
@@ -68,7 +68,7 @@
         return new PrintWriter(_writer);
     }
 
-    @Configuration(afterTestClass = true)
+    @AfterClass
     protected void tearDown() throws Exception
     {
         _writer = null;
@@ -76,14 +76,14 @@
 
     private void assertOutput(String expected)
     {
-        assertEquals(expected, _writer.toString());
+        assertEquals(_writer.toString(), expected);
 
         _writer.reset();
     }
 
     public void testIntAttribute()
     {
-        MarkupFilter filter = newFilter();
+        MarkupFilter filter = new EchoMarkupFilter();
         PrintWriter writer = newPrintWriter();
 
         replay();
@@ -91,14 +91,12 @@
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
 
         mw.begin("span");
-
-        assertOutput("<span");
-
         mw.attribute("width", 5);
-
-        assertOutput(" width=\"5\"");
+        mw.end();
 
         verify();
+        
+        assertOutput("<span width=\"{5}\"></span>");
     }
 
     public void testIntAttributeRequiresTag()
@@ -125,26 +123,22 @@
 
     public void testBooleanAttribute()
     {
-        MarkupFilter filter = newFilter();
+        MarkupFilter filter = new EchoMarkupFilter();
         PrintWriter writer = newPrintWriter();
 
         replay();
 
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
-
+        
         mw.begin("div");
-
-        assertOutput("<div");
-
         mw.attribute("true", true);
-
-        assertOutput(" true=\"true\"");
-
         mw.attribute("false", false);
-
-        assertOutput(" false=\"false\"");
-
+        
+        mw.end();
+        
         verify();
+        
+        assertOutput("<div true=\"{true}\" false=\"{false}\"></div>");
     }
 
     public void testBooleanAttributeRequiresTag()
@@ -173,36 +167,53 @@
     {
         MarkupFilter filter = new EchoMarkupFilter();
         PrintWriter writer = newPrintWriter();
-
+        
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
-
+        
         mw.begin("span");
         mw.attribute("width", "100%");
-
+        mw.end();
+        
         // Braces added by EchoMarkupFilter, to prove its there.
 
-        assertOutput("<span width=\"{100%}\"");
+        assertOutput("<span width=\"{100%}\"></span>");
     }
 
     public void testAttributeNull()
     {
-        MarkupFilter filter = newFilter();
+        MarkupFilter filter = new EchoMarkupFilter();
         PrintWriter writer = newPrintWriter();
-
+        
         replay();
 
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
 
         mw.begin("span");
         mw.attribute("width", null);
-
+        mw.end();
+        
         // Braces added by EchoMarkupFilter, to prove its there.
-
-        assertOutput("<span width=\"\"");
-
+        
+        assertOutput("<span width=\"\"></span>");
+        
         verify();
     }
-
+    
+    public void test_Duplicate_Attributes()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.attribute("width", "100%");
+        mw.attribute("width", "80%");
+        mw.end();
+        
+        assertOutput("<span width=\"{80%}\"></span>");
+    }
+    
     public void testAttributeRequiresTag()
     {
         MarkupFilter filter = newFilter();
@@ -225,6 +236,131 @@
         verify();
     }
 
+    public void test_Append_Attribute()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttribute("class", "fred");
+        mw.appendAttribute("class", "barney");
+        mw.appendAttribute("type", false);
+        mw.end();
+        
+        assertOutput("<span class=\"{fred barney}\" type=\"{false}\"></span>");
+    }
+    
+    public void test_Append_Attribute_Null()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttribute("class", "fred");
+        mw.appendAttribute("class", null);
+        mw.end();
+        
+        assertOutput("<span class=\"{fred}\"></span>");
+    }
+    
+    public void test_Append_Attribute_Raw()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttributeRaw("class", null);
+        mw.appendAttributeRaw("type", "&lt;&gt;");
+        mw.end();
+        
+        assertOutput("<span class=\"\" type=\"&lt;&gt;\"></span>");
+    }
+    
+    public void test_Get_Attribute()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttribute("class", "fred");
+        
+        assertNotNull(mw.getAttribute("class"));
+        assertEquals(mw.getAttribute("class").toString(), "fred");
+        
+        mw.end();
+        
+        assertOutput("<span class=\"{fred}\"></span>");
+    }
+    
+    public void test_Has_Attribute()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttribute("class", "fred");
+        
+        assertTrue(mw.hasAttribute("class"));
+        assertEquals(mw.getAttribute("class").toString(), "fred");
+        
+        mw.end();
+        
+        assertOutput("<span class=\"{fred}\"></span>");
+    }
+    
+    public void test_Remove_Attribute()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.appendAttribute("class", "fred");
+        
+        assertTrue(mw.hasAttribute("class"));
+        
+        assertEquals(mw.removeAttribute("class").toString(), "fred");
+        
+        assertFalse(mw.hasAttribute("class"));
+        
+        mw.end();
+        
+        assertOutput("<span></span>");
+    }
+    
+    public void test_Clear_Attributes()
+    {
+        MarkupFilter filter = new EchoMarkupFilter();
+        PrintWriter writer = newPrintWriter();
+        
+        IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
+        
+        mw.begin("span");
+        mw.attribute("class", "fred");
+        mw.attribute("barney", "bam bam");
+        
+        assertTrue(mw.hasAttribute("barney"));
+        mw.clearAttributes();
+        
+        assertFalse(mw.hasAttribute("barney"));
+        assertFalse(mw.hasAttribute("class"));
+        
+        mw.end();
+        
+        assertOutput("<span></span>");
+    }
+    
     public void testEnd()
     {
         MarkupFilter filter = new EchoMarkupFilter();
@@ -441,31 +577,26 @@
 
         verify();
     }
-
-    /*
-     * Seems to cause problems with JDK 1.5
-     * 
-     *
-     *
+    
     public void testFlush()
     {
         _writer = new CharArrayWriter();
 
         MarkupFilter filter = newFilter();
-        PrintWriter writer = (PrintWriter) newMock(PrintWriterFixture.class);
+        PrintWriter writer = org.easymock.classextension.EasyMock.createMock(PrintWriterFixture.class);
 
         writer.flush();
-
+        
         replay();
-
+        org.easymock.classextension.EasyMock.replay(writer);
+        
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
 
         mw.flush();
 
         verify();
+        org.easymock.classextension.EasyMock.verify(writer);
     }
-    
-    */
 
     public void testPrintCharArray()
     {
@@ -601,36 +732,29 @@
     {
         MarkupFilter filter = new EchoMarkupFilter();
         PrintWriter writer = newPrintWriter();
-
+        
         IMarkupWriter mw = new MarkupWriterImpl("text/html", writer, filter);
-
+        
         mw.begin("div");
-
+        
         IMarkupWriter nested = mw.getNestedWriter();
-
+        
         assertEquals("text/html", nested.getContentType());
-
+        
         nested.begin("span");
         nested.attribute("class", "inner");
         nested.print("nested content");
-
+        
         mw.attribute("class", "outer");
-
-        assertOutput("<div class=\"{outer}\"");
-
+        
         nested.close();
 
         // Close the <div>, then comes the inner/nested content.
-
-        assertOutput("><span class=\"{inner}\">{nested content}</span>");
-
+        
         mw.print("after content");
-
-        assertOutput("{after content}");
-
         mw.end();
-
-        assertOutput("</div>");
+        
+        assertOutput("<div class=\"{outer}\"><span class=\"{inner}\">{nested content}</span>{after content}</div>");
     }
 
     public void testRepeatCloseOnNestedWriter()