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 }