001: /*
002: * Copyright 2004-2006 The Apache Software Foundation.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.apache.myfaces.shared_impl.renderkit.html;
017:
018: import org.apache.commons.logging.Log;
019: import org.apache.commons.logging.LogFactory;
020: import org.apache.myfaces.shared_impl.renderkit.RendererUtils;
021: import org.apache.myfaces.shared_impl.renderkit.html.util.UnicodeEncoder;
022:
023: import javax.faces.FacesException;
024: import javax.faces.component.UIComponent;
025: import javax.faces.context.FacesContext;
026: import javax.faces.context.ResponseWriter;
027: import java.io.IOException;
028: import java.io.UnsupportedEncodingException;
029: import java.io.Writer;
030: import java.util.HashSet;
031: import java.util.Set;
032:
033: /**
034: * @author Manfred Geiler (latest modification by $Author: baranda $)
035: * @author Anton Koinov
036: * @version $Revision: 544646 $ $Date: 2007-06-05 23:51:27 +0200 (Di, 05 Jun 2007) $
037: */
038: public class HtmlResponseWriterImpl extends ResponseWriter {
039: private static final Log log = LogFactory
040: .getLog(HtmlResponseWriterImpl.class);
041:
042: private static final String DEFAULT_CONTENT_TYPE = "text/html";
043: private static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
044: private static final String UTF8 = "UTF-8";
045:
046: private boolean _writeDummyForm = false;
047: private Set _dummyFormParams = null;
048:
049: private Writer _writer;
050: private String _contentType;
051: private String _characterEncoding;
052: private String _startElementName;
053: private Boolean _isScript;
054: private Boolean _isStyle;
055: private Boolean _isTextArea;
056: private UIComponent _startElementUIComponent;
057: private boolean _startTagOpen;
058:
059: private static final Set s_emptyHtmlElements = new HashSet();
060:
061: private static final String CDATA_START = "<![CDATA[ \n";
062: private static final String COMMENT_START = "<!--\n";
063: private static final String CDATA_COMMENT_END = "\n//]]>";
064: private static final String CDATA_END = "\n]]>";
065: private static final String COMMENT_COMMENT_END = "\n//-->";
066: private static final String COMMENT_END = "\n-->";
067:
068: static {
069: s_emptyHtmlElements.add("area");
070: s_emptyHtmlElements.add("br");
071: s_emptyHtmlElements.add("base");
072: s_emptyHtmlElements.add("basefont");
073: s_emptyHtmlElements.add("col");
074: s_emptyHtmlElements.add("frame");
075: s_emptyHtmlElements.add("hr");
076: s_emptyHtmlElements.add("img");
077: s_emptyHtmlElements.add("input");
078: s_emptyHtmlElements.add("isindex");
079: s_emptyHtmlElements.add("link");
080: s_emptyHtmlElements.add("meta");
081: s_emptyHtmlElements.add("param");
082: }
083:
084: public HtmlResponseWriterImpl(Writer writer, String contentType,
085: String characterEncoding) throws FacesException {
086: _writer = writer;
087: _contentType = contentType;
088: if (_contentType == null) {
089: if (log.isDebugEnabled())
090: log
091: .debug("No content type given, using default content type "
092: + DEFAULT_CONTENT_TYPE);
093: _contentType = DEFAULT_CONTENT_TYPE;
094: }
095: if (characterEncoding == null) {
096: if (log.isDebugEnabled())
097: log
098: .debug("No character encoding given, using default character encoding "
099: + DEFAULT_CHARACTER_ENCODING);
100: _characterEncoding = DEFAULT_CHARACTER_ENCODING;
101: } else {
102: // validates the encoding, it will throw an UnsupportedEncodingException if the encoding is invalid
103: try {
104: new String("myfaces".getBytes(), characterEncoding);
105: } catch (UnsupportedEncodingException e) {
106: throw new IllegalArgumentException(
107: "Unsupported encoding: " + characterEncoding);
108: }
109:
110: // canonize to uppercase, that's the standard format
111: _characterEncoding = characterEncoding.toUpperCase();
112: }
113: }
114:
115: public static boolean supportsContentType(String contentType) {
116: String[] supportedContentTypes = HtmlRendererUtils
117: .getSupportedContentTypes();
118:
119: for (int i = 0; i < supportedContentTypes.length; i++) {
120: String supportedContentType = supportedContentTypes[i];
121:
122: if (supportedContentType.indexOf(contentType) != -1)
123: return true;
124: }
125: return false;
126: }
127:
128: public String getContentType() {
129: return _contentType;
130: }
131:
132: public String getCharacterEncoding() {
133: return _characterEncoding;
134: }
135:
136: public void flush() throws IOException {
137: // API doc says we should not flush the underlying writer
138: //_writer.flush();
139: // but rather clear any values buffered by this ResponseWriter:
140: closeStartTagIfNecessary();
141: }
142:
143: public void startDocument() {
144: // do nothing
145: }
146:
147: public void endDocument() throws IOException {
148: _writer.flush();
149: }
150:
151: public void startElement(String name, UIComponent uiComponent)
152: throws IOException {
153: if (name == null) {
154: throw new NullPointerException(
155: "elementName name must not be null");
156: }
157:
158: closeStartTagIfNecessary();
159: _writer.write('<');
160: _writer.write(name);
161:
162: resetStartedElement();
163:
164: _startElementName = name;
165: _startElementUIComponent = uiComponent;
166: _startTagOpen = true;
167: }
168:
169: private void closeStartTagIfNecessary() throws IOException {
170: if (_startTagOpen) {
171: if (s_emptyHtmlElements.contains(_startElementName
172: .toLowerCase())) {
173: _writer.write(" />");
174: // make null, this will cause NullPointer in some invalid element nestings
175: // (better than doing nothing)
176: resetStartedElement();
177: } else {
178: _writer.write('>');
179:
180: if (isScriptOrStyle()) {
181: if (HtmlRendererUtils
182: .isXHTMLContentType(_contentType)) {
183: if (HtmlRendererUtils
184: .isAllowedCdataSection(FacesContext
185: .getCurrentInstance())) {
186: _writer.write(CDATA_START);
187: }
188: } else {
189: _writer.write(COMMENT_START);
190: }
191: }
192: }
193: _startTagOpen = false;
194: }
195: }
196:
197: private void resetStartedElement() {
198: _startElementName = null;
199: _startElementUIComponent = null;
200: _isScript = null;
201: _isStyle = null;
202: _isTextArea = null;
203: }
204:
205: public void endElement(String name) throws IOException {
206: if (name == null) {
207: throw new NullPointerException(
208: "elementName name must not be null");
209: }
210:
211: if (log.isWarnEnabled()) {
212: if (_startElementName != null
213: && !name.equals(_startElementName)) {
214: if (log.isWarnEnabled())
215: log
216: .warn("HTML nesting warning on closing "
217: + name
218: + ": element "
219: + _startElementName
220: + (_startElementUIComponent == null ? ""
221: : (" rendered by component : " + RendererUtils
222: .getPathToComponent(_startElementUIComponent)))
223: + " not explicitly closed");
224: }
225: }
226:
227: if (_startTagOpen) {
228:
229: // we will get here only if no text or attribute was written after the start element was opened
230: // now we close out the started tag - if it is an empty tag, this is then fully closed
231: closeStartTagIfNecessary();
232:
233: //tag was no empty tag - it has no accompanying end tag now.
234: if (_startElementName != null) {
235: //write closing tag
236: writeEndTag(name);
237: }
238: } else {
239: if (s_emptyHtmlElements.contains(name.toLowerCase())) {
240: /*
241: Should this be here? It warns even when you have an x:htmlTag value="br", it should just close.
242:
243: if (log.isWarnEnabled())
244: log.warn("HTML nesting warning on closing " + name + ": This element must not contain nested elements or text in HTML");
245: */
246: } else {
247: writeEndTag(name);
248: }
249: }
250:
251: resetStartedElement();
252: }
253:
254: private void writeEndTag(String name) throws IOException {
255: if (isScriptOrStyle()) {
256: if (HtmlRendererUtils.isXHTMLContentType(_contentType)) {
257: if (HtmlRendererUtils
258: .isAllowedCdataSection(FacesContext
259: .getCurrentInstance())) {
260: if (isScript())
261: _writer.write(CDATA_COMMENT_END);
262: else
263: _writer.write(CDATA_END);
264: }
265: } else {
266: if (isScript())
267: _writer.write(COMMENT_COMMENT_END);
268: else
269: _writer.write(COMMENT_END);
270: }
271: }
272:
273: _writer.write("</");
274: _writer.write(name);
275: _writer.write('>');
276: }
277:
278: public void writeAttribute(String name, Object value,
279: String componentPropertyName) throws IOException {
280: if (name == null) {
281: throw new NullPointerException(
282: "attributeName name must not be null");
283: }
284: if (!_startTagOpen) {
285: throw new IllegalStateException(
286: "Must be called before the start element is closed (attribute '"
287: + name + "')");
288: }
289:
290: if (value instanceof Boolean) {
291: if (((Boolean) value).booleanValue()) {
292: // name as value for XHTML compatibility
293: _writer.write(' ');
294: _writer.write(name);
295: _writer.write("=\"");
296: _writer.write(name);
297: _writer.write('"');
298: }
299: } else {
300: String strValue = (value == null) ? "" : value.toString();
301: _writer.write(' ');
302: _writer.write(name);
303: _writer.write("=\"");
304: _writer
305: .write(org.apache.myfaces.shared_impl.renderkit.html.util.HTMLEncoder
306: .encode(strValue, false, false, !UTF8
307: .equals(_characterEncoding)));
308: _writer.write('"');
309: }
310: }
311:
312: public void writeURIAttribute(String name, Object value,
313: String componentPropertyName) throws IOException {
314: if (name == null) {
315: throw new NullPointerException(
316: "attributeName name must not be null");
317: }
318: if (!_startTagOpen) {
319: throw new IllegalStateException(
320: "Must be called before the start element is closed (attribute '"
321: + name + "')");
322: }
323:
324: String strValue = value.toString();
325: _writer.write(' ');
326: _writer.write(name);
327: _writer.write("=\"");
328: if (strValue.toLowerCase().startsWith("javascript:")) {
329: _writer
330: .write(org.apache.myfaces.shared_impl.renderkit.html.util.HTMLEncoder
331: .encode(strValue, false, false, !UTF8
332: .equals(_characterEncoding)));
333: } else {
334: /*
335: Todo: what is this section about? still needed?
336: client side state saving is now done via javascript...
337:
338: if (_startElementName.equalsIgnoreCase(HTML.ANCHOR_ELEM) && //Also support image and button urls?
339: name.equalsIgnoreCase(HTML.HREF_ATTR) &&
340: !strValue.startsWith("#"))
341: {
342: FacesContext facesContext = FacesContext.getCurrentInstance();
343: if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext))
344: {
345: // saving state in url depends on the work together
346: // of 3 (theoretically) pluggable components:
347: // ViewHandler, ResponseWriter and ViewTag
348: // We should try to make this HtmlResponseWriterImpl able
349: // to handle this alone!
350: if (strValue.indexOf('?') < 0)
351: {
352: strValue = strValue + '?' + JspViewHandlerImpl.URL_STATE_MARKER;
353: }
354: else
355: {
356: strValue = strValue + '&' + JspViewHandlerImpl.URL_STATE_MARKER;
357: }
358: }
359: }
360: */
361: _writer.write(strValue);
362: }
363: _writer.write('"');
364: }
365:
366: public void writeComment(Object value) throws IOException {
367: if (value == null) {
368: throw new NullPointerException(
369: "comment name must not be null");
370: }
371:
372: closeStartTagIfNecessary();
373: _writer.write("<!--");
374: _writer.write(value.toString()); //TODO: Escaping: must not have "-->" inside!
375: _writer.write("-->");
376: }
377:
378: public void writeText(Object value, String componentPropertyName)
379: throws IOException {
380: if (value == null) {
381: throw new NullPointerException("Text must not be null.");
382: }
383:
384: closeStartTagIfNecessary();
385:
386: String strValue = value.toString();
387:
388: if (isScriptOrStyle()) {
389: // Don't bother encoding anything if chosen character encoding is UTF-8
390: if (UTF8.equals(_characterEncoding))
391: _writer.write(strValue);
392: else
393: _writer.write(UnicodeEncoder.encode(strValue));
394: } else {
395: _writer
396: .write(org.apache.myfaces.shared_impl.renderkit.html.util.HTMLEncoder
397: .encode(strValue, false, false, !UTF8
398: .equals(_characterEncoding)));
399: }
400: }
401:
402: public void writeText(char cbuf[], int off, int len)
403: throws IOException {
404: if (cbuf == null) {
405: throw new NullPointerException("cbuf name must not be null");
406: }
407: if (cbuf.length < off + len) {
408: throw new IndexOutOfBoundsException((off + len) + " > "
409: + cbuf.length);
410: }
411:
412: closeStartTagIfNecessary();
413:
414: if (isScriptOrStyle()) {
415: String strValue = new String(cbuf, off, len);
416: // Don't bother encoding anything if chosen character encoding is UTF-8
417: if (UTF8.equals(_characterEncoding))
418: _writer.write(strValue);
419: else
420: _writer.write(UnicodeEncoder.encode(strValue));
421: } else if (isTextarea()) {
422: // For textareas we must *not* map successive spaces to   or Newlines to <br/>
423: // TODO: Make HTMLEncoder support char arrays directly
424: String strValue = new String(cbuf, off, len);
425: _writer
426: .write(org.apache.myfaces.shared_impl.renderkit.html.util.HTMLEncoder
427: .encode(strValue, false, false, !UTF8
428: .equals(_characterEncoding)));
429: } else {
430: // We map successive spaces to and Newlines to <br/>
431: // TODO: Make HTMLEncoder support char arrays directly
432: String strValue = new String(cbuf, off, len);
433: _writer
434: .write(org.apache.myfaces.shared_impl.renderkit.html.util.HTMLEncoder
435: .encode(strValue, true, true, !UTF8
436: .equals(_characterEncoding)));
437: }
438: }
439:
440: private boolean isScriptOrStyle() {
441: initializeStartedTagInfo();
442:
443: return (_isStyle != null && _isStyle.booleanValue())
444: || (_isScript != null && _isScript.booleanValue());
445: }
446:
447: private boolean isScript() {
448: initializeStartedTagInfo();
449:
450: return (_isScript != null && _isScript.booleanValue());
451: }
452:
453: private boolean isTextarea() {
454: initializeStartedTagInfo();
455:
456: return _isTextArea != null && _isTextArea.booleanValue();
457: }
458:
459: private void initializeStartedTagInfo() {
460: if (_startElementName != null) {
461: if (_isScript == null) {
462: if (_startElementName
463: .equalsIgnoreCase(HTML.SCRIPT_ELEM)) {
464: _isScript = Boolean.TRUE;
465: _isStyle = Boolean.FALSE;
466: _isTextArea = Boolean.FALSE;
467: } else {
468: _isScript = Boolean.FALSE;
469: }
470: }
471: if (_isStyle == null) {
472: if (_startElementName
473: .equalsIgnoreCase(org.apache.myfaces.shared_impl.renderkit.html.HTML.STYLE_ELEM)) {
474: _isStyle = Boolean.TRUE;
475: _isTextArea = Boolean.FALSE;
476: } else {
477: _isStyle = Boolean.FALSE;
478: }
479: }
480:
481: if (_isTextArea == null) {
482: if (_startElementName
483: .equalsIgnoreCase(HTML.TEXTAREA_ELEM)) {
484: _isTextArea = Boolean.TRUE;
485: } else {
486: _isTextArea = Boolean.FALSE;
487: }
488: }
489: }
490: }
491:
492: public ResponseWriter cloneWithWriter(Writer writer) {
493: HtmlResponseWriterImpl newWriter = new HtmlResponseWriterImpl(
494: writer, getContentType(), getCharacterEncoding());
495: newWriter._writeDummyForm = _writeDummyForm;
496: newWriter._dummyFormParams = _dummyFormParams;
497: return newWriter;
498: }
499:
500: // Writer methods
501:
502: public void close() throws IOException {
503: closeStartTagIfNecessary();
504: _writer.close();
505: }
506:
507: public void write(char cbuf[], int off, int len) throws IOException {
508: closeStartTagIfNecessary();
509: String strValue = new String(cbuf, off, len);
510: // Don't bother encoding anything if chosen character encoding is UTF-8
511: if (UTF8.equals(_characterEncoding))
512: _writer.write(strValue);
513: else
514: _writer.write(UnicodeEncoder.encode(strValue));
515: }
516:
517: public void write(int c) throws IOException {
518: closeStartTagIfNecessary();
519: _writer.write(c);
520: }
521:
522: public void write(char cbuf[]) throws IOException {
523: closeStartTagIfNecessary();
524: String strValue = new String(cbuf);
525: // Don't bother encoding anything if chosen character encoding is UTF-8
526: if (UTF8.equals(_characterEncoding))
527: _writer.write(strValue);
528: else
529: _writer.write(UnicodeEncoder.encode(strValue));
530: }
531:
532: public void write(String str) throws IOException {
533: closeStartTagIfNecessary();
534: // empty string commonly used to force the start tag to be closed.
535: // in such case, do not call down the writer chain
536: if (str.length() > 0) {
537: // Don't bother encoding anything if chosen character encoding is UTF-8
538: if (UTF8.equals(_characterEncoding))
539: _writer.write(str);
540: else
541: _writer.write(UnicodeEncoder.encode(str));
542: }
543: }
544:
545: public void write(String str, int off, int len) throws IOException {
546: closeStartTagIfNecessary();
547: String strValue = str.substring(off, off + len);
548: // Don't bother encoding anything if chosen character encoding is UTF-8
549: if (UTF8.equals(_characterEncoding))
550: _writer.write(strValue);
551: else
552: _writer.write(UnicodeEncoder.encode(strValue));
553: }
554:
555: /**
556: * This method ignores the <code>UIComponent</code> provided and simply calls
557: * <code>writeText(Object,String)</code>
558: * @since 1.2
559: */
560: public void writeText(Object object, UIComponent component,
561: String string) throws IOException {
562: writeText(object, string);
563: }
564: }
|