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