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_orchestra.renderkit.html;
20  
21  import org.apache.commons.logging.Log;
22  import org.apache.commons.logging.LogFactory;
23  import org.apache.myfaces.shared_orchestra.renderkit.RendererUtils;
24  import org.apache.myfaces.shared_orchestra.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: 779411 $ $Date: 2009-05-27 21:36:00 -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 void setContentType(java.lang.String contentType) {
141         _contentType = contentType;
142     }
143 
144     public String getCharacterEncoding()
145     {
146         return _characterEncoding;
147     }
148 
149     public void flush() throws IOException
150     {
151         // API doc says we should not flush the underlying writer
152         //_writer.flush();
153         // but rather clear any values buffered by this ResponseWriter:
154         closeStartTagIfNecessary();
155     }
156 
157     public void startDocument()
158     {
159         // do nothing
160     }
161 
162     public void endDocument() throws IOException
163     {
164         _writer.flush();
165     }
166 
167     public void startElement(String name, UIComponent uiComponent) throws IOException
168     {
169         if (name == null)
170         {
171             throw new NullPointerException("elementName name must not be null");
172         }
173 
174         closeStartTagIfNecessary();
175         _writer.write('<');
176         _writer.write(name);
177 
178         resetStartedElement();
179 
180         _startElementName = name;
181         _startElementUIComponent = uiComponent;
182         _startTagOpen = true;
183     }
184 
185     private void closeStartTagIfNecessary() throws IOException
186     {
187         if (_startTagOpen)
188         {
189             if (s_emptyHtmlElements.contains(_startElementName.toLowerCase()))
190             {
191                 _writer.write(" />");
192                 // make null, this will cause NullPointer in some invalid element nestings
193                 // (better than doing nothing)
194                 resetStartedElement();
195             }
196             else
197             {
198                 _writer.write('>');
199 
200                 if(isScriptOrStyle())
201                 {
202                     if(HtmlRendererUtils.isXHTMLContentType(_contentType))
203                     {
204                         if(HtmlRendererUtils.isAllowedCdataSection(FacesContext.getCurrentInstance()))
205                         {
206                             _writer.write(CDATA_START);
207                         }
208                     }
209                     else
210                     {
211                         _writer.write(COMMENT_START);
212                     }
213                 }
214             }
215             _startTagOpen = false;
216         }
217     }
218 
219     private void resetStartedElement()
220     {
221         _startElementName = null;
222         _startElementUIComponent = null;
223         _isScript = null;
224         _isStyle = null;
225         _isTextArea = null;
226     }
227 
228     public void endElement(String name) throws IOException
229     {
230         if (name == null)
231         {
232             throw new NullPointerException("elementName name must not be null");
233         }
234 
235         if (log.isWarnEnabled())
236         {
237             if (_startElementName != null &&
238                 !name.equals(_startElementName))
239             {
240                 if (log.isWarnEnabled())
241                     log.warn("HTML nesting warning on closing " + name + ": element " + _startElementName +
242                             (_startElementUIComponent==null?"":(" rendered by component : "+
243                             RendererUtils.getPathToComponent(_startElementUIComponent)))+" not explicitly closed");
244             }
245         }
246 
247         if(_startTagOpen)
248         {
249 
250             // we will get here only if no text or attribute was written after the start element was opened
251             // now we close out the started tag - if it is an empty tag, this is then fully closed
252             closeStartTagIfNecessary();
253 
254             //tag was no empty tag - it has no accompanying end tag now.
255             if(_startElementName!=null)
256             {
257                 //write closing tag
258                 writeEndTag(name);
259             }
260         }
261         else
262         {
263             if (s_emptyHtmlElements.contains(name.toLowerCase()))
264             {
265            /*
266            Should this be here?  It warns even when you have an x:htmlTag value="br", it should just close.
267 
268                 if (log.isWarnEnabled())
269                     log.warn("HTML nesting warning on closing " + name + ": This element must not contain nested elements or text in HTML");
270                     */
271             }
272             else
273             {
274                 writeEndTag(name);
275             }
276         }
277 
278         resetStartedElement();
279     }
280 
281     private void writeEndTag(String name)
282         throws IOException
283     {
284         if(isScriptOrStyle())
285         {
286             if(HtmlRendererUtils.isXHTMLContentType(_contentType))
287             {
288                 if(HtmlRendererUtils.isAllowedCdataSection(FacesContext.getCurrentInstance()))
289                 {
290                     if(isScript())
291                         _writer.write(CDATA_COMMENT_END);
292                     else
293                         _writer.write(CDATA_END);
294                 }
295             }
296             else
297             {
298                 if(isScript())
299                     _writer.write(COMMENT_COMMENT_END);
300                 else
301                     _writer.write(COMMENT_END);
302             }
303         }
304 
305         _writer.write("</");
306         _writer.write(name);
307         _writer.write('>');
308     }
309 
310     public void writeAttribute(String name, Object value, String componentPropertyName) throws IOException
311     {
312         if (name == null)
313         {
314             throw new NullPointerException("attributeName name must not be null");
315         }
316         if (!_startTagOpen)
317         {
318             throw new IllegalStateException("Must be called before the start element is closed (attribute '" + name + "')");
319         }
320 
321         if (value instanceof Boolean)
322         {
323             if (((Boolean)value).booleanValue())
324             {
325                 // name as value for XHTML compatibility
326                 _writer.write(' ');
327                 _writer.write(name);
328                 _writer.write("=\"");
329                 _writer.write(name);
330                 _writer.write('"');
331             }
332         }
333         else
334         {
335             String strValue = (value==null)?"":value.toString();
336             _writer.write(' ');
337             _writer.write(name);
338             _writer.write("=\"");
339             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
340             _writer.write('"');
341         }
342     }
343 
344     public void writeURIAttribute(String name, Object value, String componentPropertyName) throws IOException
345     {
346         if (name == null)
347         {
348             throw new NullPointerException("attributeName name must not be null");
349         }
350         if (!_startTagOpen)
351         {
352             throw new IllegalStateException("Must be called before the start element is closed (attribute '" + name + "')");
353         }
354 
355         String strValue = value.toString();
356         _writer.write(' ');
357         _writer.write(name);
358         _writer.write("=\"");
359         if (strValue.toLowerCase().startsWith("javascript:"))
360         {
361             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
362         }
363         else
364         {
365             /*
366             Todo: what is this section about? still needed?
367             client side state saving is now done via javascript...
368 
369             if (_startElementName.equalsIgnoreCase(HTML.ANCHOR_ELEM) && //Also support image and button urls?
370                 name.equalsIgnoreCase(HTML.HREF_ATTR) &&
371                 !strValue.startsWith("#"))
372             {
373                 FacesContext facesContext = FacesContext.getCurrentInstance();
374                 if (facesContext.getApplication().getStateManager().isSavingStateInClient(facesContext))
375                 {
376                     // saving state in url depends on the work together
377                     // of 3 (theoretically) pluggable components:
378                     // ViewHandler, ResponseWriter and ViewTag
379                     // We should try to make this HtmlResponseWriterImpl able
380                     // to handle this alone!
381                     if (strValue.indexOf('?') < 0)
382                     {
383                         strValue = strValue + '?' + JspViewHandlerImpl.URL_STATE_MARKER;
384                     }
385                     else
386                     {
387                         strValue = strValue + '&' + JspViewHandlerImpl.URL_STATE_MARKER;
388                     }
389                 }
390             }
391             */
392             //_writer.write(strValue);
393             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encodeURIAtributte(strValue, _characterEncoding));
394         }
395         _writer.write('"');
396     }
397 
398     public void writeComment(Object value) throws IOException
399     {
400         if (value == null)
401         {
402             throw new NullPointerException("comment name must not be null");
403         }
404 
405         closeStartTagIfNecessary();
406         _writer.write("<!--");
407         _writer.write(value.toString());    //TODO: Escaping: must not have "-->" inside!
408         _writer.write("-->");
409     }
410 
411     public void writeText(Object value, String componentPropertyName) throws IOException
412     {
413         if (value == null)
414         {
415             throw new NullPointerException("Text must not be null.");
416         }
417 
418         closeStartTagIfNecessary();
419 
420         String strValue = value.toString();
421 
422         if (isScriptOrStyle())
423         {
424             // Don't bother encoding anything if chosen character encoding is UTF-8
425             if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
426             else _writer.write(UnicodeEncoder.encode(strValue) );
427         }
428         else
429         {
430             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
431         }
432     }
433 
434     public void writeText(char cbuf[], int off, int len) throws IOException
435     {
436         if (cbuf == null)
437         {
438             throw new NullPointerException("cbuf name must not be null");
439         }
440         if (cbuf.length < off + len)
441         {
442             throw new IndexOutOfBoundsException((off + len) + " > " + cbuf.length);
443         }
444 
445         closeStartTagIfNecessary();
446 
447         if (isScriptOrStyle())
448         {
449             String strValue = new String(cbuf, off, len);
450             // Don't bother encoding anything if chosen character encoding is UTF-8
451             if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
452             else _writer.write(UnicodeEncoder.encode(strValue) );
453         }
454         else if (isTextarea())
455         {
456             // For textareas we must *not* map successive spaces to &nbsp or Newlines to <br/>
457             // TODO: Make HTMLEncoder support char arrays directly
458             String strValue = new String(cbuf, off, len);
459             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encode(strValue, false, false, !UTF8.equals(_characterEncoding)));
460         }
461         else
462         {
463             // We map successive spaces to &nbsp; and Newlines to <br/>
464             // TODO: Make HTMLEncoder support char arrays directly
465             String strValue = new String(cbuf, off, len);
466             _writer.write(org.apache.myfaces.shared_orchestra.renderkit.html.util.HTMLEncoder.encode(strValue, true, true, !UTF8.equals(_characterEncoding)));
467         }
468     }
469 
470     private boolean isScriptOrStyle()
471     {
472         initializeStartedTagInfo();
473 
474         return (_isStyle != null && _isStyle.booleanValue()) ||
475                 (_isScript != null && _isScript.booleanValue());
476     }
477 
478     private boolean isScript()
479     {
480         initializeStartedTagInfo();
481 
482         return (_isScript != null && _isScript.booleanValue());
483     }
484 
485     private boolean isTextarea()
486     {
487         initializeStartedTagInfo();
488 
489         return _isTextArea != null && _isTextArea.booleanValue();
490     }
491 
492     private void initializeStartedTagInfo()
493     {
494         if(_startElementName != null)
495         {
496             if(_isScript==null)
497             {
498                 if(_startElementName.equalsIgnoreCase(HTML.SCRIPT_ELEM))
499                 {
500                     _isScript = Boolean.TRUE;
501                     _isStyle = Boolean.FALSE;
502                     _isTextArea = Boolean.FALSE;
503                 }
504                 else
505                 {
506                     _isScript = Boolean.FALSE;
507                 }
508             }
509             if(_isStyle == null)
510             {
511                 if(_startElementName.equalsIgnoreCase(org.apache.myfaces.shared_orchestra.renderkit.html.HTML.STYLE_ELEM))
512                 {
513                     _isStyle = Boolean.TRUE;
514                     _isTextArea = Boolean.FALSE;
515                 }
516                 else
517                 {
518                     _isStyle = Boolean.FALSE;
519                 }
520             }
521 
522             if(_isTextArea == null)
523             {
524                 if(_startElementName.equalsIgnoreCase(HTML.TEXTAREA_ELEM))
525                 {
526                     _isTextArea = Boolean.TRUE;
527                 }
528                 else
529                 {
530                     _isTextArea = Boolean.FALSE;
531                 }
532             }
533         }
534     }
535 
536     public ResponseWriter cloneWithWriter(Writer writer)
537     {
538         HtmlResponseWriterImpl newWriter
539                 = new HtmlResponseWriterImpl(writer, getContentType(), getCharacterEncoding());
540         newWriter._writeDummyForm = _writeDummyForm;
541         newWriter._dummyFormParams = _dummyFormParams;
542         return newWriter;
543     }
544 
545 
546     // Writer methods
547 
548     public void close() throws IOException
549     {
550         closeStartTagIfNecessary();
551         _writer.close();
552     }
553 
554     public void write(char cbuf[], int off, int len) throws IOException
555     {
556         closeStartTagIfNecessary();
557         String strValue = new String(cbuf, off, len);
558         // Don't bother encoding anything if chosen character encoding is UTF-8
559         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
560         else _writer.write(UnicodeEncoder.encode(strValue) );
561     }
562 
563     public void write(int c) throws IOException
564     {
565         closeStartTagIfNecessary();
566         _writer.write(c);
567     }
568 
569     public void write(char cbuf[]) throws IOException
570     {
571         closeStartTagIfNecessary();
572         String strValue = new String(cbuf);
573         // Don't bother encoding anything if chosen character encoding is UTF-8
574         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
575         else _writer.write(UnicodeEncoder.encode(strValue) );
576     }
577 
578     public void write(String str) throws IOException
579     {
580         closeStartTagIfNecessary();
581         // empty string commonly used to force the start tag to be closed.
582         // in such case, do not call down the writer chain
583         if (str.length() > 0)
584         {
585             // Don't bother encoding anything if chosen character encoding is UTF-8
586             if (UTF8.equals(_characterEncoding)) _writer.write(str);
587             else _writer.write(UnicodeEncoder.encode(str) );
588         }
589     }
590 
591     public void write(String str, int off, int len) throws IOException
592     {
593         closeStartTagIfNecessary();
594         String strValue = str.substring(off, off+len);
595         // Don't bother encoding anything if chosen character encoding is UTF-8
596         if (UTF8.equals(_characterEncoding)) _writer.write(strValue);
597         else _writer.write(UnicodeEncoder.encode(strValue) );
598     }
599 }