TRINIDAD-2551
Add Pass-through attributes support

ResponseWriter changes
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriter.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriter.java
index 7796cd8..42c49f4 100644
--- a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriter.java
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriter.java
@@ -22,9 +22,13 @@
 import java.io.UnsupportedEncodingException;
 import java.io.Writer;
 import java.util.ArrayList;
+import java.util.Map;
 
+import javax.el.ValueExpression;
 import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
 import javax.faces.context.ResponseWriter;
+import javax.faces.render.Renderer;
 
 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
 import org.apache.myfaces.trinidad.util.IntegerUtils;
@@ -125,6 +129,9 @@
   public void startElement(String name,
                            UIComponent component) throws IOException
   {
+
+    name = _processPassThroughAttributes(name, component);
+
     // =-=AEW Should we force all lowercase?
     if (name.charAt(0) == 's')
     {
@@ -152,6 +159,29 @@
     _startElementImpl(name);
   }
 
+  private String _processPassThroughAttributes(String name, UIComponent component) {
+    if (component == null) {
+      return name;
+    }
+    _passThroughAttributes = component.getPassThroughAttributes(false);
+    if (_passThroughAttributes != null)
+    {
+      Object value = _passThroughAttributes.get(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY);
+      if (value instanceof ValueExpression)
+      {
+        value = ((ValueExpression)value).getValue(FacesContext.getCurrentInstance().getELContext());
+      }
+      if (value != null)
+      {
+        String elementName = value.toString();
+        if (!name.equals(elementName)) {
+          name = elementName;
+        }
+      }
+    }
+    return name;
+  }
+
 
   @Override
   public void endElement(String name) throws IOException
@@ -173,10 +203,11 @@
     // end tag should be output as well
     if (element != null)
     {
-      if (!element.equals(name))
+      // TODO remove this because passThroughAttributes could change the element
+      /*if (!element.equals(name))
       {
         _LOG.severe("ELEMENT_END_NAME_NOT_MATCH_START_NAME", new Object[]{name, element});
-      }
+      }*/
 
       Writer out = _out;
 
@@ -203,7 +234,7 @@
       }
 
       out.write("</");
-      out.write(name);
+      out.write(element);
       out.write('>');
     }
   }
@@ -423,6 +454,51 @@
     out.write('<');
     out.write(name);
     _closeStart = true;
+    _writePassThroughAttributes();
+
+  }
+
+  private void _writePassThroughAttributes() throws IOException {
+    if (_passThroughAttributes != null)
+    {
+      for (Map.Entry<String, Object> entry : _passThroughAttributes.entrySet())
+      {
+        String key = entry.getKey();
+        Object value = entry.getValue();
+        if (Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY.equals(key))
+        {
+          // Special attribute stored in passthrough attribute map,
+          // skip rendering
+          continue;
+        }
+        if (value instanceof ValueExpression)
+        {
+          value = ((ValueExpression)value).getValue(FacesContext.getCurrentInstance().getELContext());
+        }
+        // encodeAndWriteURIAttribute(key, value, key);
+        // JSF 2.2 In the renderkit javadoc of jsf 2.2 spec says this
+        // (Rendering Pass Through Attributes):
+        // "... The ResponseWriter must ensure that any pass through attributes are
+        // rendered on the outer-most markup element for the component. If there is
+        // a pass through attribute with the same name as a renderer specific
+        // attribute, the pass through attribute takes precedence. Pass through
+        // attributes are rendered as if they were passed to
+        // ResponseWriter.writeURIAttribute(). ..."
+        // Note here it says "as if they were passed", instead say "... attributes are
+        // encoded and rendered as if ...". Black box testing against RI shows that there
+        // is no URI encoding at all in this part, so in this case the best is do the
+        // same here. After all, it is resposibility of the one who set the passthrough
+        // attribute to do the proper encoding in cases when a URI is provided. However,
+        // that does not means the attribute should not be encoded as other attributes.
+        // According to tests done, if passthrough attribute is null, the attribute must not
+        // be rendered.
+        if (value != null)
+        {
+          writeAttribute(key, value, null);
+        }
+      }
+      _passThroughAttributes = null;
+    }
 
   }
 
@@ -667,6 +743,8 @@
   // number of CDATA sections started
   private int         _cdataCount;
 
+  private Map<String, Object> _passThroughAttributes;
+
   // stack of skipped and unskipped elements used to determine when
   // to suppress the end tag of a skipped element
   private final ArrayList<String> _skippedElements = new ArrayList<String>(20);
