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