blob: a479ad60ad3b9ab7eb87536ae74f163f682e731d [file] [log] [blame]
/*
* 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.IOException;
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;
import org.apache.myfaces.trinidadinternal.share.url.EncoderUtils;
import org.apache.myfaces.trinidadinternal.share.util.CaboHttpUtils;
/**
* Implementation of ResponseWriter for outputting HTML.
*
*/
public class HtmlResponseWriter extends ResponseWriter
{
static public final String HTML_CONTENT_TYPE = "text/html";
/**
* Creates an HtmlResponseWriter.
* @param out a Writer to write to
* @param encoding the character encoding the Writer uses
*/
public HtmlResponseWriter(Writer out, String encoding)
throws UnsupportedEncodingException
{
_out = out;
_encoding = encoding;
_cdataCount = 0;
CaboHttpUtils.validateEncoding(encoding);
}
@Override
public String getCharacterEncoding()
{
return _encoding;
}
@Override
public String getContentType()
{
return HTML_CONTENT_TYPE;
}
@Override
public void startDocument() throws IOException
{
}
/**
* Writes out CDATA start.
* @throws IOException on any read/write error
*/
public void startCDATA() throws IOException
{
_closeStartIfNecessary();
// Ignore all nested calls to start a CDATA section except the first - a CDATA section cannot contain the string
// "]]>" as the section ends ends with the first occurrence of this sequence.
_cdataCount++;
if (_cdataCount == 1)
_out.write("<![CDATA[");
}
/**
* Writes out an end CDATA element.
* @throws IOException on any read/write error
*/
public void endCDATA() throws IOException
{
// Only close the outermost CDATA section and ignore nested calls to endCDATA().
if (_cdataCount == 1)
_out.write("]]>");
_cdataCount--;
}
@Override
public void endDocument() throws IOException
{
_out.flush();
}
@Override
public void flush() throws IOException
{
_closeStartIfNecessary();
_out.flush();
}
@Override
public void close()throws IOException
{
flush();
_out.close();
}
@Override
public void startElement(String name,
UIComponent component) throws IOException
{
name = _processPassThroughAttributes(name, component);
// =-=AEW Should we force all lowercase?
if (name.charAt(0) == 's')
{
// Optimization (see bug 2009019): our code has a tendency
// to write out unnecessary empty <span> elements. So,
// when we start a "span" HTML element, don't actually write
// out anything just yet; mark it pending.
if ("span".equals(name))
{
// push any pending element onto the stack of skipped elements
_markPendingElements();
// make this the current pending element
_pendingElement = name;
return;
}
else if ("script".equals(name) ||
"style".equals(name))
{
_dontEscape = true;
}
}
// start writing the element
_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
{
// eliminate any <pending></pending> combinations
if (_pendingElement != null)
{
// we need to return immedediately because in this
// case, the element was never pushed onto the
// element stack.
_pendingElement = null;
return;
}
// get the name of the last outputted element
String element = _popSkippedElement();
// non-null names indicate that the element was ouput, so its
// end tag should be output as well
if (element != null)
{
// 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;
// always turn escaping back on once an element ends
_dontEscape = false;
if (_closeStart)
{
boolean isEmptyElement = _isEmptyElement(element);
if (_currAttr != null)
{
out.write('"');
_currAttr = null;
}
out.write('>');
_closeStart = false;
if (isEmptyElement)
{
return;
}
}
out.write("</");
out.write(element);
out.write('>');
}
}
@Override
public void writeAttribute(String name,
Object value,
String componentPropertyName)
throws IOException
{
if (value == null)
return;
// if we have a pending element, flush it because
// it has an attribute, and is thus needed
_outputPendingElements();
// if we aren't in in the element's start tag, we shouldn't be writing attributes
if (!_closeStart)
throw new IllegalStateException();
Writer out = _out;
Class<?> valueClass = value.getClass();
// See what attribute we were involved in
// FIXME: delete the _currAttr code, which is unused and contrary
// to the JSF spec
String currAttr = _currAttr;
if (currAttr != null)
{
if (currAttr.equals(name))
{
_writeValue(valueClass, value, true);
return;
}
else
{
out.write('"');
}
}
// Output Boolean values specially
if (valueClass == _BOOLEAN_CLASS)
{
if (Boolean.TRUE.equals(value))
{
out.write(' ');
out.write(name);
}
_currAttr = null;
}
else
{
out.write(' ');
out.write(name);
out.write("=\"");
// write the attribute value
_writeValue(valueClass, value, true);
_currAttr = name;
}
}
@Override
public void writeURIAttribute(String name,
Object value,
String componentPropertyName)
throws IOException
{
if (value == null)
return;
// if we have a pending element, flush it because
// it has an attribute, and is thus needed
_outputPendingElements();
// if we aren't in in the element's start tag, we shouldn't be writing attributes
if (!_closeStart)
throw new IllegalStateException();
Writer out = _out;
// No current support for multi-part URI attributes
String currAttr = _currAttr;
if (currAttr != null)
{
out.write('"');
_currAttr = null;
}
out.write(' ');
out.write(name);
out.write("=\"");
String stringValue = value.toString();
// =-=AEW I'm pretty sure that Javascript URLs _shouldn't_ be
// encoded...
if (stringValue.startsWith("javascript:"))
HTMLEscapes.writeAttribute(out, _buffer, stringValue);
else
EncoderUtils.writeURLForHTML(out, stringValue, _encoding, false);
out.write('"');
}
@Override
public void writeComment(Object comment) throws IOException
{
if (comment != null)
{
_closeStartIfNecessary();
_out.write("<!--");
_out.write(comment.toString());
_out.write("-->");
}
}
@Override
public void writeText(Object text, String componentPropertyName)
throws IOException
{
if (text != null)
{
if (_dontEscape)
{
write(text.toString());
}
else
{
_closeStartIfNecessary();
HTMLEscapes.writeText(_out, _buffer, text.toString());
}
}
}
@Override
public void writeText(char text[], int off, int len)
throws IOException
{
if (_dontEscape)
{
write(text, off, len);
}
else
{
_closeStartIfNecessary();
HTMLEscapes.writeText(_out, _buffer, text, off, len);
}
}
@Override
public void write(char cbuf[], int off, int len) throws IOException
{
_closeStartIfNecessary();
_out.write(cbuf, off, len);
}
@Override
public void write(String str) throws IOException
{
_closeStartIfNecessary();
_out.write(str);
}
@Override
public void write(int c) throws IOException
{
_closeStartIfNecessary();
_out.write((char) c);
}
@Override
public ResponseWriter cloneWithWriter(Writer writer)
{
try
{
return new HtmlResponseWriter(writer, getCharacterEncoding());
}
catch (UnsupportedEncodingException e)
{
// this can't happen; the character encoding should already
// be legal.
assert(false);
throw new IllegalStateException();
}
}
public String toString()
{
return "HtmlResponseWriter[" + _out + "]";
}
//
// Private methods
//
private void _startElementImpl(String name) throws IOException
{
// close any previously stated element, if necessary
_closeStartIfNecessary();
// note that we started a non-skipped element
_pushOutputtedElement(name);
Writer out = _out;
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;
}
}
/**
* Writes the value of an object
*/
private void _writeValue(
Class<?> valueClass,
Object value,
boolean isAttribute
) throws IOException
{
assert(valueClass != _CHAR_ARRAY_CLASS) :
"Character arrays not supported as HTML attributes";
if (valueClass == _INTEGER_CLASS)
{
// Integers never need to be escaped - and
// we can cache common instances.
_out.write(IntegerUtils.getString((Integer) value));
return;
}
String stringValue = value.toString();
if (isAttribute)
{
HTMLEscapes.writeAttribute(_out, _buffer, stringValue);
}
else
{
HTMLEscapes.writeText(_out, _buffer, stringValue);
}
}
private void _closeStartIfNecessary() throws IOException
{
_markPendingElements();
if (_closeStart)
{
if (_currAttr != null)
{
_out.write('"');
_currAttr = null;
}
_out.write('>');
_closeStart = false;
}
}
/**
* Flushes out any pending element, clearing the pending
* entry.
*/
private void _outputPendingElements() throws IOException
{
String pendingElement = _pendingElement;
if (pendingElement != null)
{
// we clear the pending element BEFORE calling
// startElementImpl to prevent _startElementImpl's indirect call
// to _markPendingElements from pushing our element onto
// the skipped stack, imbalancing the stack
_pendingElement = null;
// start the pending element
_startElementImpl(pendingElement);
}
}
/**
* If an element is pending, push it onto the stack of skipped
* elements because it doesn't have any attributes
*/
private void _markPendingElements()
{
String pendingElement = _pendingElement;
if (pendingElement != null)
{
_pushSkippedElement();
_pendingElement = null;
}
}
/**
* Retrieves the name of the last output element. If it is null,
* that element was skipped and thus its end tag should be suppressed
* as well
*/
private String _popSkippedElement()
{
int size = _skippedElements.size();
if (size == 0)
return null;
return _skippedElements.remove(size - 1);
}
/**
* Marks the skipped element so that the output of its
* end tag can also be suppressed
*/
private void _pushSkippedElement()
{
_skippedElements.add(null);
}
/**
* Marks that we have outputted a real element so that the ordering of
* the outputted and skipped elements can be maintained. This
* also aids debuggin by ensuring that the element being ended matches
* the actual ouputted name.
*/
private void _pushOutputtedElement(
String name
)
{
_skippedElements.add(name);
}
static private boolean _isEmptyElement(String name)
{
// =-=AEW Performance? Certainly slower to use a hashtable,
// at least if we can't assume the input name is lowercased.
String[] array = _emptyElementArr[name.charAt(0)];
if (array != null)
{
for (int i = array.length - 1; i >= 0; i--)
{
if (name.equalsIgnoreCase(array[i]))
return true;
}
}
return false;
}
static private String[][] _emptyElementArr = new String[256][];
static private String[] _aNames = new String[]
{
"area",
};
static private String[] _bNames = new String[]
{
"br",
"base",
"basefont",
};
static private String[] _cNames = new String[]
{
"col",
};
static private String[] _eNames = new String[]
{
"embed",
};
static private String[] _fNames = new String[]
{
"frame",
};
static private String[] _hNames = new String[]
{
"hr",
};
static private String[] _iNames = new String[]
{
"img",
"input",
"isindex",
};
static private String[] _lNames = new String[]
{
"link",
};
static private String[] _mNames = new String[]
{
"meta",
};
static private String[] _pNames = new String[]
{
"param",
};
static
{
_emptyElementArr['a'] = _aNames;
_emptyElementArr['A'] = _aNames;
_emptyElementArr['b'] = _bNames;
_emptyElementArr['B'] = _bNames;
_emptyElementArr['c'] = _cNames;
_emptyElementArr['C'] = _cNames;
_emptyElementArr['e'] = _eNames;
_emptyElementArr['E'] = _eNames;
_emptyElementArr['f'] = _fNames;
_emptyElementArr['F'] = _fNames;
_emptyElementArr['h'] = _hNames;
_emptyElementArr['H'] = _hNames;
_emptyElementArr['i'] = _iNames;
_emptyElementArr['I'] = _iNames;
_emptyElementArr['l'] = _lNames;
_emptyElementArr['L'] = _lNames;
_emptyElementArr['m'] = _mNames;
_emptyElementArr['M'] = _mNames;
_emptyElementArr['p'] = _pNames;
_emptyElementArr['P'] = _pNames;
}
// buffer for accumulating results
// Relies on ResponseWriter not being threadsafe
private char[] _buffer = new char[1028];
private boolean _closeStart;
private boolean _dontEscape;
private Writer _out;
private String _encoding;
// holds an element that will only be started if it has attributes
private String _pendingElement;
private String _currAttr;
// 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);
private static final Class<?> _CHAR_ARRAY_CLASS = (new char[0]).getClass();
private static final Class<?> _BOOLEAN_CLASS = Boolean.class;
private static final Class<?> _INTEGER_CLASS = Integer.class;
static private final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(HtmlResponseWriter.class);
}