diff --git a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriter.java b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriter.java
index 8b4a2ed..201dee1 100755
--- a/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriter.java
+++ b/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriter.java
@@ -21,9 +21,14 @@
 
 import java.io.IOException;
 import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Map;
 
+import javax.el.ValueExpression;
 import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
 import javax.faces.context.ResponseWriter;
+import javax.faces.render.Renderer;
 
 import org.apache.myfaces.trinidadinternal.io.XMLEscapes;
 
@@ -91,11 +96,77 @@
     UIComponent component) throws IOException
   {
     closeStartIfNecessary();
-
+    Map<String, Object> passThroughAttributes =
+            (component != null) ? component.getPassThroughAttributes(false) : null;
+    name = _processPassThroughAttributes(name, passThroughAttributes);
+    _pushElement(name);
     Writer out = _out;
     out.write('<');
     out.write(name);
     _closeStart = true;
+    _writePassThroughAttributes(passThroughAttributes);
+  }
+
+  private String _processPassThroughAttributes(String name, Map<String, Object> passThroughAttributes)
+  {
+    if (passThroughAttributes != null)
+    {
+      Object value = passThroughAttributes.get(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY);
+      if (value instanceof ValueExpression)
+      {
+        value = ((ValueExpression)value).getValue(FacesContext.getCurrentInstance().getELContext());
+      }
+      if (value != null)
+      {
+        String elementName = value.toString();
+        if (!name.equals(elementName)) {
+          name = elementName;
+        }
+      }
+    }
+    return name;
+  }
+
+  private void _writePassThroughAttributes(Map<String, Object> passThroughAttributes) throws IOException {
+    if (passThroughAttributes != null)
+    {
+      for (Map.Entry<String, Object> entry : passThroughAttributes.entrySet())
+      {
+        String key = entry.getKey();
+        Object value = entry.getValue();
+        if (Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY.equals(key))
+        {
+          // Special attribute stored in passthrough attribute map,
+          // skip rendering
+          continue;
+        }
+        if (value instanceof ValueExpression)
+        {
+          value = ((ValueExpression)value).getValue(FacesContext.getCurrentInstance().getELContext());
+        }
+        // encodeAndWriteURIAttribute(key, value, key);
+        // JSF 2.2 In the renderkit javadoc of jsf 2.2 spec says this
+        // (Rendering Pass Through Attributes):
+        // "... The ResponseWriter must ensure that any pass through attributes are
+        // rendered on the outer-most markup element for the component. If there is
+        // a pass through attribute with the same name as a renderer specific
+        // attribute, the pass through attribute takes precedence. Pass through
+        // attributes are rendered as if they were passed to
+        // ResponseWriter.writeURIAttribute(). ..."
+        // Note here it says "as if they were passed", instead say "... attributes are
+        // encoded and rendered as if ...". Black box testing against RI shows that there
+        // is no URI encoding at all in this part, so in this case the best is do the
+        // same here. After all, it is resposibility of the one who set the passthrough
+        // attribute to do the proper encoding in cases when a URI is provided. However,
+        // that does not means the attribute should not be encoded as other attributes.
+        // According to tests done, if passthrough attribute is null, the attribute must not
+        // be rendered.
+        if (value != null)
+        {
+          writeAttribute(key, value, null);
+        }
+      }
+    }
   }
 
   public void writeAttribute(
@@ -164,6 +235,7 @@
   public void endElement(
     String name) throws IOException
   {
+    name = _popElement();
     Writer out = _out;
     if (_closeStart)
     {
@@ -241,9 +313,33 @@
       _closeStart = false;
     }
   }
+
+  /**
+   * Retrieves the name of the last output element.  If it is null,
+   * something is wrong
+   */
+  private String _popElement()
+  {
+    int size = _elements.size();
+    if (size == 0)
+      return null;
+
+    return _elements.remove(size - 1);
+  }
+
+  /**
+   * Marks that we have outputted a real element so that the ordering of
+   * the outputted and skipped elements can be maintained.
+   */
+  private void _pushElement(String name)
+  {
+    _elements.add(name);
+  }
   
   private final Writer      _out;
   private final String      _encoding;
   private       boolean     _closeStart;
   private       int         _cdataCount;
+  private final ArrayList<String> _elements = new ArrayList<String>(20);
+
 }
