View Javadoc

1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    * 
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   */
19  package org.apache.myfaces.shared.renderkit.html;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.myfaces.shared.renderkit.RendererUtils;
24  import org.apache.myfaces.shared.renderkit.html.util.UnicodeEncoder;
25  
26  import javax.faces.FacesException;
27  import javax.faces.component.UIComponent;
28  import javax.faces.context.FacesContext;
29  import javax.faces.context.ResponseWriter;
30  import java.io.IOException;
31  import java.io.UnsupportedEncodingException;
32  import java.io.Writer;
33  import java.util.HashSet;
34  import java.util.Set;
35  
36  /***
37   * @author Manfred Geiler (latest modification by $Author: lu4242 $)
38   * @author Anton Koinov
39   * @version $Revision: 779412 $ $Date: 2009-05-27 21:36:25 -0500 (Wed, 27 May 2009) $
40   */
41  public class HtmlResponseWriterImpl
42          extends ResponseWriter
43  {
44      private static final Log log = LogFactory.getLog(HtmlResponseWriterImpl.class);
45  
46      private static final String DEFAULT_CONTENT_TYPE = "text/html";
47      private static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
48      private static final String UTF8 = "UTF-8";
49  
50      private boolean _writeDummyForm = false;
51      private Set _dummyFormParams = null;
52  
53      private Writer _writer;
54      private String _contentType;
55      private String _characterEncoding;
56      private String _startElementName;
57      private Boolean _isScript;
58      private Boolean _isStyle;
59      private Boolean _isTextArea;
60      private UIComponent _startElementUIComponent;
61      private boolean _startTagOpen;
62  
63      private static final Set s_emptyHtmlElements = new HashSet();
64  
65      private static final String CDATA_START = "<![CDATA[ \n";
66      private static final String COMMENT_START = "<!--\n";
67      private static final String CDATA_COMMENT_END = "\n//]]>";
68      private static final String CDATA_END = "\n]]>";
69      private static final String COMMENT_COMMENT_END = "\n//-->";
70      private static final String COMMENT_END = "\n-->";
71  
72      static
73      {
74          s_emptyHtmlElements.add("area");
75          s_emptyHtmlElements.add("br");
76          s_emptyHtmlElements.add("base");
77          s_emptyHtmlElements.add("basefont");
78          s_emptyHtmlElements.add("col");
79          s_emptyHtmlElements.add("frame");
80          s_emptyHtmlElements.add("hr");
81          s_emptyHtmlElements.add("img");
82          s_emptyHtmlElements.add("input");
83          s_emptyHtmlElements.add("isindex");
84          s_emptyHtmlElements.add("link");
85          s_emptyHtmlElements.add("meta");
86          s_emptyHtmlElements.add("param");
87      }
88  
89      public HtmlResponseWriterImpl(Writer writer, String contentType, String characterEncoding)
90      throws FacesException
91      {
92          _writer = writer;
93          _contentType = contentType;
94          if (_contentType == null)
95          {
96              if (log.isDebugEnabled()) log.debug("No content type given, using default content type " + DEFAULT_CONTENT_TYPE);
97              _contentType = DEFAULT_CONTENT_TYPE;
98          }
99          if (characterEncoding == null)
100         {
101             if (log.isDebugEnabled()) log.debug("No character encoding given, using default character encoding " + DEFAULT_CHARACTER_ENCODING);
102             _characterEncoding = DEFAULT_CHARACTER_ENCODING;
103         }
104         else
105         {
106             // validates the encoding, it will throw an UnsupportedEncodingException if the encoding is invalid
107             try
108             {
109                 new String("myfaces".getBytes(), characterEncoding);
110             }
111             catch (UnsupportedEncodingException e)
112             {
113                 throw new IllegalArgumentException("Unsupported encoding: "+characterEncoding);
114             }
115             
116             // canonize to uppercase, that's the standard format
117             _characterEncoding = characterEncoding.toUpperCase();
118         }
119     }
120 
121     public static boolean supportsContentType(String contentType)
122     {
123         String[] supportedContentTypes = HtmlRendererUtils.getSupportedContentTypes();
124 
125         for (int i = 0; i < supportedContentTypes.length; i++)
126         {
127             String supportedContentType = supportedContentTypes[i];
128 
129             if(supportedContentType.indexOf(contentType)!=-1)
130                 return true;
131         }
132         return false;
133     }
134 
135     public String getContentType()
136     {
137         return _contentType;
138     }
139 
140     public String getCharacterEncoding()
141     {
142         return _characterEncoding;
143     }
144 
145     public void flush() throws IOException
146     {
147         // API doc says we should not flush the underlying writer
148         //_writer.flush();
149         // but rather clear any values buffered by this ResponseWriter:
150         closeStartTagIfNecessary();
151     }
152 
153     public void startDocument()
154     {
155         // do nothing
156     }
157 
158     public void endDocument() throws IOException
159     {
160         _writer.flush();
161     }
162 
163     public void startElement(String name, UIComponent uiComponent) throws IOException
164     {
165         if (name == null)
166         {
167             throw new NullPointerException("elementName name must not be null");
168         }
169 
170         closeStartTagIfNecessary();
171         _writer.write('<');
172         _writer.write(name);
173 
174         resetStartedElement();
175 
176         _startElementName = name;
177         _startElementUIComponent = uiComponent;
178         _startTagOpen = true;
179     }
180 
181     private void closeStartTagIfNecessary() throws IOException
182     {
183         if (_startTagOpen)
184         {
185             if (s_emptyHtmlElements.contains(_startElementName.toLowerCase()))
186             {
187                 _writer.write(" />");
188                 // make null, this will cause NullPointer in some invalid element nestings
189                 // (better than doing nothing)
190                 resetStartedElement();
191             }
192             else
193             {
194                 _writer.write('>');
195 
196                 if(isScriptOrStyle())
197                 {
198                     if(HtmlRendererUtils.isXHTMLContentType(_contentType))
199                     {
200                         if(HtmlRendererUtils.isAllowedCdataSection(FacesContext.getCurrentInstance()))
201                         {
202                             _writer.write(CDATA_START);
203                         }
204                     }
205                     else
206                     {
207                         _writer.write(COMMENT_START);
208                     }
209                 }
210             }
211             _startTagOpen = false;
212         }
213     }
214 
215     private void resetStartedElement()
216     {
217         _startElementName = null;
218         _startElementUIComponent = null;
219         _isScript = null;
220         _isStyle = null;
221         _isTextArea = null;
222     }
223 
224     public void endElement(String name) throws IOException
225     {
226         if (name == null)
227         {
228             throw new NullPointerException("elementName name must not be null");
229         }
230 
231         if (log.isWarnEnabled())
232         {
233             if (_startElementName != null &&
234                 !name.equals(_startElementName))
235             {
236                 if (log.isWarnEnabled())
237                     log.warn("HTML nesting warning on closing " + name + ": element " + _startElementName +
238                             (_startElementUIComponent==null?"":(" rendered by component : "+
239                             RendererUtils.getPathToComponent(_startElementUIComponent)))+" not explicitly closed");
240             }
241         }
242 
243         if(_startTagOpen)
244         {
245 
246             // we will get here only if no text or attribute was written after the start element was opened
247             // now we close out the started tag - if it is an empty tag, this is then fully closed
248             closeStartTagIfNecessary();
249 
250             //tag was no empty tag - it has no accompanying end tag now.
251             if(_startElementName!=null)
252             {
253                 //write closing tag
254                 writeEndTag(name);
255             }
256         }
257         else
258         {
259             if (s_emptyHtmlElements.contains(name.toLowerCase()))
260             {
261            /*
262            Should this be here?  It warns even when you have an x:htmlTag value="br", it should just close.
263 
264                 if (log.isWarnEnabled())
265                     log.warn("HTML nesting warning on closing " + name + ": This element must not contain nested elements or text in HTML");
266                     */
267             }
268             else
269             {
270                 writeEndTag(name);
271             }
272         }
273 
274         resetStartedElement();
275     }
276 
277     private void writeEndTag(String name)
278         throws IOException
279     {
280         if(isScriptOrStyle())
281         {
282             if(HtmlRendererUtils.isXHTMLContentType(_contentType))
283             {
284                 if(HtmlRendererUtils.isAllowedCdataSection(FacesContext.getCurrentInstance()))
285                 {
286                     if(isScript())
287                         _writer.write(CDATA_COMMENT_END);
288                     else
289                         _writer.write(CDATA_END);
290                 }
291             }
292             else
293             {
294                 if(isScript())
295                     _writer.write(COMMENT_COMMENT_END);
296                 else
297                     _writer.write(COMMENT_END);
298             }
299         }
300 
301         _writer.write("</");
302         _writer.write(name);
303         _writer.write('>');
304     }
305 
306     public void writeAttribute(String name, Object value, String componentPropertyName) throws IOException
307     {
308         if (name == null)
309         {
310             throw new NullPointerException("attributeName name must not be null");
311         }
312         if (!_startTagOpen)
313         {
314             throw new IllegalStateException("Must be called before the start element is closed (attribute '" + name + "')");
315         }
316 
317         if (value instanceof Boolean)
318         {
319             if (((Boolean)value).booleanValue())
320             {
321                 // name as value for XHTML compatibility
322                 _writer.write(' ');
323                 _writer.write(name);
324                 _writer.write("=\"");
325                 _writer.write(name);
326                 _writer.write('"');
327             }
328         }
329         else
330         {
331             String strValue = (value==null)?"":value.toString();
332             _writer.write(' ');
333             _writer.write(name);
334             _writer.write("=\"");
335             _writer.write(org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
336             _writer.write('"');
337         }
338     }
339 
340     public void writeURIAttribute(String name, Object value, String componentPropertyName) throws IOException
341     {
342         if (name == null)
343         {
344             throw new NullPointerException("attributeName name must not be null");
345         }
346         if (!_startTagOpen)
347         {
348             throw new IllegalStateException("Must be called before the start element is closed (attribute '" + name + "')");
349         }
350 
351         String strValue = value.toString();
352         _writer.write(' ');
353         _writer.write(name);
354         _writer.write("=\"");
355         if (strValue.toLowerCase().startsWith("javascript:"))
356         {
357             _writer.write(org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
358         }
359         else
360         {
361             /*
362             Todo: what is this section about? still needed?
363             client side state saving is now done via javascript...
364 
365             if (_startElementName.equalsIgnoreCase(HTML.ANCHOR_ELEM) && //Also support image and button urls?
366                 name.equalsIgnoreCase(HTML.HREF_ATTR) &&
367                 !strValue.startsWith("#"))
368             {
369                 FacesContext facesContext = FacesContext.getCurrentInstance();
370                 if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext))
371                 {
372                     // saving state in url depends on the work together
373                     // of 3 (theoretically) pluggable components:
374                     // ViewHandler, ResponseWriter and ViewTag
375                     // We should try to make this HtmlResponseWriterImpl able
376                     // to handle this alone!
377                     if (strValue.indexOf('?') < 0)
378                     {
379                         strValue = strValue + '?' + JspViewHandlerImpl.URL_STATE_MARKER;
380                     }
381                     else
382                     {
383                         strValue = strValue + '&' + JspViewHandlerImpl.URL_STATE_MARKER;
384                     }
385                 }
386             }
387             */
388             //_writer.write(strValue);
389             _writer.write(org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encodeURIAtributte(strValue, _characterEncoding));
390         }
391         _writer.write('"');
392     }
393 
394     public void writeComment(Object value) throws IOException
395     {
396         if (value == null)
397         {
398             throw new NullPointerException("comment name must not be null");
399         }
400 
401         closeStartTagIfNecessary();
402         _writer.write("<!--");
403         _writer.write(value.toString());    //TODO: Escaping: must not have "-->" inside!
404         _writer.write("-->");
405     }
406 
407     public void writeText(Object value, String componentPropertyName) throws IOException
408     {
409         if (value == null)
410         {
411             throw new NullPointerException("Text must not be null.");
412         }
413 
414         closeStartTagIfNecessary();
415 
416         String strValue = value.toString();
417 
418         if (isScriptOrStyle())
419         {
420             // Don't bother encoding anything if chosen character encoding is UTF-8
421             if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
422             else _writer.write(UnicodeEncoder.encode(strValue) );
423         }
424         else
425         {
426             _writer.write(org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
427         }
428     }
429 
430     public void writeText(char cbuf[], int off, int len) throws IOException
431     {
432         if (cbuf == null)
433         {
434             throw new NullPointerException("cbuf name must not be null");
435         }
436         if (cbuf.length < off + len)
437         {
438             throw new IndexOutOfBoundsException((off + len) + " > " + cbuf.length);
439         }
440 
441         closeStartTagIfNecessary();
442 
443         if (isScriptOrStyle())
444         {
445             String strValue = new String(cbuf, off, len);
446             // Don't bother encoding anything if chosen character encoding is UTF-8
447             if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
448             else _writer.write(UnicodeEncoder.encode(strValue) );
449         }
450         else if (isTextarea())
451         {
452             // For textareas we must *not* map successive spaces to &nbsp or Newlines to <br/>
453             org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(cbuf, off, len, false, false, !UTF8.equals(_characterEncoding), _writer);
454         }
455         else
456         {
457             // We map successive spaces to &nbsp; and Newlines to <br/>
458             org.apache.myfaces.shared.renderkit.html.util.HTMLEncoder.encode(cbuf, off, len, true, true, !UTF8.equals(_characterEncoding), _writer);
459         }
460     }
461 
462     private boolean isScriptOrStyle()
463     {
464         initializeStartedTagInfo();
465 
466         return (_isStyle != null && _isStyle.booleanValue()) ||
467                 (_isScript != null && _isScript.booleanValue());
468     }
469 
470     private boolean isScript()
471     {
472         initializeStartedTagInfo();
473 
474         return (_isScript != null && _isScript.booleanValue());
475     }
476 
477     private boolean isTextarea()
478     {
479         initializeStartedTagInfo();
480 
481         return _isTextArea != null && _isTextArea.booleanValue();
482     }
483 
484     private void initializeStartedTagInfo()
485     {
486         if(_startElementName != null)
487         {
488             if(_isScript==null)
489             {
490                 if(_startElementName.equalsIgnoreCase(HTML.SCRIPT_ELEM))
491                 {
492                     _isScript = Boolean.TRUE;
493                     _isStyle = Boolean.FALSE;
494                     _isTextArea = Boolean.FALSE;
495                 }
496                 else
497                 {
498                     _isScript = Boolean.FALSE;
499                 }
500             }
501             if(_isStyle == null)
502             {
503                 if(_startElementName.equalsIgnoreCase(org.apache.myfaces.shared.renderkit.html.HTML.STYLE_ELEM))
504                 {
505                     _isStyle = Boolean.TRUE;
506                     _isTextArea = Boolean.FALSE;
507                 }
508                 else
509                 {
510                     _isStyle = Boolean.FALSE;
511                 }
512             }
513 
514             if(_isTextArea == null)
515             {
516                 if(_startElementName.equalsIgnoreCase(HTML.TEXTAREA_ELEM))
517                 {
518                     _isTextArea = Boolean.TRUE;
519                 }
520                 else
521                 {
522                     _isTextArea = Boolean.FALSE;
523                 }
524             }
525         }
526     }
527 
528     public ResponseWriter cloneWithWriter(Writer writer)
529     {
530         HtmlResponseWriterImpl newWriter
531                 = new HtmlResponseWriterImpl(writer, getContentType(), getCharacterEncoding());
532         newWriter._writeDummyForm = _writeDummyForm;
533         newWriter._dummyFormParams = _dummyFormParams;
534         return newWriter;
535     }
536 
537 
538     // Writer methods
539 
540     public void close() throws IOException
541     {
542         closeStartTagIfNecessary();
543         _writer.close();
544     }
545 
546     public void write(char cbuf[], int off, int len) throws IOException
547     {
548         closeStartTagIfNecessary();
549         String strValue = new String(cbuf, off, len);
550         // Don't bother encoding anything if chosen character encoding is UTF-8
551         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
552         else _writer.write(UnicodeEncoder.encode(strValue) );
553     }
554 
555     public void write(int c) throws IOException
556     {
557         closeStartTagIfNecessary();
558         _writer.write(c);
559     }
560 
561     public void write(char cbuf[]) throws IOException
562     {
563         closeStartTagIfNecessary();
564         String strValue = new String(cbuf);
565         // Don't bother encoding anything if chosen character encoding is UTF-8
566         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
567         else _writer.write(UnicodeEncoder.encode(strValue) );
568     }
569 
570     public void write(String str) throws IOException
571     {
572         closeStartTagIfNecessary();
573         // empty string commonly used to force the start tag to be closed.
574         // in such case, do not call down the writer chain
575         if (str.length() > 0)
576         {
577             // Don't bother encoding anything if chosen character encoding is UTF-8
578             if (UTF8.equals(_characterEncoding)) _writer.write(str);
579             else _writer.write(UnicodeEncoder.encode(str) );
580         }
581     }
582 
583     public void write(String str, int off, int len) throws IOException
584     {
585         closeStartTagIfNecessary();
586         String strValue = str.substring(off, off+len);
587         // Don't bother encoding anything if chosen character encoding is UTF-8
588         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
589         else _writer.write(UnicodeEncoder.encode(strValue) );
590     }
591     
592     /***
593      * This method ignores the <code>UIComponent</code> provided and simply calls
594      * <code>writeText(Object,String)</code>
595      * @since 1.2
596      */
597     public void writeText(Object object, UIComponent component, String string) throws IOException
598     {
599         writeText(object,string);
600     }
601 }