blob: 32a36ff039ef0c9b74d4e206bbd0a321a2e93dcc [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 javax.faces.component.UIComponent;
import javax.faces.context.ResponseWriter;
import org.apache.myfaces.trinidad.logging.TrinidadLogger;
import org.apache.myfaces.trinidad.util.IntegerUtils;
import org.apache.myfaces.trinidadinternal.share.util.CaboHttpUtils;
/**
* Implementation of ResponseWriter for outputting XHTML.
*
*/
public class XhtmlResponseWriter extends ResponseWriter
{
static public final String XHTML_CONTENT_TYPE = "application/xhtml+xml";
/**
* Creates an XhtmlResponseWriter.
* @param out a Writer to write to
* @param contentType the xhtml content type
* @param encoding the character encoding the Writer uses
*/
public XhtmlResponseWriter(
Writer out,
String contentType,
String encoding) throws UnsupportedEncodingException
{
_out = out;
// Ignored: getContentType() always identifies this as XHTML
//_contentType = contentType;
_encoding = encoding;
_cdataCount = 0;
CaboHttpUtils.validateEncoding(encoding);
}
@Override
public String getCharacterEncoding()
{
return _encoding;
}
@Override
public String getContentType()
{
return XHTML_CONTENT_TYPE;
}
@Override
public void startDocument() throws IOException
{
}
@Override
public void endDocument() throws IOException
{
_out.flush();
}
/**
* 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 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
{
// =-=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);
}
@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)
{
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)
{
out.write('>');
_closeStart = false;
}
out.write("</");
out.write(name);
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();
Writer out = _out;
Class<?> valueClass = value.getClass();
// Output Boolean values specially
if (valueClass == _BOOLEAN_CLASS)
{
if (Boolean.TRUE.equals(value))
{
out.write(' ');
out.write(name);
out.write("=\"");
out.write(name);
out.write("\"");
}
}
else
{
out.write(' ');
out.write(name);
out.write("=\"");
// write the attribute value
_writeValue(valueClass, value, true);
out.write('"');
}
}
@Override
public void writeURIAttribute(String name,
Object value,
String componentPropertyName)
throws IOException
{
// XML attributes are XML attributes, etc.
writeAttribute(name, value, componentPropertyName);
}
@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)
{
String valueString = text.toString();
if (_dontEscape)
{
write(text.toString());
}
else
{
_closeStartIfNecessary();
XMLEscapes.writeText(_out, valueString);
}
}
}
@Override
public void writeText(char text[], int off, int len)
throws IOException
{
if (_dontEscape)
{
write(text, off, len);
}
else
{
_closeStartIfNecessary();
XMLEscapes.writeText(_out, 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 XhtmlResponseWriter(writer, getContentType(),
getCharacterEncoding());
}
catch (UnsupportedEncodingException e)
{
// this can't happen; the character encoding should already
// be legal.
assert(false);
throw new IllegalStateException();
}
}
//
// 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;
}
/**
* 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)
{
XMLEscapes.writeAttribute(_out, stringValue);
}
else
{
XMLEscapes.writeText(_out, stringValue);
}
}
private void _closeStartIfNecessary() throws IOException
{
_markPendingElements();
if (_closeStart)
{
_out.write('>');
_closeStart = false;
}
}
/**
* Flushes out any pending element, celaring 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);
}
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;
// number of CDATA sections started
private int _cdataCount;
// 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(XhtmlResponseWriter.class);
}