001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package com.sun.rave.web.ui.model;
042:
043: import java.io.UnsupportedEncodingException;
044: import java.net.URLEncoder;
045:
046: import javax.faces.FacesException;
047: import javax.faces.component.UIComponent;
048:
049: /**
050: * <p>Utility bean that serves as an accumulating buffer for
051: * well formed markup fragments typically generated by renderers.
052: * The fundamental API is modelled after <code>ResponseWriter</code>
053: * in JavaServer Faces.</p>
054: */
055:
056: public class Markup {
057:
058: // ------------------------------------------------------- Static Variables
059:
060: /*
061: * <p>Entities from HTML 4.0, section 24.2.1;
062: * character codes 0xA0 to 0xFF</p>
063: */
064: private static String[] ISO8859_1_Entities = new String[] { "nbsp",
065: "iexcl", "cent", "pound", "curren", "yen", "brvbar",
066: "sect", "uml", "copy", "ordf", "laquo", "not", "shy",
067: "reg", "macr", "deg", "plusmn", "sup2", "sup3", "acute",
068: "micro", "para", "middot", "cedil", "sup1", "ordm",
069: "raquo", "frac14", "frac12", "frac34", "iquest", "Agrave",
070: "Aacute", "Acirc", "Atilde", "Auml", "Aring", "AElig",
071: "Ccedil", "Egrave", "Eacute", "Ecirc", "Euml", "Igrave",
072: "Iacute", "Icirc", "Iuml", "ETH", "Ntilde", "Ograve",
073: "Oacute", "Ocirc", "Otilde", "Ouml", "times", "Oslash",
074: "Ugrave", "Uacute", "Ucirc", "Uuml", "Yacute", "THORN",
075: "szlig", "agrave", "aacute", "acirc", "atilde", "auml",
076: "aring", "aelig", "ccedil", "egrave", "eacute", "ecirc",
077: "euml", "igrave", "iacute", "icirc", "iuml", "eth",
078: "ntilde", "ograve", "oacute", "ocirc", "otilde", "ouml",
079: "divide", "oslash", "ugrave", "uacute", "ucirc", "uuml",
080: "yacute", "thorn", "yuml" };
081:
082: // ----------------------------------------------------- Instance Variables
083:
084: /**
085: * <p>Buffer into which we accumulate the created markup.</p>
086: */
087: private StringBuffer buffer = new StringBuffer();
088:
089: /**
090: * <p>The character encoding that we assume will be used when
091: * the markup contained in this instance is rendered. The
092: * default value ("ISO-8859-1") is an attempt to be conservative.</p>
093: */
094: private String encoding = "ISO-8859-1";
095:
096: /**
097: * <p>Flag indicating that an element is currently open.</p>
098: */
099: private boolean open = false;
100:
101: // ------------------------------------------------------------- Properties
102:
103: /**
104: * <p>Return the character encoding assumed to be used when the
105: * markup contained in this instance is ultimately rendered.</p>
106: */
107: public String getEncoding() {
108:
109: return this .encoding;
110:
111: }
112:
113: /**
114: * <p>Set the character encoding assumed to be used when the
115: * markup contained in this instance is ultimately rendered.</p>
116: *
117: * @param encoding The new character encoding
118: */
119: public void setEncoding(String encoding) {
120:
121: this .encoding = encoding;
122:
123: }
124:
125: /**
126: * <p>Return the markup that has been accumulated in this element,
127: * as a String suitable for direct transcription to the response
128: * buffer.</p>
129: */
130: public String getMarkup() {
131:
132: close();
133: return buffer.toString();
134:
135: }
136:
137: // --------------------------------------------------------- Public Methods
138:
139: /**
140: * <p>Clear any accumulated markup stored in this object,
141: * making it suitable for reuse.</p>
142: */
143: public void clear() {
144:
145: buffer.setLength(0);
146: open = false;
147:
148: }
149:
150: /**
151: * <p>Return the markup that has been accumulated in this element.
152: * This is an alias for the <code>getMarkup()</code> method.</p>
153: */
154: public String toString() {
155:
156: return getMarkup();
157:
158: }
159:
160: /**
161: * <p>Accumulate the start of a new element, up to and including
162: * the element name. Once this method has been called, clients
163: * can call <code>writeAttribute()</code> or
164: * <code>writeURIAttriute()</code> to add attributes and their
165: * corresponding values. The starting element will be closed
166: * on any subsequent call to <code>startElement()</code>,
167: * <code>writeComment()</code>, <code>writeText()</code>,
168: * <code>writeRaw()</code>, <code>endElement()</code>, or
169: * <code>getMarkup()</code>.</p>
170: *
171: * @param name Name of the element to be started
172: * @param component The <code>UIComponent</code> (if any)
173: * to which this element corresponds
174: *
175: * @exception NullPointerException if <code>name</code>
176: * is <code>null</code>
177: */
178: public void startElement(String name, UIComponent component) {
179:
180: if (name == null) {
181: throw new NullPointerException();
182: }
183: close();
184: buffer.append('<'); //NOI18N
185: buffer.append(name);
186: open = true;
187:
188: }
189:
190: /**
191: * <p>Accumulate the end of an element, after closing any open element
192: * created by a call to <code>startElement()</code>. Elements must be
193: * closed in the inverse order from which they were opened; it is an
194: * error to do otherwise.</p>
195: *
196: * @param name Name of the element to be ended
197: *
198: * @exception NullPointerException if <code>name</code>
199: * is <code>null</code>
200: */
201: public void endElement(String name) {
202:
203: if (name == null) {
204: throw new NullPointerException();
205: }
206: if (open) {
207: buffer.append('/'); //NOI18N
208: close();
209: } else {
210: buffer.append("</"); //NOI18N
211: buffer.append(name);
212: buffer.append('>'); //NOI18N
213: }
214:
215: }
216:
217: /**
218: * <p>Accumulate an attribute name and corresponding value. This
219: * method may only be called after a call to <code>startElement()</code>
220: * and before the opened element has been closed.</p>
221: *
222: * @param name Attribute name to be added
223: * @param value Attribute value to be added
224: * @param property Name of the component property or attribute (if any)
225: * of the <code>UIComponent</code> associated with the containing
226: * element, to which the generated attribute corresponds
227: *
228: * @exception IllegalStateException if this method is called
229: * when there is no currently open element
230: * @exception NullPointerException if <code>name</code>
231: * or <code>value</code> is <code>null</code>
232: */
233: public void writeAttribute(String name, Object value,
234: String property) {
235:
236: if ((name == null) || (value == null)) {
237: throw new NullPointerException();
238: }
239: if (!open) {
240: throw new IllegalStateException(
241: "No element is currently open"); //I18N - FIXME
242: }
243:
244: // Handle boolean values specially
245: Class clazz = value.getClass();
246: if (clazz == Boolean.class) {
247: if (Boolean.TRUE.equals(value)) {
248: // No attribute minimization for XHTML like markup
249: buffer.append(' '); //NOI18N
250: buffer.append(name);
251: buffer.append("=\""); //NOI18N
252: buffer.append(name);
253: buffer.append('"'); //NOI18N
254: // } else {
255: // Write nothing for false boolean attributes
256: }
257: return;
258: }
259:
260: // Render the attribute name and beginning of the value
261: buffer.append(' '); //NOI18N
262: buffer.append(name);
263: buffer.append("=\""); //NOI18N
264:
265: // Render the value itself
266: String text = value.toString();
267: int length = text.length();
268: for (int i = 0; i < length; i++) {
269: char ch = text.charAt(i);
270:
271: // Tilde or less...
272: if (ch < 0xA0) {
273: // If "?" or over, no escaping is needed (this covers
274: // most of the Latin alphabet)
275: if (ch >= 0x3f) {
276: buffer.append(ch);
277: } else if (ch >= 0x27) { // If above "'"...
278: // If between "'" and ";", no escaping is needed
279: if (ch < 0x3c) {
280: buffer.append(ch);
281: // Note - "<" isn't escaped in attributes, as per
282: // HTML spec
283: } else if (ch == '>') { //NOI18N
284: buffer.append(">"); //NOI18N
285: } else {
286: buffer.append(ch);
287: }
288: } else {
289: if (ch == '&') { //NOI18N
290: // HTML 4.0, section B.7.1: ampersands followed by
291: // an open brace don't get escaped
292: if ((i + 1 < length)
293: && (text.charAt(i + 1) == '{')) //NOI18N
294: buffer.append(ch);
295: else
296: buffer.append("&"); //NOI18N
297: } else if (ch == '"') {
298: buffer.append("""); //NOI18N
299: } else {
300: buffer.append(ch);
301: }
302: }
303: } else if (ch <= 0xff) {
304: // ISO-8859-1 entities: encode as needed
305: buffer.append('&'); //NOI18N
306: buffer.append(ISO8859_1_Entities[ch - 0xA0]);
307: buffer.append(';'); //NOI18N
308: } else {
309: // Double-byte characters to encode.
310: // PENDING: when outputting to an encoding that
311: // supports double-byte characters (UTF-8, for example),
312: // we should not be encoding
313: numeric(ch);
314: }
315: }
316:
317: // Render the end of the value
318: buffer.append('"'); //NOI18N
319:
320: }
321:
322: /**
323: * <p>Accumulate an attribute name and corresponding URI value. This
324: * method may only be called after a call to <code>startElement()</code>
325: * and before the opened element has been closed.</p>
326: *
327: * @param name Attribute name to be added
328: * @param value Attribute value to be added
329: * @param property Name of the component property or attribute (if any)
330: * of the <code>UIComponent</code> associated with the containing
331: * element, to which the generated attribute corresponds
332: *
333: * @exception IllegalStateException if this method is called
334: * when there is no currently open element
335: * @exception NullPointerException if <code>name</code>
336: * or <code>value</code> is <code>null</code>
337: */
338: public void writeURIAttribute(String name, Object value,
339: String property) {
340:
341: if ((name == null) || (value == null)) {
342: throw new NullPointerException();
343: }
344: if (!open) {
345: throw new IllegalStateException(
346: "No element is currently open"); //I18N - FIXME
347: }
348:
349: String text = value.toString();
350: if (text.startsWith("javascript:")) {
351: writeAttribute(name, value, property);
352: return;
353: }
354:
355: // Render the attribute name and beginning of the value
356: buffer.append(' '); //NOI18N
357: buffer.append(name);
358: buffer.append("=\""); //NOI18N
359:
360: // Render the value itself
361: int length = text.length();
362:
363: for (int i = 0; i < length; i++) {
364: char ch = text.charAt(i);
365:
366: if ((ch < 33) || (ch > 126)) {
367: if (ch == ' ') { //NOI18N
368: buffer.append('+'); //NOI18N
369: } else {
370: // ISO-8859-1. Blindly assume the character will be < 255.
371: // Not much we can do if it isn't.
372: hexadecimals(ch);
373: }
374: }
375: // DO NOT encode '%'. If you do, then for starters,
376: // we'll double-encode anything that's pre-encoded.
377: // And, what's worse, there becomes no way to use
378: // characters that must be encoded if you
379: // don't want them to be interpreted, like '?' or '&'.
380: // else if('%' == ch)
381: // {
382: // hexadecimals(ch);
383: // }
384: else if (ch == '"') {
385: buffer.append("%22"); //NOI18N
386: }
387: // Everything in the query parameters will be decoded
388: // as if it were in the request's character set. So use
389: // the real encoding for those!
390: else if (ch == '?') { //NOI18N
391: buffer.append('?'); //NOI18N
392: try {
393: buffer.append(URLEncoder.encode(text
394: .substring(i + 1), encoding));
395: } catch (UnsupportedEncodingException e) {
396: throw new FacesException(e);
397: }
398: break;
399: } else {
400: buffer.append(ch);
401: }
402: }
403:
404: // Render the end of the value
405: buffer.append('"'); //NOI18N
406:
407: }
408:
409: /**
410: * <p>Accumulate a comment containing the specified text, after
411: * converting that text to a String (if necessary) and performing
412: * any escaping appropriate for the markup language being rendered.</p>
413: *
414: * <p>If there is an open element that has been created by a call to
415: * <code>startElement()</code>, that element will be closed first.</p>
416: *
417: * @param comment Text content of the comment
418: *
419: * @exception NullPointerException if <code>comment</code>
420: * is <code>null</code>
421: */
422: public void writeComment(Object comment) {
423:
424: if (comment == null) {
425: throw new NullPointerException();
426: }
427: close();
428: buffer.append("<!-- "); //NOI18N
429: buffer.append(comment); // FIXME - filtering?
430: buffer.append(" -->"); //NOI18N
431:
432: }
433:
434: /**
435: * <p>Accumulate an object, after converting it to a String (if necessary)
436: * <strong>WITHOUT</strong> performing escaping appropriate for the
437: * markup language being rendered.</p>
438: * <p>If there is an open element that has been created by a call to
439: * <code>startElement()</code>, that element will be closed first.</p>
440: *
441: * @param raw Raw content to be written
442: * @param property Name of the component property or attribute (if any)
443: * of the <code>UIComponent</code> associated with the containing
444: * element, to which the generated content corresponds
445: *
446: * @exception NullPointerException if <code>text</code>
447: * is <code>null</code>
448: */
449: public void writeRaw(Object raw, String property) {
450:
451: if (raw == null) {
452: throw new NullPointerException();
453: }
454: close();
455: buffer.append(raw.toString());
456:
457: }
458:
459: /**
460: * <p>Accumulate an object, after converting it to a String (if necessary)
461: * and after performing any escaping appropriate for the markup
462: * language being rendered.</p>
463: *
464: * <p>If there is an open element that has been created by a call to
465: * <code>startElement()</code>, that element will be closed first.</p>
466: *
467: * @param text Text to be written
468: * @param property Name of the component property or attribute (if any)
469: * of the <code>UIComponent</code> associated with the containing
470: * element, to which the generated attribute corresponds
471: *
472: * @exception NullPointerException if <code>text</code>
473: * is <code>null</code>
474: */
475: public void writeText(Object text, String property) {
476:
477: if (text == null) {
478: throw new NullPointerException();
479: }
480: // Close any open element
481: close();
482:
483: // Render the filtered version of the specified text
484: String stext = text.toString();
485: int length = stext.length();
486:
487: for (int i = 0; i < length; i++) {
488: char ch = stext.charAt(i);
489:
490: // Tilde or less...
491: if (ch < 0xA0) {
492: // If "?" or over, no escaping is needed (this covers
493: // most of the Latin alphabet)
494: if (ch >= 0x3f) {
495: buffer.append(ch);
496: } else if (ch >= 0x27) { // If above "'"...
497: // If between "'" and ";", no escaping is needed
498: if (ch < 0x3c) {
499: buffer.append(ch);
500: } else if (ch == '<') {
501: buffer.append("<"); //NOI18N
502: } else if (ch == '>') {
503: buffer.append(">"); //NOI18N
504: } else {
505: buffer.append(ch);
506: }
507: } else {
508: if (ch == '&') {
509: buffer.append("&"); //NOI18N
510: } else {
511: buffer.append(ch);
512: }
513: }
514: } else if (ch <= 0xff) {
515: // ISO-8859-1 entities: encode as needed
516: buffer.append('&'); //NOI18N
517: buffer.append(ISO8859_1_Entities[ch - 0xA0]);
518: buffer.append(';'); //NOI18N
519: } else {
520: // Double-byte characters to encode.
521: // PENDING: when outputting to an encoding that
522: // supports double-byte characters (UTF-8, for example),
523: // we should not be encoding
524: numeric(ch);
525: }
526: }
527:
528: }
529:
530: // ------------------------------------------------------ Protected Methods
531:
532: /**
533: * <p>Close the currently open starting element, if any.</p>
534: */
535: protected void close() {
536:
537: if (open) {
538: buffer.append('>');
539: open = false;
540: }
541:
542: }
543:
544: /**
545: * <p>Append the hexadecimal equivalent of the specified
546: * numeric value.</p>
547: */
548: protected void hexadecimal(int i) {
549:
550: if (i < 10) {
551: buffer.append((char) ('0' + i));
552: } else {
553: buffer.append((char) ('A' + (i - 10)));
554: }
555:
556: }
557:
558: /**
559: * <p>Append the specified character as an escaped two-hex-digit value.</p>
560: *
561: * @param ch Character to be escaped
562: */
563: protected void hexadecimals(char ch) {
564:
565: buffer.append('%'); //NOI18N
566: hexadecimal((int) ((ch >> 4) % 0x10));
567: hexadecimal((int) (ch % 0x10));
568:
569: }
570:
571: /**
572: * <p>Append a numeric escape for the specified character.</p>
573: *
574: * @param ch Character to be escaped
575: */
576: protected void numeric(char ch) {
577:
578: if (ch == '\u20ac') { //NOI18N
579: buffer.append("€"); //NOI18N
580: return;
581: }
582:
583: // Formerly used String.valueOf(). This version tests out
584: // about 40% faster in a microbenchmark (and on systems where GC is
585: // going gonzo, it should be even better)
586: int i = (int) ch;
587: if (i > 10000) {
588: buffer.append('0' + (i / 10000));
589: i = i % 10000;
590: buffer.append('0' + (i / 1000));
591: i = i % 1000;
592: buffer.append('0' + (i / 100));
593: i = i % 100;
594: buffer.append('0' + (i / 10));
595: i = i % 10;
596: buffer.append('0' + i);
597: } else if (i > 1000) {
598: buffer.append('0' + (i / 1000));
599: i = i % 1000;
600: buffer.append('0' + (i / 100));
601: i = i % 100;
602: buffer.append('0' + (i / 10));
603: i = i % 10;
604: buffer.append('0' + i);
605: } else {
606: buffer.append('0' + (i / 100));
607: i = i % 100;
608: buffer.append('0' + (i / 10));
609: i = i % 10;
610: buffer.append('0' + i);
611: }
612: buffer.append(';'); //NOI18N
613:
614: }
615:
616: }
|