/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2012 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://glassfish.java.net/public/CDDL+GPL_1_1.html
* or packager/legal/LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at packager/legal/LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.faces.renderkit.html_basic;
import java.io.IOException;
import java.io.Writer;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.faces.FacesException;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import com.sun.faces.RIConstants;
import com.sun.faces.config.WebConfiguration;
import com.sun.faces.config.WebConfiguration.BooleanWebContextInitParameter;
import com.sun.faces.io.FastStringWriter;
import com.sun.faces.util.HtmlUtils;
import com.sun.faces.util.MessageUtils;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.el.ValueExpression;
import javax.faces.context.ExternalContext;
import javax.faces.render.Renderer;
/**
* <p><strong>HtmlResponseWriter</strong> is an Html specific implementation
* of the <code>ResponseWriter</code> abstract class.
* Kudos to Adam Winer (Oracle) for much of this code.
*/
public class HtmlResponseWriter extends ResponseWriter {
// Content Type for this Writer.
//
private String contentType = "text/html";
// Character encoding of that Writer - this may be null
// if the encoding isn't known.
//
private String encoding = null;
// Writer to use for output;
//
private Writer writer = null;
// True when we need to close a start tag
//
private boolean closeStart;
// Configuration flag regarding disableUnicodeEscaping
//
private WebConfiguration.DisableUnicodeEscaping disableUnicodeEscaping;
//Flag to escape Unicode
//
private boolean escapeUnicode;
// Flag to escape ISO-8859-1 codes
//
private boolean escapeIso;
// True when we shouldn't be escaping output (basically,
// inside of <script> and <style> elements). Note
// that this will *not* be set for CDATA blocks - that's
// instead the writingCdata flag
//
private boolean dontEscape;
// flag to indicate we're writing a CDATA section
private boolean writingCdata;
// flat to indicate the current element is CDATA
private boolean isCdata;
// flag to indicate that we're writing a 'script' or 'style' element
private boolean isScript;
// flag to indicate that we're writing a 'style' element
private boolean isStyle;
// flag to indicate if we are within a script element
private boolean withinScript;
// flag to indicate if we are within a style element
private boolean withinStyle;
// flag to indicate that we're writing a 'src' attribute as part of
// 'script' or 'style' element
private boolean scriptOrStyleSrc;
// flag to indicate if this is a partial response
private boolean isPartial;
// flag to indicate if the content type is XHTML
private boolean isXhtml;
// HtmlResponseWriter to use when buffering is required
private Writer origWriter;
// Keep one instance of the script buffer per Writer
private FastStringWriter scriptBuffer;
// Keep one instance of attributesBuffer to buffer the writing
// of all attributes for a particular element to reduce the number
// of writes
private FastStringWriter attributesBuffer;
// Enables hiding of inlined script and style
// elements from old browsers
private Boolean isScriptHidingEnabled;
// Enables scripts to be included in attribute values
private Boolean isScriptInAttributeValueEnabled;
// Internal buffer used when outputting properly escaped information
// using HtmlUtils class.
//
private char[] buffer = new char[1028];
// Internal buffer used when outputting properly escaped CData information.
//
private final static int cdataBufferSize = 1024;
private char[] cdataBuffer = new char[cdataBufferSize];
private int cdataBufferLength = 0;
// Secondary cdata buffer, used for writeText
private final static int cdataTextBufferSize = 128;
private char[] cdataTextBuffer = new char[cdataTextBufferSize];
private Map<String, Object> passthroughAttributes;
// Internal buffer for to store the result of String.getChars() for
// values passed to the writer as String to reduce the overhead
// of String.charAt(). This buffer will be grown, if necessary, to
// accomodate larger values.
private char[] textBuffer = new char[128];
private char[] charHolder = new char[1];
private LinkedList<String> elementNames;
private static final String BREAKCDATA = "]]><![CDATA[";
private static final char[] ESCAPEDSINGLEBRACKET = ("]"+BREAKCDATA).toCharArray();
private static final char[] ESCAPEDLT= ("<"+BREAKCDATA).toCharArray();
private static final char[] ESCAPEDSTART= ("<"+BREAKCDATA+"![").toCharArray();
private static final char[] ESCAPEDEND= ("]"+BREAKCDATA+"]>").toCharArray();
private static final int CLOSEBRACKET = (int)']';
private static final int LT = (int)'<';
static final Pattern CDATA_START_SLASH_SLASH;
static final Pattern CDATA_END_SLASH_SLASH;
static final Pattern CDATA_START_SLASH_STAR;
static final Pattern CDATA_END_SLASH_STAR;
static {
// At the beginning of a line, match // followed by any amount of
// whitespace, followed by <![CDATA[
CDATA_START_SLASH_SLASH = Pattern.compile("^//\\s*\\Q<![CDATA[\\E");
// At the end of a line, match // followed by any amout of whitespace,
// followed by ]]>
CDATA_END_SLASH_SLASH = Pattern.compile("//\\s*\\Q]]>\\E$");
// At the beginning of a line, match /* followed by any amout of
// whitespace, followed by <![CDATA[, followed by any amount of whitespace,
// followed by */
CDATA_START_SLASH_STAR = Pattern.compile("^/\\*\\s*\\Q<![CDATA[\\E\\s*\\*/");
// At the end of a line, match /* followed by any amount of whitespace,
// followed by ]]> followed by any amount of whitespace, followed by */
CDATA_END_SLASH_STAR = Pattern.compile("/\\*\\s*\\Q]]>\\E\\s*\\*/$");
}
// ------------------------------------------------------------ Constructors
/**
* Constructor sets the <code>ResponseWriter</code> and
* encoding, and enables script hiding by default.
*
* @param writer the <code>ResponseWriter</code>
* @param contentType the content type.
* @param encoding the character encoding.
*
* @throws javax.faces.FacesException the encoding is not recognized.
*/
public HtmlResponseWriter(Writer writer,
String contentType,
String encoding)
throws FacesException {
this(writer, contentType, encoding, null, null, null, false);
}
/**
* <p>Constructor sets the <code>ResponseWriter</code> and
* encoding.</p>
*
* <p>The argument configPrefs is a map of configurable prefs that affect
* this instance's behavior. Supported keys are:</p>
*
* <p>BooleanWebContextInitParameter.EnableJSStyleHiding: <code>true</code>
* if the writer should attempt to hide JS from older browsers</p>
*
* @param writer the <code>ResponseWriter</code>
* @param contentType the content type.
* @param encoding the character encoding.
*
* @throws javax.faces.FacesException the encoding is not recognized.
*/
public HtmlResponseWriter(Writer writer,
String contentType,
String encoding,
Boolean isScriptHidingEnabled,
Boolean isScriptInAttributeValueEnabled,
WebConfiguration.DisableUnicodeEscaping disableUnicodeEscaping,
boolean isPartial)
throws FacesException {
this.writer = writer;
if (null != contentType) {
this.contentType = contentType;
}
this.encoding = encoding;
// init those configuration parameters not yet initialized
WebConfiguration webConfig = null;
if (isScriptHidingEnabled == null) {
webConfig = getWebConfiguration(webConfig);
isScriptHidingEnabled = (null == webConfig) ? BooleanWebContextInitParameter.EnableJSStyleHiding.getDefaultValue() :
webConfig.isOptionEnabled(
BooleanWebContextInitParameter.EnableJSStyleHiding);
}
if (isScriptInAttributeValueEnabled == null) {
webConfig = getWebConfiguration(webConfig);
isScriptInAttributeValueEnabled = (null == webConfig) ? BooleanWebContextInitParameter.EnableScriptInAttributeValue.getDefaultValue() :
webConfig.isOptionEnabled(
BooleanWebContextInitParameter.EnableScriptInAttributeValue);
}
if (disableUnicodeEscaping == null) {
webConfig = getWebConfiguration(webConfig);
disableUnicodeEscaping =
WebConfiguration.DisableUnicodeEscaping.getByValue(
(null == webConfig) ? WebConfiguration.WebContextInitParameter.DisableUnicodeEscaping.getDefaultValue() :
webConfig.getOptionValue(
WebConfiguration.WebContextInitParameter.DisableUnicodeEscaping));
if (disableUnicodeEscaping == null) {
disableUnicodeEscaping = WebConfiguration.DisableUnicodeEscaping.False;
}
}
// and store them for later use
this.isPartial = isPartial;
this.isScriptHidingEnabled = isScriptHidingEnabled;
this.isScriptInAttributeValueEnabled = isScriptInAttributeValueEnabled;
this.disableUnicodeEscaping = disableUnicodeEscaping;
this.attributesBuffer = new FastStringWriter(128);
// Check the character encoding
if (!HtmlUtils.validateEncoding(encoding)) {
throw new IllegalArgumentException(MessageUtils.getExceptionMessageString(
MessageUtils.ENCODING_ERROR_MESSAGE_ID));
}
String charsetName = encoding.toUpperCase();
switch (disableUnicodeEscaping)
{
case True:
// html escape noting (except the dangerous characters like "<>'" etc
escapeUnicode = false;
escapeIso = false;
break;
case False:
// html escape any non-ascii character
escapeUnicode = true;
escapeIso = true;
break;
case Auto:
// is stream capable of rendering unicode, do not escape
escapeUnicode = !HtmlUtils.isUTFencoding(charsetName);
// is stream capable of rendering unicode or iso-8859-1, do not escape
escapeIso = !HtmlUtils.isISO8859_1encoding(charsetName) && !HtmlUtils.isUTFencoding(charsetName);
break;
}
}
private WebConfiguration getWebConfiguration(WebConfiguration webConfig) {
if (webConfig != null)
{
return webConfig;
}
FacesContext context = FacesContext.getCurrentInstance();
if (null != context) {
ExternalContext extContext = context.getExternalContext();
if (null != extContext) {
webConfig = WebConfiguration.getInstance(extContext);
}
}
return webConfig;
}
// -------------------------------------------------- Methods From Closeable
/** Methods From <code>java.io.Writer</code> */
@Override
public void close() throws IOException {
closeStartIfNecessary();
writer.close();
}
// -------------------------------------------------- Methods From Flushable
/**
* Flush any buffered output to the contained writer.
*
* @throws IOException if an input/output error occurs.
*/
@Override
public void flush() throws IOException {
// NOTE: Internal buffer's contents (the ivar "buffer") is
// written to the contained writer in the HtmlUtils class - see
// HtmlUtils.flushBuffer method; Buffering is done during
// writeAttribute/writeText - otherwise, output is written
// directly to the writer (ex: writer.write(....)..
//
// close any previously started element, if necessary
closeStartIfNecessary();
}
// ---------------------------------------------------------- Public Methods
/** @return the content type such as "text/html" for this ResponseWriter. */
@Override
public String getContentType() {
return contentType;
}
/**
* <p>Create a new instance of this <code>ResponseWriter</code> using
* a different <code>Writer</code>.
*
* @param writer The <code>Writer</code> that will be used to create
* another <code>ResponseWriter</code>.
*/
@Override
public ResponseWriter cloneWithWriter(Writer writer) {
try {
HtmlResponseWriter responseWriter = new HtmlResponseWriter(writer,
getContentType(),
getCharacterEncoding(),
isScriptHidingEnabled,
isScriptInAttributeValueEnabled,
disableUnicodeEscaping,
isPartial);
responseWriter.dontEscape = this.dontEscape;
responseWriter.writingCdata = this.writingCdata;
return responseWriter;
} catch (FacesException e) {
// This should never happen
throw new IllegalStateException();
}
}
/** Output the text for the end of a document. */
@Override
public void endDocument() throws IOException {
/*
* If the FastStringWriter is kept because of an error in <script>
* writing we get it here and write out the result. See issue #3473
*/
if (writer instanceof FastStringWriter) {
FastStringWriter fastStringWriter = (FastStringWriter) writer;
String result = fastStringWriter.getBuffer().toString();
fastStringWriter.reset();
writer = origWriter;
writer.write(result);
}
writer.flush();
}
/**
* <p>Write the end of an element. This method will first
* close any open element created by a call to
* <code>startElement()</code>.
*
* @param name Name of the element to be ended
*
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>name</code>
* is <code>null</code>
*/
@Override
public void endElement(String name) throws IOException {
if (name == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
}
// Keep track when we are exiting a script or style element
// for escaping purposes.
if ("script".equalsIgnoreCase(name)) {
withinScript = false;
}
if ("style".equalsIgnoreCase(name)) {
withinStyle = false;
}
// always turn escaping back on once an element ends
if (!withinScript && !withinStyle) {
dontEscape = false;
}
isXhtml = getContentType().equals(
RIConstants.XHTML_CONTENT_TYPE);
if (isScriptOrStyle(name)
&& !scriptOrStyleSrc
&& writer instanceof FastStringWriter) {
String result = ((FastStringWriter) writer).getBuffer().toString();
writer = origWriter;
if (result != null) {
String trim = result.trim();
if (isXhtml) {
if (isScript) {
Matcher
cdataStartSlashSlash =
CDATA_START_SLASH_SLASH.matcher(trim),
cdataEndSlashSlash =
CDATA_END_SLASH_SLASH.matcher(trim),
cdataStartSlashStar =
CDATA_START_SLASH_STAR.matcher(trim),
cdataEndSlashStar =
CDATA_END_SLASH_STAR.matcher(trim);
int trimLen = trim.length(), start, end;
// case 1 start is // end is //
if (cdataStartSlashSlash.find() &&
cdataEndSlashSlash.find()) {
start = cdataStartSlashSlash.end() - cdataStartSlashSlash.start();
end = trimLen - (cdataEndSlashSlash.end() - cdataEndSlashSlash.start());
writer.write(trim.substring(start, end));
}
// case 2 start is // end is /* */
else if ((null != cdataStartSlashSlash.reset() && cdataStartSlashSlash.find()) &&
cdataEndSlashStar.find()) {
start = cdataStartSlashSlash.end() - cdataStartSlashSlash.start();
end = trimLen - (cdataEndSlashStar.end() - cdataEndSlashStar.start());
writer.write(trim.substring(start, end));
}
// case 3 start is /* */ end is /* */
else if (cdataStartSlashStar.find() &&
(null != cdataEndSlashStar.reset() && cdataEndSlashStar.find())) {
start = cdataStartSlashStar.end() - cdataStartSlashStar.start();
end = trimLen - (cdataEndSlashStar.end() - cdataEndSlashStar.start());
writer.write(trim.substring(start, end));
}
// case 4 start is /* */ end is //
else if ((null != cdataStartSlashStar.reset() && cdataStartSlashStar.find()) &&
(null != cdataEndSlashStar.reset() && cdataEndSlashSlash.find())) {
start = cdataStartSlashStar.end() - cdataStartSlashStar.start();
end = trimLen - (cdataEndSlashSlash.end() - cdataEndSlashSlash.start());
writer.write(trim.substring(start, end));
}
// case 5 no commented out cdata present.
else {
writer.write(result);
}
} else {
if (trim.startsWith("<![CDATA[") && trim.endsWith("]]>")) {
writer.write(trim.substring(9, trim.length() - 3));
} else {
writer.write(result);
}
}
} else {
if (trim.startsWith("<!--") && trim.endsWith("//-->")) {
writer.write(trim.substring(4, trim.length() - 5));
} else {
writer.write(result);
}
}
}
if (isXhtml) {
if (!writingCdata) {
if (isScript) {
writer.write("\n//]]>\n");
} else {
writer.write("\n]]>\n");
}
}
} else {
if (isScriptHidingEnabled) {
writer.write("\n//-->\n");
}
}
}
isScript = false;
isStyle = false;
dontEscape = false;
if ("cdata".equalsIgnoreCase(name)) {
endCDATA();
return;
}
// See if we need to close the start of the last element
if (closeStart) {
boolean isEmptyElement = HtmlUtils.isEmptyElement(name);
// Tricky: we need to use the writer ivar here, rather than the
// one from the FacesContext because we don't want
// spurious /> characters to appear in the output.
if (isEmptyElement) {
flushAttributes();
writer.write(" />");
closeStart = false;
popElementName(name);
return;
}
flushAttributes();
writer.write('>');
closeStart = false;
}
writer.write("</");
writer.write(popElementName(name));
writer.write('>');
}
/**
* @return the character encoding, such as "ISO-8859-1" for this
* ResponseWriter. Refer to:
* <a href="http://www.iana.org/assignments/character-sets">theIANA</a>
* for a list of character encodings.
*/
@Override
public String getCharacterEncoding() {
return encoding;
}
/**
* <p>Write the text that should begin a response.</p>
*
* @throws IOException if an input/output error occurs
*/
@Override
public void startDocument() throws IOException {
// do nothing;
}
/**
* <p>Write the start of an element, up to and including the
* element name. Clients call <code>writeAttribute()</code> or
* <code>writeURIAttribute()</code> methods to add attributes after
* calling this method.
*
* @param name Name of the starting element
* @param componentForElement The UIComponent instance that applies to this
* element. This argument may be <code>null</code>.
*
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>name</code>
* is <code>null</code>
*/
@Override
public void startElement(String name, UIComponent componentForElement)
throws IOException {
if (name == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
}
// Keep track if we are in either a script or style element so we
// know we do not want to escape.
if ("script".equalsIgnoreCase(name)) {
withinScript = true;
}
if ("style".equalsIgnoreCase(name)) {
withinStyle = true;
}
closeStartIfNecessary();
isScriptOrStyle(name);
scriptOrStyleSrc = false;
if ("cdata".equalsIgnoreCase(name)) {
isCdata = true;
startCDATA();
return;
} else if (writingCdata) {
// starting an element within a cdata section,
// keep escaping disabled
isCdata = false;
writingCdata = true;
}
if (null != componentForElement) {
Map<String, Object> passThroughAttrs = componentForElement.getPassThroughAttributes(false);
if (null != passThroughAttrs && !passThroughAttrs.isEmpty()) {
considerPassThroughAttributes(passThroughAttrs);
}
}
writer.write('<');
String elementName = pushElementName(name);
writer.write(elementName);
closeStart = true;
}
/**
* Starts a CDATA block. Nested blocks are not allowed.
*
* @since 2.0
* @throws IOException on a read/write error
* @throws IllegalStateException If startCDATA is called a second time before endCDATA.
*/
// RELEASE_PENDING_2_1 edburns, rogerk - need to expand on this description.
@Override
public void startCDATA() throws IOException {
if (writingCdata) {
throw new IllegalStateException("CDATA tags may not nest");
}
closeStartIfNecessary();
writingCdata = true;
writer.write("<![CDATA[");
closeStart = false;
}
/**
* Closes the CDATA block.
*
* @since 2.0
* @throws IOException
*/
// RELEASE_PENDING_2_1 edburns, rogerk - need to expand on this description.
@Override
public void endCDATA() throws IOException {
closeStartIfNecessary();
writer.write("]]>");
writingCdata = false;
}
@Override
public void write(char[] cbuf) throws IOException {
closeStartIfNecessary();
writer.write(cbuf);
}
@Override
public void write(int c) throws IOException {
closeStartIfNecessary();
writer.write(c);
}
@Override
public void write(String str) throws IOException {
closeStartIfNecessary();
writer.write(str);
}
@Override
public void write(char[] cbuf, int off, int len) throws IOException {
closeStartIfNecessary();
writer.write(cbuf, off, len);
}
@Override
public void write(String str, int off, int len) throws IOException {
closeStartIfNecessary();
writer.write(str, off, len);
}
/**
* <p>Write a properly escaped attribute name and the corresponding
* value. The value text will be converted to a String if
* necessary. This method may only be called after a call to
* <code>startElement()</code>, and before the opened element has been
* closed.</p>
*
* @param name Attribute name to be added
* @param value Attribute value to be added
* @param componentPropertyName The name of the component property to
* which this attribute argument applies. This argument may be
* <code>null</code>.
*
* @throws IllegalStateException if this method is called when there
* is no currently open element
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>name</code> is <code>null</code>
*/
@Override
public void writeAttribute(String name, Object value,
String componentPropertyName)
throws IOException {
if (name == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
}
if (value == null) {
return;
}
if (isCdata) {
return;
}
if (containsPassThroughAttribute(name)) {
return;
}
if (name.equalsIgnoreCase("src") && isScriptOrStyle()) {
scriptOrStyleSrc = true;
}
Class valueClass = value.getClass();
// Output Boolean values specially
if (valueClass == Boolean.class) {
if (Boolean.TRUE.equals(value)) {
// NOTE: HTML 4.01 states that boolean attributes
// may legally take a single value which is the
// name of the attribute itself or appear using
// minimization.
// http://www.w3.org/TR/html401/intro/sgmltut.html#h-3.3.4.2
attributesBuffer.write(' ');
attributesBuffer.write(name);
attributesBuffer.write("=\"");
attributesBuffer.write(name);
attributesBuffer.write('"');
}
} else {
attributesBuffer.write(' ');
attributesBuffer.write(name);
attributesBuffer.write("=\"");
// write the attribute value
String val = value.toString();
ensureTextBufferCapacity(val);
HtmlUtils.writeAttribute(attributesBuffer,
escapeUnicode,
escapeIso,
buffer,
val,
textBuffer,
isScriptInAttributeValueEnabled);
attributesBuffer.write('"');
}
}
/**
* <p>Write a comment string containing the specified text.
* The text will be converted to a String if necessary.
* If there is an open element that has been created by a call
* to <code>startElement()</code>, that element will be closed
* first.</p>
*
* @param comment Text content of the comment
*
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>comment</code>
* is <code>null</code>
*/
@Override
public void writeComment(Object comment) throws IOException {
if (comment == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID));
}
if (writingCdata) {
return;
}
closeStartIfNecessary();
// Don't include a trailing space after the '<!--'
// or a leading space before the '-->' to support
// IE conditional commentsoth
writer.write("<!--");
String str = comment.toString();
ensureTextBufferCapacity(str);
HtmlUtils.writeText(writer, true, true, buffer, str, textBuffer);
writer.write("-->");
}
/**
* <p>Write a properly escaped single character, If there
* is an open element that has been created by a call to
* <code>startElement()</code>, that element will be closed first.</p>
* <p/>
* <p>All angle bracket occurrences in the argument must be escaped
* using the > < syntax.</p>
*
* @param text Text to be written
*
* @throws IOException if an input/output error occurs
*/
public void writeText(char text) throws IOException {
closeStartIfNecessary();
if (dontEscape) {
writer.write(text);
} else if (isPartial || !writingCdata) {
charHolder[0] = text;
HtmlUtils.writeText(writer, escapeUnicode, escapeIso, buffer, charHolder);
} else { // if writingCdata
assert writingCdata;
charHolder[0] = text;
writeEscaped(charHolder, 0, 1);
}
}
/**
* <p>Write properly escaped text from a character array.
* The output from this command is identical to the invocation:
* <code>writeText(c, 0, c.length)</code>.
* If there is an open element that has been created by a call to
* <code>startElement()</code>, that element will be closed first.</p>
* </p>
* <p/>
* <p>All angle bracket occurrences in the argument must be escaped
* using the > < syntax.</p>
*
* @param text Text to be written
*
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>text</code>
* is <code>null</code>
*/
public void writeText(char text[]) throws IOException {
if (text == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
}
closeStartIfNecessary();
if (dontEscape) {
writer.write(text);
} else if (isPartial || !writingCdata) {
HtmlUtils.writeText(writer, escapeUnicode, escapeIso, buffer, text);
} else { // if writingCdata
assert writingCdata;
writeEscaped(text, 0, text.length);
}
}
/**
* <p>Write a properly escaped object. The object will be converted
* to a String if necessary. If there is an open element
* that has been created by a call to <code>startElement()</code>,
* that element will be closed first.</p>
*
* @param text Text to be written
* @param componentPropertyName The name of the component property to
* which this text argument applies. This argument may be <code>null</code>.
*
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>text</code>
* is <code>null</code>
*/
@Override
public void writeText(Object text, String componentPropertyName)
throws IOException {
if (text == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
}
closeStartIfNecessary();
String textStr = text.toString();
if (dontEscape) {
writer.write(textStr);
} else if (isPartial || !writingCdata) {
ensureTextBufferCapacity(textStr);
HtmlUtils.writeText(writer,
escapeUnicode,
escapeIso,
buffer,
textStr,
textBuffer);
} else { // if writingCdata
assert writingCdata;
int textLen = textStr.length();
if (textLen > cdataTextBufferSize) {
writeEscaped(textStr.toCharArray(), 0, textLen);
} else if (textLen >= 16) { // >16, < cdataTextBufferSize
textStr.getChars(0, textLen, cdataTextBuffer, 0);
writeEscaped(cdataTextBuffer, 0, textLen);
} else { // <16
for (int i=0; i < textLen; i++) {
cdataTextBuffer[i] = textStr.charAt(i);
}
writeEscaped(cdataTextBuffer, 0, textLen);
}
}
}
/**
* <p>Write properly escaped text from a character array.
* If there is an open element that has been created by a call
* to <code>startElement()</code>, that element will be closed
* first.</p>
* <p/>
* <p>All angle bracket occurrences in the argument must be escaped
* using the > < syntax.</p>
*
* @param text Text to be written
* @param off Starting offset (zero-relative)
* @param len Number of characters to be written
*
* @throws IndexOutOfBoundsException if the calculated starting or
* ending position is outside the bounds of the character array
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>text</code>
* is <code>null</code>
*/
@Override
public void writeText(char text[], int off, int len)
throws IOException {
if (text == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
}
if (off < 0 || off > text.length || len < 0 || len > text.length) {
throw new IndexOutOfBoundsException();
}
closeStartIfNecessary();
// optimize away zero length write, called by Facelets to close tags
if (len == 0) return;
if (dontEscape) {
writer.write(text, off, len);
} else if (isPartial || !writingCdata) {
HtmlUtils.writeText(writer, escapeUnicode, escapeIso, buffer, text, off, len);
} else { // if (writingCdata)
assert writingCdata;
writeEscaped(text, off, len);
}
}
/**
* <p>Write a properly encoded URI attribute name and the corresponding
* value. The value text will be converted to a String if necessary).
* This method may only be called after a call to
* <code>startElement()</code>, and before the opened element has been
* closed.</p>
*
* @param name Attribute name to be added
* @param value Attribute value to be added
* @param componentPropertyName The name of the component property to
* which this attribute argument applies. This argument may be
* <code>null</code>.
*
* @throws IllegalStateException if this method is called when there
* is no currently open element
* @throws IOException if an input/output error occurs
* @throws NullPointerException if <code>name</code> or
* <code>value</code> is <code>null</code>
*/
@Override
public void writeURIAttribute(String name, Object value,
String componentPropertyName)
throws IOException {
if (null != name && containsPassThroughAttribute(name)) {
return;
}
writeURIAttributeIgnoringPassThroughAttributes(name, value,
componentPropertyName, false);
}
private void writeURIAttributeIgnoringPassThroughAttributes(String name, Object value,
String componentPropertyName, boolean isPassthrough) throws IOException {
if (name == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
}
if (value == null) {
throw new NullPointerException(MessageUtils.getExceptionMessageString(
MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "value"));
}
if (isCdata) {
return;
}
if (name.equals(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY)) {
return;
}
if (name.equalsIgnoreCase("src") && isScriptOrStyle()) {
scriptOrStyleSrc = true;
}
attributesBuffer.write(' ');
attributesBuffer.write(name);
attributesBuffer.write("=\"");
String stringValue = value.toString();
ensureTextBufferCapacity(stringValue);
// Javascript URLs should not be URL-encoded
if (stringValue.startsWith("javascript:") || isPassthrough) {
HtmlUtils.writeAttribute(attributesBuffer,
escapeUnicode,
escapeIso,
buffer,
stringValue,
textBuffer,
isScriptInAttributeValueEnabled);
} else {
HtmlUtils.writeURL(attributesBuffer,
stringValue,
textBuffer,
encoding);
}
attributesBuffer.write('"');
}
// --------------------------------------------------------- Private Methods
private void ensureTextBufferCapacity(String source) {
int len = source.length();
if (textBuffer.length < len) {
textBuffer = new char[len * 2];
}
if (buffer.length < len) {
buffer = new char[len * 2];
}
}
/**
* This method automatically closes a previous element (if not
* already closed).
* @throws IOException if an error occurs writing
*/
private void closeStartIfNecessary() throws IOException {
if (closeStart) {
flushAttributes();
writer.write('>');
closeStart = false;
if (isScriptOrStyle() && !scriptOrStyleSrc) {
isXhtml = getContentType().equals(
RIConstants.XHTML_CONTENT_TYPE);
if (isXhtml) {
if (!writingCdata) {
if (isScript) {
writer.write("\n//<![CDATA[\n");
} else {
writer.write("\n<![CDATA[\n");
}
}
} else {
if (isScriptHidingEnabled) {
writer.write("\n<!--\n");
}
}
origWriter = writer;
if (scriptBuffer == null) {
scriptBuffer = new FastStringWriter(1024);
}
scriptBuffer.reset();
writer = scriptBuffer;
isScript = false;
isStyle = false;
}
}
}
private void considerPassThroughAttributes(Map<String, Object> toCopy) {
assert(null != toCopy && !toCopy.isEmpty());
if (null != passthroughAttributes) {
throw new IllegalStateException("Error, this method should only be called once per instance.");
}
passthroughAttributes = new ConcurrentHashMap<>(toCopy);
}
private boolean containsPassThroughAttribute(String attrName) {
boolean result = false;
if (null != passthroughAttributes) {
result = passthroughAttributes.containsKey(attrName);
}
return result;
}
private void flushAttributes() throws IOException {
boolean hasPassthroughAttributes =
null != passthroughAttributes && !passthroughAttributes.isEmpty();
if (hasPassthroughAttributes) {
FacesContext context = FacesContext.getCurrentInstance();
for (Map.Entry<String, Object> entry : passthroughAttributes.entrySet()) {
Object valObj = entry.getValue();
String val = getAttributeValue(context, valObj);
String key = entry.getKey();
if (val != null) {
writeURIAttributeIgnoringPassThroughAttributes(key, val, key, true);
}
}
}
// a little complex, but the end result is, potentially, two
// fewer temp objects created per call.
StringBuilder b = attributesBuffer.getBuffer();
int totalLength = b.length();
if (totalLength != 0) {
int curIdx = 0;
while (curIdx < totalLength) {
if ((totalLength - curIdx) > buffer.length) {
int end = curIdx + buffer.length;
b.getChars(curIdx, end, buffer, 0);
writer.write(buffer);
curIdx += buffer.length;
} else {
int len = totalLength - curIdx;
b.getChars(curIdx, curIdx + len, buffer, 0);
writer.write(buffer, 0, len);
curIdx += len;
}
}
attributesBuffer.reset();
}
if (hasPassthroughAttributes) {
passthroughAttributes.clear();
passthroughAttributes = null;
}
}
private String getAttributeValue(FacesContext context, Object valObj) {
String val;
if (valObj instanceof ValueExpression) {
Object result = ((ValueExpression) valObj).getValue(context.getELContext());
val = result != null ? result.toString() : null;
} else {
val = valObj.toString();
}
return val;
}
private String pushElementName(String original) {
if (original.equals("option")) {
if(elementNames == null) {
elementNames = new LinkedList<>();
}
elementNames.push(original);
return original;
}
String name = getElementName(original);
if(passthroughAttributes != null) {
passthroughAttributes.remove(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY);
if(passthroughAttributes.isEmpty()) {
passthroughAttributes = null;
}
}
if(!original.equals(name) || elementNames != null) {
if(elementNames == null) {
elementNames = new LinkedList<>();
}
elementNames.push(name);
}
return name;
}
private String popElementName(String original) {
if(elementNames == null || elementNames.isEmpty()) {
return original;
}
return elementNames.pop();
}
private String getElementName(String name) {
if(containsPassThroughAttribute(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY)) {
FacesContext context = FacesContext.getCurrentInstance();
String elementName = getAttributeValue(context, passthroughAttributes.get(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY));
if(elementName != null && elementName.trim().length() > 0) {
return elementName;
}
}
return name;
}
private boolean isScriptOrStyle(String name) {
if ("script".equalsIgnoreCase(name)) {
isScript = true;
dontEscape = true;
} else if ("style".equalsIgnoreCase(name)) {
isStyle = true;
dontEscape = true;
} else {
isScript = false;
isStyle = false;
if (!withinScript && !withinStyle) {
dontEscape = false;
}
}
return (isScript || isStyle);
}
private boolean isScriptOrStyle() {
return (isScript || isStyle);
}
/*
* Method to escape all CDATA instances in a character array, then write to writer.
*
* This method looks for occurrences of "<![" and "]]>"
*/
private void writeEscaped(char cbuf[], int offset, int length) throws IOException {
if (cbuf == null || cbuf.length == 0 || length == 0) {
return;
}
if (offset < 0 || length < 0 || offset + length > cbuf.length ) {
throw new IndexOutOfBoundsException("off < 0 || len < 0 || off + len > cbuf.length");
}
// Single char case
if (length == 1) {
if (cbuf[offset] == '<') {
appendBuffer(ESCAPEDLT);
} else if (cbuf[offset] == ']') {
appendBuffer(ESCAPEDSINGLEBRACKET);
} else {
appendBuffer(cbuf[offset]);
}
flushBuffer();
return;
}
// two char case
if (length == 2) {
if (cbuf[offset] == '<' && cbuf[offset + 1] == '!') {
appendBuffer(ESCAPEDLT);
appendBuffer(cbuf[offset + 1]);
} else if (cbuf[offset] == ']' && cbuf[offset + 1] == ']') {
appendBuffer(ESCAPEDSINGLEBRACKET);
appendBuffer(ESCAPEDSINGLEBRACKET);
} else {
appendBuffer(cbuf[offset]);
appendBuffer(cbuf[offset + 1]);
}
flushBuffer();
return;
}
// > 2 char case
boolean last = false;
for (int i = offset; i < length - 2; i++) {
if (cbuf[i] == '<' && cbuf[i + 1] == '!' && cbuf[i + 2] == '[') {
appendBuffer(ESCAPEDSTART);
i += 2;
} else if (cbuf[i] == ']' && cbuf[i + 1] == ']' && cbuf[i + 2] == '>') {
appendBuffer(ESCAPEDEND);
i += 2;
} else {
appendBuffer(cbuf[i]);
}
if (i == (offset + length - 1)) {
last = true;
}
}
// if we didn't look at the last characters, look at them now
if (!last) {
if (cbuf[offset + length - 2] == '<') {
appendBuffer(ESCAPEDLT);
} else if (cbuf[offset + length - 2] == ']') {
appendBuffer(ESCAPEDSINGLEBRACKET);
} else {
appendBuffer(cbuf[offset + length - 2]);
}
if (cbuf[offset + length - 1] == '<') {
appendBuffer(ESCAPEDLT);
} else if (cbuf[offset + length - 1] == ']') {
appendBuffer(ESCAPEDSINGLEBRACKET);
} else {
appendBuffer(cbuf[offset + length - 1]);
}
}
flushBuffer();
}
/*
* append a character array to the cdatabuffer
*/
private void appendBuffer(char[] cbuf) throws IOException {
if (cbuf.length + cdataBufferLength >= cdataBufferSize) {
flushBuffer();
}
if (cbuf.length >= cdataBufferSize) { // bigger than the buffer, direct write
writer.write(cbuf);
}
System.arraycopy(cbuf, 0, cdataBuffer, cdataBufferLength, cbuf.length);
cdataBufferLength = cdataBufferLength + cbuf.length;
}
/*
* append a character to the cdatabuffer
*/
private void appendBuffer(char c) throws IOException {
if (cdataBufferLength + 1 >= cdataBufferSize) {
flushBuffer();
}
cdataBuffer[cdataBufferLength] = c;
cdataBufferLength++;
}
/*
* flush the cdatabuffer to the writer
*/
private void flushBuffer() throws IOException {
if (cdataBufferLength == 0) {
return;
}
writer.write(cdataBuffer, 0, cdataBufferLength);
cdataBufferLength = 0;
}
}