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());
+ }
+}