001/*
002 * $Id: HtmlResponseWriter.java,v 1.47.4.13 2008/12/11 17:53:22 rlubke Exp $
003 */
004
005/*
006 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
007 *
008 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
009 *
010 * The contents of this file are subject to the terms of either the GNU
011 * General Public License Version 2 only ("GPL") or the Common Development
012 * and Distribution License("CDDL") (collectively, the "License").  You
013 * may not use this file except in compliance with the License. You can obtain
014 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
015 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
016 * language governing permissions and limitations under the License.
017 *
018 * When distributing the software, include this License Header Notice in each
019 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
020 * Sun designates this particular file as subject to the "Classpath" exception
021 * as provided by Sun in the GPL Version 2 section of the License file that
022 * accompanied this code.  If applicable, add the following below the License
023 * Header, with the fields enclosed by brackets [] replaced by your own
024 * identifying information: "Portions Copyrighted [year]
025 * [name of copyright owner]"
026 *
027 * Contributor(s):
028 *
029 * If you wish your version of this file to be governed by only the CDDL or
030 * only the GPL Version 2, indicate your decision by adding "[Contributor]
031 * elects to include this software in this distribution under the [CDDL or GPL
032 * Version 2] license."  If you don't indicate a single choice of license, a
033 * recipient has the option to distribute your version of this file under
034 * either the CDDL, the GPL Version 2 or to extend the choice of license to
035 * its licensees as provided above.  However, if you add GPL Version 2 code
036 * and therefore, elected the GPL Version 2 license, then the option applies
037 * only if the new code is made subject to such option by the copyright
038 * holder.
039 *
040 * Portions Copyrighted 2013 Nuxeo
041 */
042
043package org.nuxeo.ecm.platform.ui.web.util;
044
045import java.io.IOException;
046import java.io.Writer;
047import java.util.regex.Matcher;
048import java.util.regex.Pattern;
049
050import javax.faces.FacesException;
051import javax.faces.component.UIComponent;
052import javax.faces.context.ExternalContext;
053import javax.faces.context.FacesContext;
054import javax.faces.context.ResponseWriter;
055
056import com.sun.faces.RIConstants;
057import com.sun.faces.config.WebConfiguration;
058import com.sun.faces.io.FastStringWriter;
059import com.sun.faces.renderkit.html_basic.HtmlResponseWriter;
060import com.sun.faces.util.HtmlUtils;
061import com.sun.faces.util.MessageUtils;
062
063/**
064 * CSV specific response writer copied pasted from com.sun.faces.renderkit.html_basic.HtmlResponseWriter without the
065 * HTML encode part.
066 *
067 * @since 5.9.1, 5.8-HF01
068 */
069public class NXHtmlResponseWriter extends ResponseWriter {
070
071    // Content Type for this Writer.
072    //
073    private String contentType = "text/html";
074
075    // Character encoding of that Writer - this may be null
076    // if the encoding isn't known.
077    //
078    private String encoding = null;
079
080    // Writer to use for output;
081    //
082    private Writer writer = null;
083
084    // True when we need to close a start tag
085    //
086    private boolean closeStart;
087
088    // Configuration flag regarding disableUnicodeEscaping
089    //
090    private WebConfiguration.DisableUnicodeEscaping disableUnicodeEscaping;
091
092    // Flag to escape Unicode
093    //
094    private boolean escapeUnicode;
095
096    // Flag to escape ISO-8859-1 codes
097    //
098    private boolean escapeIso;
099
100    // flag to indicate we're writing a CDATA section
101    private boolean writingCdata;
102
103    // flat to indicate the current element is CDATA
104    private boolean isCdata;
105
106    // flag to indicate that we're writing a 'script' or 'style' element
107    private boolean isScript;
108
109    // flag to indicate that we're writing a 'style' element
110    private boolean isStyle;
111
112    // flag to indicate that we're writing a 'src' attribute as part of
113    // 'script' or 'style' element
114    private boolean scriptOrStyleSrc;
115
116    // flag to indicate if the content type is Xhtml
117    private boolean isXhtml;
118
119    // HtmlResponseWriter to use when buffering is required
120    private Writer origWriter;
121
122    // Keep one instance of the script buffer per Writer
123    private FastStringWriter scriptBuffer;
124
125    // Keep one instance of attributesBuffer to buffer the writting
126    // of all attributes for a particular element to reducr the number
127    // of writes
128    private FastStringWriter attributesBuffer;
129
130    // Enables hiding of inlined script and style
131    // elements from old browsers
132    private Boolean isScriptHidingEnabled;
133
134    // Enables scripts to be included in attribute values
135    private Boolean isScriptInAttributeValueEnabled;
136
137    // Internal buffer used when outputting properly escaped information
138    // using HtmlUtils class.
139    //
140    private char[] buffer = new char[1028];
141
142    // Internal buffer for to store the result of String.getChars() for
143    // values passed to the writer as String to reduce the overhead
144    // of String.charAt(). This buffer will be grown, if necessary, to
145    // accomodate larger values.
146    private char[] textBuffer = new char[128];
147
148    static final Pattern CDATA_START_SLASH_SLASH;
149
150    static final Pattern CDATA_END_SLASH_SLASH;
151
152    static final Pattern CDATA_START_SLASH_STAR;
153
154    static final Pattern CDATA_END_SLASH_STAR;
155
156    static {
157        // At the beginning of a line, match // followed by any amount of
158        // whitespace, followed by <![CDATA[
159        CDATA_START_SLASH_SLASH = Pattern.compile("^//\\s*\\Q<![CDATA[\\E");
160
161        // At the end of a line, match // followed by any amout of whitespace,
162        // followed by ]]>
163        CDATA_END_SLASH_SLASH = Pattern.compile("//\\s*\\Q]]>\\E$");
164
165        // At the beginning of a line, match /* followed by any amout of
166        // whitespace, followed by <![CDATA[, followed by any amount of
167        // whitespace,
168        // followed by */
169        CDATA_START_SLASH_STAR = Pattern.compile("^/\\*\\s*\\Q<![CDATA[\\E\\s*\\*/");
170
171        // At the end of a line, match /* followed by any amount of whitespace,
172        // followed by ]]> followed by any amount of whitespace, followed by */
173        CDATA_END_SLASH_STAR = Pattern.compile("/\\*\\s*\\Q]]>\\E\\s*\\*/$");
174
175    }
176
177    // ------------------------------------------------------------
178    // Constructors
179
180    /**
181     * Constructor sets the <code>ResponseWriter</code> and encoding, and enables script hiding by default.
182     *
183     * @param writer the <code>ResponseWriter</code>
184     * @param contentType the content type.
185     * @param encoding the character encoding.
186     * @throws javax.faces.FacesException the encoding is not recognized.
187     */
188    public NXHtmlResponseWriter(Writer writer, String contentType, String encoding) throws FacesException {
189        this(writer, contentType, encoding, null, null, null);
190    }
191
192    /**
193     * <p>
194     * Constructor sets the <code>ResponseWriter</code> and encoding.
195     * </p>
196     * <p>
197     * The argument configPrefs is a map of configurable prefs that affect this instance's behavior. Supported keys are:
198     * </p>
199     * <p>
200     * BooleanWebContextInitParameter.EnableJSStyleHiding: <code>true</code> if the writer should attempt to hide JS
201     * from older browsers
202     * </p>
203     *
204     * @param writer the <code>ResponseWriter</code>
205     * @param contentType the content type.
206     * @param encoding the character encoding.
207     * @throws javax.faces.FacesException the encoding is not recognized.
208     */
209    public NXHtmlResponseWriter(Writer writer, String contentType, String encoding, Boolean isScriptHidingEnabled,
210            Boolean isScriptInAttributeValueEnabled, WebConfiguration.DisableUnicodeEscaping disableUnicodeEscaping)
211            throws FacesException {
212
213        this.writer = writer;
214
215        if (null != contentType) {
216            this.contentType = contentType;
217        }
218
219        this.encoding = encoding;
220
221        // init those configuration parameters not yet initialized
222        WebConfiguration webConfig = null;
223        if (isScriptHidingEnabled == null) {
224            webConfig = getWebConfiguration(webConfig);
225            isScriptHidingEnabled = (null == webConfig) ? WebConfiguration.BooleanWebContextInitParameter.EnableJSStyleHiding.getDefaultValue()
226                    : webConfig.isOptionEnabled(WebConfiguration.BooleanWebContextInitParameter.EnableJSStyleHiding);
227        }
228
229        if (isScriptInAttributeValueEnabled == null) {
230            webConfig = getWebConfiguration(webConfig);
231            isScriptInAttributeValueEnabled = (null == webConfig) ? WebConfiguration.BooleanWebContextInitParameter.EnableScriptInAttributeValue.getDefaultValue()
232                    : webConfig.isOptionEnabled(WebConfiguration.BooleanWebContextInitParameter.EnableScriptInAttributeValue);
233        }
234
235        if (disableUnicodeEscaping == null) {
236            webConfig = getWebConfiguration(webConfig);
237            disableUnicodeEscaping = WebConfiguration.DisableUnicodeEscaping.getByValue((null == webConfig) ? WebConfiguration.WebContextInitParameter.DisableUnicodeEscaping.getDefaultValue()
238                    : webConfig.getOptionValue(WebConfiguration.WebContextInitParameter.DisableUnicodeEscaping));
239            if (disableUnicodeEscaping == null) {
240                disableUnicodeEscaping = WebConfiguration.DisableUnicodeEscaping.False;
241            }
242        }
243
244        // and store them for later use
245        this.isScriptHidingEnabled = isScriptHidingEnabled;
246        this.isScriptInAttributeValueEnabled = isScriptInAttributeValueEnabled;
247        this.disableUnicodeEscaping = disableUnicodeEscaping;
248
249        attributesBuffer = new FastStringWriter(128);
250
251        // Check the character encoding
252        if (!HtmlUtils.validateEncoding(encoding)) {
253            throw new IllegalArgumentException(
254                    MessageUtils.getExceptionMessageString(MessageUtils.ENCODING_ERROR_MESSAGE_ID));
255        }
256
257        String charsetName = encoding.toUpperCase();
258
259        switch (disableUnicodeEscaping) {
260        case True:
261            // html escape noting (except the dangerous characters like "<>'"
262            // etc
263            escapeUnicode = false;
264            escapeIso = false;
265            break;
266        case False:
267            // html escape any non-ascii character
268            escapeUnicode = true;
269            escapeIso = true;
270            break;
271        case Auto:
272            // is stream capable of rendering unicode, do not escape
273            escapeUnicode = !HtmlUtils.isUTFencoding(charsetName);
274            // is stream capable of rendering unicode or iso-8859-1, do not
275            // escape
276            escapeIso = !HtmlUtils.isISO8859_1encoding(charsetName) && !HtmlUtils.isUTFencoding(charsetName);
277            break;
278        }
279    }
280
281    private WebConfiguration getWebConfiguration(WebConfiguration webConfig) {
282        if (webConfig != null) {
283            return webConfig;
284        }
285
286        FacesContext context = FacesContext.getCurrentInstance();
287        if (null != context) {
288            ExternalContext extContext = context.getExternalContext();
289            if (null != extContext) {
290                webConfig = WebConfiguration.getInstance(extContext);
291            }
292        }
293        return webConfig;
294    }
295
296    // -------------------------------------------------- Methods From
297    // Closeable
298
299    /** Methods From <code>java.io.Writer</code> */
300
301    @Override
302    public void close() throws IOException {
303
304        closeStartIfNecessary();
305        writer.close();
306
307    }
308
309    // -------------------------------------------------- Methods From
310    // Flushable
311
312    /**
313     * Flush any buffered output to the contained writer.
314     *
315     * @throws IOException if an input/output error occurs.
316     */
317    @Override
318    public void flush() throws IOException {
319
320        // NOTE: Internal buffer's contents (the ivar "buffer") is
321        // written to the contained writer in the HtmlUtils class - see
322        // HtmlUtils.flushBuffer method; Buffering is done during
323        // writeAttribute/writeText - otherwise, output is written
324        // directly to the writer (ex: writer.write(....)..
325        //
326        // close any previously started element, if necessary
327        closeStartIfNecessary();
328
329    }
330
331    // ---------------------------------------------------------- Public
332    // Methods
333
334    /** @return the content type such as "text/html" for this ResponseWriter. */
335    @Override
336    public String getContentType() {
337
338        return contentType;
339
340    }
341
342    /**
343     * <p>
344     * Create a new instance of this <code>ResponseWriter</code> using a different <code>Writer</code>.
345     *
346     * @param writer The <code>Writer</code> that will be used to create another <code>ResponseWriter</code>.
347     */
348    @Override
349    public ResponseWriter cloneWithWriter(Writer writer) {
350
351        try {
352            return new HtmlResponseWriter(writer, getContentType(), getCharacterEncoding(), isScriptHidingEnabled,
353                    isScriptInAttributeValueEnabled, disableUnicodeEscaping, false);
354        } catch (FacesException e) {
355            // This should never happen
356            throw new IllegalStateException();
357        }
358
359    }
360
361    /** Output the text for the end of a document. */
362    @Override
363    public void endDocument() throws IOException {
364
365        writer.flush();
366
367    }
368
369    /**
370     * <p>
371     * Write the end of an element. This method will first close any open element created by a call to
372     * <code>startElement()</code>.
373     *
374     * @param name Name of the element to be ended
375     * @throws IOException if an input/output error occurs
376     * @throws NullPointerException if <code>name</code> is <code>null</code>
377     */
378    @Override
379    public void endElement(String name) throws IOException {
380
381        if (name == null) {
382            throw new NullPointerException(MessageUtils.getExceptionMessageString(
383                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
384        }
385
386        isXhtml = getContentType().equals(RIConstants.XHTML_CONTENT_TYPE);
387
388        if (isScriptOrStyle(name) && !scriptOrStyleSrc && writer instanceof FastStringWriter) {
389            String result = ((FastStringWriter) writer).getBuffer().toString();
390            writer = origWriter;
391
392            if (result != null) {
393                String trim = result.trim();
394                if (isXhtml) {
395                    if (isScript) {
396                        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);
397                        int trimLen = trim.length(), start, end;
398                        // case 1 start is // end is //
399                        if (cdataStartSlashSlash.find() && cdataEndSlashSlash.find()) {
400                            start = cdataStartSlashSlash.end() - cdataStartSlashSlash.start();
401                            end = trimLen - (cdataEndSlashSlash.end() - cdataEndSlashSlash.start());
402                            writer.write(trim.substring(start, end));
403                        }
404                        // case 2 start is // end is /* */
405                        else if ((null != cdataStartSlashSlash.reset() && cdataStartSlashSlash.find())
406                                && cdataEndSlashStar.find()) {
407                            start = cdataStartSlashSlash.end() - cdataStartSlashSlash.start();
408                            end = trimLen - (cdataEndSlashStar.end() - cdataEndSlashStar.start());
409                            writer.write(trim.substring(start, end));
410                        }
411                        // case 3 start is /* */ end is /* */
412                        else if (cdataStartSlashStar.find()
413                                && (null != cdataEndSlashStar.reset() && cdataEndSlashStar.find())) {
414                            start = cdataStartSlashStar.end() - cdataStartSlashStar.start();
415                            end = trimLen - (cdataEndSlashStar.end() - cdataEndSlashStar.start());
416                            writer.write(trim.substring(start, end));
417                        }
418                        // case 4 start is /* */ end is //
419                        else if ((null != cdataStartSlashStar.reset() && cdataStartSlashStar.find())
420                                && (null != cdataEndSlashStar.reset() && cdataEndSlashSlash.find())) {
421                            start = cdataStartSlashStar.end() - cdataStartSlashStar.start();
422                            end = trimLen - (cdataEndSlashSlash.end() - cdataEndSlashSlash.start());
423                            writer.write(trim.substring(start, end));
424                        }
425                        // case 5 no commented out cdata present.
426                        else {
427                            writer.write(result);
428                        }
429                    } else {
430                        if (trim.startsWith("<![CDATA[") && trim.endsWith("]]>")) {
431                            writer.write(trim.substring(9, trim.length() - 3));
432                        } else {
433                            writer.write(result);
434                        }
435                    }
436                } else {
437                    if (trim.startsWith("<!--") && trim.endsWith("//-->")) {
438                        writer.write(trim.substring(4, trim.length() - 5));
439                    } else {
440                        writer.write(result);
441                    }
442                }
443            }
444            if (isXhtml) {
445                if (!writingCdata) {
446                    if (isScript) {
447                        writer.write("\n//]]>\n");
448                    } else {
449                        writer.write("\n]]>\n");
450                    }
451                }
452            } else {
453                if (isScriptHidingEnabled) {
454                    writer.write("\n//-->\n");
455                }
456            }
457        }
458        isScript = false;
459        isStyle = false;
460        if ("cdata".equalsIgnoreCase(name)) {
461            writer.write("]]>");
462            writingCdata = false;
463            isCdata = false;
464            return;
465        }
466        // See if we need to close the start of the last element
467        if (closeStart) {
468            boolean isEmptyElement = HtmlUtils.isEmptyElement(name);
469
470            // Tricky: we need to use the writer ivar here, rather than the
471            // one from the FacesContext because we don't want
472            // spurious /> characters to appear in the output.
473            if (isEmptyElement) {
474                flushAttributes();
475                writer.write(" />");
476                closeStart = false;
477                return;
478            }
479            flushAttributes();
480            writer.write('>');
481            closeStart = false;
482        }
483
484        writer.write("</");
485        writer.write(name);
486        writer.write('>');
487
488    }
489
490    /**
491     * @return the character encoding, such as "ISO-8859-1" for this ResponseWriter. Refer to: <a
492     *         href="http://www.iana.org/assignments/character-sets" >theIANA</a> for a list of character encodings.
493     */
494    @Override
495    public String getCharacterEncoding() {
496
497        return encoding;
498
499    }
500
501    /**
502     * <p>
503     * Write the text that should begin a response.
504     * </p>
505     *
506     * @throws IOException if an input/output error occurs
507     */
508    @Override
509    public void startDocument() throws IOException {
510
511        // do nothing;
512
513    }
514
515    /**
516     * <p>
517     * Write the start of an element, up to and including the element name. Clients call <code>writeAttribute()</code>
518     * or <code>writeURIAttribute()</code> methods to add attributes after calling this method.
519     *
520     * @param name Name of the starting element
521     * @param componentForElement The UIComponent instance that applies to this element. This argument may be
522     *            <code>null</code>.
523     * @throws IOException if an input/output error occurs
524     * @throws NullPointerException if <code>name</code> is <code>null</code>
525     */
526    @Override
527    public void startElement(String name, UIComponent componentForElement) throws IOException {
528
529        if (name == null) {
530            throw new NullPointerException(MessageUtils.getExceptionMessageString(
531                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
532        }
533        closeStartIfNecessary();
534        isScriptOrStyle(name);
535        scriptOrStyleSrc = false;
536        if ("cdata".equalsIgnoreCase(name)) {
537            isCdata = true;
538            writingCdata = true;
539            writer.write("<![CDATA[");
540            closeStart = false;
541            return;
542        } else if (writingCdata) {
543            // starting an element within a cdata section,
544            // keep escaping disabled
545            isCdata = false;
546            writingCdata = true;
547        }
548
549        writer.write('<');
550        writer.write(name);
551        closeStart = true;
552
553    }
554
555    @Override
556    public void write(char[] cbuf) throws IOException {
557
558        closeStartIfNecessary();
559        writer.write(cbuf);
560
561    }
562
563    @Override
564    public void write(int c) throws IOException {
565
566        closeStartIfNecessary();
567        writer.write(c);
568
569    }
570
571    @Override
572    public void write(String str) throws IOException {
573
574        closeStartIfNecessary();
575        writer.write(str);
576
577    }
578
579    @Override
580    public void write(char[] cbuf, int off, int len) throws IOException {
581
582        closeStartIfNecessary();
583        writer.write(cbuf, off, len);
584
585    }
586
587    @Override
588    public void write(String str, int off, int len) throws IOException {
589
590        closeStartIfNecessary();
591        writer.write(str, off, len);
592
593    }
594
595    /**
596     * <p>
597     * Write a properly escaped attribute name and the corresponding value. The value text will be converted to a String
598     * if necessary. This method may only be called after a call to <code>startElement()</code>, and before the opened
599     * element has been closed.
600     * </p>
601     *
602     * @param name Attribute name to be added
603     * @param value Attribute value to be added
604     * @param componentPropertyName The name of the component property to which this attribute argument applies. This
605     *            argument may be <code>null</code>.
606     * @throws IllegalStateException if this method is called when there is no currently open element
607     * @throws IOException if an input/output error occurs
608     * @throws NullPointerException if <code>name</code> is <code>null</code>
609     */
610    @Override
611    public void writeAttribute(String name, Object value, String componentPropertyName) throws IOException {
612
613        if (name == null) {
614            throw new NullPointerException(MessageUtils.getExceptionMessageString(
615                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
616        }
617        if (value == null) {
618            return;
619        }
620
621        if (isCdata) {
622            return;
623        }
624
625        if (name.equalsIgnoreCase("src") && isScriptOrStyle()) {
626            scriptOrStyleSrc = true;
627        }
628
629        Class valueClass = value.getClass();
630
631        // Output Boolean values specially
632        if (valueClass == Boolean.class) {
633            if (Boolean.TRUE.equals(value)) {
634                // NOTE: HTML 4.01 states that boolean attributes
635                // may legally take a single value which is the
636                // name of the attribute itself or appear using
637                // minimization.
638                // http://www.w3.org/TR/html401/intro/sgmltut.html#h-3.3.4.2
639                attributesBuffer.write(' ');
640                attributesBuffer.write(name);
641                attributesBuffer.write("=\"");
642                attributesBuffer.write(name);
643                attributesBuffer.write('"');
644            }
645        } else {
646            attributesBuffer.write(' ');
647            attributesBuffer.write(name);
648            attributesBuffer.write("=\"");
649            // write the attribute value
650            String val = value.toString();
651            ensureTextBufferCapacity(val);
652            HtmlUtils.writeAttribute(attributesBuffer, escapeUnicode, escapeIso, buffer, val, textBuffer,
653                    isScriptInAttributeValueEnabled);
654            attributesBuffer.write('"');
655        }
656
657    }
658
659    /**
660     * <p>
661     * Write a comment string containing the specified text. The text will be converted to a String if necessary. If
662     * there is an open element that has been created by a call to <code>startElement()</code>, that element will be
663     * closed first.
664     * </p>
665     *
666     * @param comment Text content of the comment
667     * @throws IOException if an input/output error occurs
668     * @throws NullPointerException if <code>comment</code> is <code>null</code>
669     */
670    @Override
671    public void writeComment(Object comment) throws IOException {
672
673        if (comment == null) {
674            throw new NullPointerException(
675                    MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID));
676        }
677
678        if (writingCdata) {
679            return;
680        }
681
682        closeStartIfNecessary();
683        // Don't include a trailing space after the '<!--'
684        // or a leading space before the '-->' to support
685        // IE conditional commentsoth
686        writer.write("<!--");
687        writer.write(comment.toString());
688        writer.write("-->");
689
690    }
691
692    /**
693     * <p>
694     * Write a properly escaped single character, If there is an open element that has been created by a call to
695     * <code>startElement()</code>, that element will be closed first.
696     * </p>
697     * <p/>
698     * <p>
699     * All angle bracket occurrences in the argument must be escaped using the &amp;gt; &amp;lt; syntax.
700     * </p>
701     *
702     * @param text Text to be written
703     * @throws IOException if an input/output error occurs
704     */
705    public void writeText(char text) throws IOException {
706
707        closeStartIfNecessary();
708        writer.write(text);
709    }
710
711    /**
712     * <p>
713     * Write properly escaped text from a character array. The output from this command is identical to the invocation:
714     * <code>writeText(c, 0, c.length)</code>. If there is an open element that has been created by a call to
715     * <code>startElement()</code>, that element will be closed first.
716     * </p>
717     * </p>
718     * <p/>
719     * <p>
720     * All angle bracket occurrences in the argument must be escaped using the &amp;gt; &amp;lt; syntax.
721     * </p>
722     *
723     * @param text Text to be written
724     * @throws IOException if an input/output error occurs
725     * @throws NullPointerException if <code>text</code> is <code>null</code>
726     */
727    public void writeText(char text[]) throws IOException {
728
729        if (text == null) {
730            throw new NullPointerException(MessageUtils.getExceptionMessageString(
731                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
732        }
733        closeStartIfNecessary();
734        writer.write(text);
735
736    }
737
738    /**
739     * <p>
740     * Write a properly escaped object. The object will be converted to a String if necessary. If there is an open
741     * element that has been created by a call to <code>startElement()</code>, that element will be closed first.
742     * </p>
743     *
744     * @param text Text to be written
745     * @param componentPropertyName The name of the component property to which this text argument applies. This
746     *            argument may be <code>null</code>.
747     * @throws IOException if an input/output error occurs
748     * @throws NullPointerException if <code>text</code> is <code>null</code>
749     */
750    @Override
751    public void writeText(Object text, String componentPropertyName) throws IOException {
752
753        if (text == null) {
754            throw new NullPointerException(MessageUtils.getExceptionMessageString(
755                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
756        }
757        closeStartIfNecessary();
758        writer.write(text.toString());
759    }
760
761    /**
762     * <p>
763     * Write properly escaped text from a character array. If there is an open element that has been created by a call
764     * to <code>startElement()</code>, that element will be closed first.
765     * </p>
766     * <p/>
767     * <p>
768     * All angle bracket occurrences in the argument must be escaped using the &amp;gt; &amp;lt; syntax.
769     * </p>
770     *
771     * @param text Text to be written
772     * @param off Starting offset (zero-relative)
773     * @param len Number of characters to be written
774     * @throws IndexOutOfBoundsException if the calculated starting or ending position is outside the bounds of the
775     *             character array
776     * @throws IOException if an input/output error occurs
777     * @throws NullPointerException if <code>text</code> is <code>null</code>
778     */
779    @Override
780    public void writeText(char text[], int off, int len) throws IOException {
781
782        if (text == null) {
783            throw new NullPointerException(MessageUtils.getExceptionMessageString(
784                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "text"));
785        }
786        if (off < 0 || off > text.length || len < 0 || len > text.length) {
787            throw new IndexOutOfBoundsException();
788        }
789        closeStartIfNecessary();
790        writer.write(text, off, len);
791    }
792
793    /**
794     * <p>
795     * Write a properly encoded URI attribute name and the corresponding value. The value text will be converted to a
796     * String if necessary). This method may only be called after a call to <code>startElement()</code>, and before the
797     * opened element has been closed.
798     * </p>
799     *
800     * @param name Attribute name to be added
801     * @param value Attribute value to be added
802     * @param componentPropertyName The name of the component property to which this attribute argument applies. This
803     *            argument may be <code>null</code>.
804     * @throws IllegalStateException if this method is called when there is no currently open element
805     * @throws IOException if an input/output error occurs
806     * @throws NullPointerException if <code>name</code> or <code>value</code> is <code>null</code>
807     */
808    @Override
809    public void writeURIAttribute(String name, Object value, String componentPropertyName) throws IOException {
810
811        if (name == null) {
812            throw new NullPointerException(MessageUtils.getExceptionMessageString(
813                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "name"));
814        }
815        if (value == null) {
816            throw new NullPointerException(MessageUtils.getExceptionMessageString(
817                    MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID, "value"));
818        }
819
820        if (isCdata) {
821            return;
822        }
823
824        if (name.equalsIgnoreCase("src") && isScriptOrStyle()) {
825            scriptOrStyleSrc = true;
826        }
827
828        attributesBuffer.write(' ');
829        attributesBuffer.write(name);
830        attributesBuffer.write("=\"");
831
832        String stringValue = value.toString();
833        ensureTextBufferCapacity(stringValue);
834        // Javascript URLs should not be URL-encoded
835        if (stringValue.startsWith("javascript:")) {
836            HtmlUtils.writeAttribute(attributesBuffer, escapeUnicode, escapeIso, buffer, stringValue, textBuffer,
837                    isScriptInAttributeValueEnabled);
838        } else {
839            HtmlUtils.writeURL(attributesBuffer, stringValue, textBuffer, encoding);
840        }
841
842        attributesBuffer.write('"');
843
844    }
845
846    // --------------------------------------------------------- Private
847    // Methods
848
849    private void ensureTextBufferCapacity(String source) {
850        int len = source.length();
851        if (textBuffer.length < len) {
852            textBuffer = new char[len * 2];
853        }
854    }
855
856    /**
857     * This method automatically closes a previous element (if not already closed).
858     *
859     * @throws IOException if an error occurs writing
860     */
861    private void closeStartIfNecessary() throws IOException {
862
863        if (closeStart) {
864            flushAttributes();
865            writer.write('>');
866            closeStart = false;
867            if (isScriptOrStyle() && !scriptOrStyleSrc) {
868                isXhtml = getContentType().equals(RIConstants.XHTML_CONTENT_TYPE);
869                if (isXhtml) {
870                    if (!writingCdata) {
871                        if (isScript) {
872                            writer.write("\n//<![CDATA[\n");
873                        } else {
874                            writer.write("\n<![CDATA[\n");
875                        }
876                    }
877                } else {
878                    if (isScriptHidingEnabled) {
879                        writer.write("\n<!--\n");
880                    }
881                }
882                origWriter = writer;
883                if (scriptBuffer == null) {
884                    scriptBuffer = new FastStringWriter(1024);
885                }
886                scriptBuffer.reset();
887                writer = scriptBuffer;
888                isScript = false;
889                isStyle = false;
890            }
891        }
892
893    }
894
895    private void flushAttributes() throws IOException {
896
897        // a little complex, but the end result is, potentially, two
898        // fewer temp objects created per call.
899        StringBuilder b = attributesBuffer.getBuffer();
900        int totalLength = b.length();
901        if (totalLength != 0) {
902            int curIdx = 0;
903            while (curIdx < totalLength) {
904                if ((totalLength - curIdx) > buffer.length) {
905                    int end = curIdx + buffer.length;
906                    b.getChars(curIdx, end, buffer, 0);
907                    writer.write(buffer);
908                    curIdx += buffer.length;
909                } else {
910                    int len = totalLength - curIdx;
911                    b.getChars(curIdx, curIdx + len, buffer, 0);
912                    writer.write(buffer, 0, len);
913                    curIdx += len;
914                }
915            }
916            attributesBuffer.reset();
917        }
918
919    }
920
921    private boolean isScriptOrStyle(String name) {
922        if ("script".equalsIgnoreCase(name)) {
923            isScript = true;
924        } else if ("style".equalsIgnoreCase(name)) {
925            isStyle = true;
926        } else {
927            isScript = false;
928            isStyle = false;
929        }
930
931        return (isScript || isStyle);
932    }
933
934    private boolean isScriptOrStyle() {
935        return (isScript || isStyle);
936    }
937}