diff --git a/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriterTest.java b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriterTest.java
new file mode 100644
index 0000000..d93f75c
--- /dev/null
+++ b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/io/HtmlResponseWriterTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.myfaces.trinidadinternal.io;
+
+
+import java.io.StringWriter;
+
+import javax.faces.component.html.HtmlInputText;
+import javax.faces.context.ResponseWriter;
+import javax.faces.render.Renderer;
+
+import org.apache.myfaces.test.el.MockValueExpression;
+import org.apache.myfaces.trinidadbuild.test.FacesTestCase;
+import org.junit.Assert;
+
+public class HtmlResponseWriterTest extends FacesTestCase
+{
+
+    private StringWriter _stringWriter;
+    private ResponseWriter _responseWriter;
+
+    public HtmlResponseWriterTest(String testName)
+    {
+        super(testName);
+    }
+
+    @Override
+    public void setUp() throws Exception
+    {
+        super.setUp();
+        _stringWriter = new StringWriter();
+        _responseWriter = new HtmlResponseWriter(_stringWriter, "UTF-8");
+    }
+
+    public void testWithoutPassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<div><div><input name=\"test\"></div></div>", _stringWriter.toString());
+    }
+
+    public void testSimplePassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put("data-test", "test");
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<div data-test=\"test\"><div><input name=\"test\"></div></div>", _stringWriter.toString());
+    }
+
+    public void testValueExpressionPassThroughAttribute() throws Exception
+    {
+        externalContext.getRequestMap().put("test", Boolean.TRUE);
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put("data-test", new MockValueExpression("#{test}", Boolean.TYPE));
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<div data-test><div><input name=\"test\"></div></div>", _stringWriter.toString());
+    }
+
+    public void testRendererLocalNamePassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, "test");
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<test><div><input name=\"test\"></div></test>", _stringWriter.toString());
+    }
+}
diff --git a/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriterTest.java b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriterTest.java
new file mode 100644
index 0000000..840c81f
--- /dev/null
+++ b/trinidad-impl/src/test/java/org/apache/myfaces/trinidadinternal/renderkit/core/ppr/XmlResponseWriterTest.java
@@ -0,0 +1,119 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.myfaces.trinidadinternal.renderkit.core.ppr;
+
+
+import java.io.StringWriter;
+
+import javax.faces.component.html.HtmlInputText;
+import javax.faces.context.PartialResponseWriter;
+import javax.faces.context.ResponseWriter;
+import javax.faces.render.Renderer;
+
+import org.apache.myfaces.test.el.MockValueExpression;
+import org.apache.myfaces.trinidadbuild.test.FacesTestCase;
+import org.junit.Assert;
+
+public class XmlResponseWriterTest extends FacesTestCase
+{
+
+    private StringWriter _stringWriter;
+    private ResponseWriter _responseWriter;
+
+    public XmlResponseWriterTest(String testName)
+    {
+        super(testName);
+    }
+
+    @Override
+    public void setUp() throws Exception
+    {
+        super.setUp();
+        _stringWriter = new StringWriter();
+        _responseWriter = new PartialResponseWriter(new XmlResponseWriter(_stringWriter, "UTF-8"));
+    }
+
+    public void testWithoutPassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<partial-response id=\"j_id1\"><div><div><input name=\"test\"/></div></div></partial-response>", _stringWriter.toString());
+    }
+
+    public void testSimplePassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put("data-test", "test");
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<partial-response id=\"j_id1\"><div data-test=\"test\"><div><input name=\"test\"/></div></div></partial-response>", _stringWriter.toString());
+    }
+
+    public void testValueExpressionPassThroughAttribute() throws Exception
+    {
+        externalContext.getRequestMap().put("test", Boolean.TRUE);
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put("data-test", new MockValueExpression("#{test}", Boolean.TYPE));
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<partial-response id=\"j_id1\"><div data-test=\"true\"><div><input name=\"test\"/></div></div></partial-response>", _stringWriter.toString());
+    }
+
+    public void testRendererLocalNamePassThroughAttribute() throws Exception
+    {
+        _responseWriter.startDocument();
+        HtmlInputText inputText = new HtmlInputText();
+        inputText.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, "test");
+        _responseWriter.startElement("div", inputText);
+        _responseWriter.startElement("div", null);
+        _responseWriter.startElement("input", null);
+        _responseWriter.writeAttribute("name", "test", null);
+        _responseWriter.endElement("input");
+        _responseWriter.endElement("div");
+        _responseWriter.endElement("div");
+        _responseWriter.endDocument();
+        Assert.assertEquals("<?xml version='1.0' encoding='UTF-8'?>\n" +
+                "<partial-response id=\"j_id1\"><test><div><input name=\"test\"/></div></test></partial-response>", _stringWriter.toString());
+    }
+}