You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by jw...@apache.org on 2008/04/14 20:46:38 UTC

svn commit: r647929 - in /myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal: taglib/listener/FileDownloadActionListener.java util/MimeUtility.java

Author: jwaldman
Date: Mon Apr 14 11:46:35 2008
New Revision: 647929

URL: http://svn.apache.org/viewvc?rev=647929&view=rev
Log:
TRINIDAD-974 nls: fileDownloadActionListener : mulitibyte char filename is garbled

Added a MimeUtility.java file to deal with this issue. The public static method is called from FileDownloadActionListener.java

Thanks Kenneth Tang for this patch.
trunk_1.2.x

Added:
    myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/MimeUtility.java
Modified:
    myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/taglib/listener/FileDownloadActionListener.java

Modified: myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/taglib/listener/FileDownloadActionListener.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/taglib/listener/FileDownloadActionListener.java?rev=647929&r1=647928&r2=647929&view=diff
==============================================================================
--- myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/taglib/listener/FileDownloadActionListener.java (original)
+++ myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/taglib/listener/FileDownloadActionListener.java Mon Apr 14 11:46:35 2008
@@ -21,6 +21,8 @@
 import java.io.BufferedOutputStream;
 import java.io.OutputStream;
 
+import java.util.Map;
+
 import javax.el.MethodExpression;
 
 import javax.faces.application.FacesMessage;
@@ -37,6 +39,7 @@
 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
 import org.apache.myfaces.trinidad.util.ComponentUtils;
 import org.apache.myfaces.trinidad.util.MessageFactory;
+import org.apache.myfaces.trinidadinternal.util.MimeUtility;
 
 
 /**
@@ -97,10 +100,15 @@
           // TODO: encoding?
           hsr.setContentType(contentType);
         if (filename != null)
-          // TODO: what about non-ASCII characters in the filename?
+        {
+          boolean isIE = false;
+          Map<String, String> headers = context.getExternalContext().getRequestHeaderMap();
+          if (headers.get("User-Agent").contains("MSIE"))
+            isIE = true;
+          // boolean isIE = CoreRenderer.isIE(RenderingContext.getCurrentInstance());
           hsr.setHeader("Content-Disposition",
-                        "attachment; filename=" + filename);
-        
+                        "attachment; filename=" + MimeUtility.encodeHTTPHeader(filename, isIE));
+        }
         MethodExpression method = getMethod();
         OutputStream out = new BufferedOutputStream(hsr.getOutputStream());
         method.invoke(context.getELContext(), new Object[]{context, out});

Added: myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/MimeUtility.java
URL: http://svn.apache.org/viewvc/myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/MimeUtility.java?rev=647929&view=auto
==============================================================================
--- myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/MimeUtility.java (added)
+++ myfaces/trinidad/trunk_1.2.x/trinidad-impl/src/main/java/org/apache/myfaces/trinidadinternal/util/MimeUtility.java Mon Apr 14 11:46:35 2008
@@ -0,0 +1,237 @@
+package org.apache.myfaces.trinidadinternal.util;
+
+import java.io.UnsupportedEncodingException;
+
+import java.net.URLEncoder;
+
+public class MimeUtility
+{
+  /**
+   * Encode a string for HTTP header value
+   * 
+   * @param word The string data to encode.
+   * @param isIE If true, use URL UTF-8 encoding, which MSIE expects.
+   *             If false, use RFC-2047, UTF-8 quoted-printable, according to HTTP 1.1 spec.
+   */
+  public static String encodeHTTPHeader(String word, boolean isIE)
+  {
+    if (isIE)
+    {
+      // IE requires UTF-8 URL encoded content-disposition header value
+      try
+      {
+        // IE does not understand "+ = space", workaround here
+        return URLEncoder.encode(word, "UTF-8").replace("+", "%20");
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        // this will not happen
+        return null;
+      }
+    }
+    else
+    {
+      // FF requires RFC-2047 encoded content-disposition header value
+      // according to HTTP 1.1
+      // use UTF-8 quoted-printable here
+
+      // this is the maximum size of a segment of encoded data, which is based off 
+      // of a 75 character size limit and all of the encoding overhead elements.
+      int sizeLimit = 63; // 75 - 7 - "UTF-8".length
+
+      StringBuffer result = new StringBuffer();
+
+      try
+      {
+        // The Apache MimeUtillity is designed to support different encoding mechanism, 
+        // quoted-printable, base64, UUEncode currently.
+        // In our case, we just need one, either base64 or quoted-printable
+        _encodeQuotedPrintable(word, result, sizeLimit, "UTF-8", true, false, _QP_WORD_SPECIALS);
+      }
+      catch (UnsupportedEncodingException e)
+      {
+        // this will not happen as UTF-8 is hardwired.
+      }
+      return result.toString();
+    }
+  }
+
+  /**
+   * Encode a string into quoted printable encoding, taking into 
+   * account the maximum segment length. 
+   * 
+   * @param data      The string data to encode.
+   * @param out       The output buffer used for the result.
+   * @param sizeLimit The maximum amount of encoded data we're allowed
+   *                  to have in a single encoded segment.
+   * @param charset   The character set marker that needs to be added to the
+   *                  encoding header.
+   * @param firstSegment
+   *                  If true, this is the first (left-most) segment in the
+   *                  data.  Used to determine if segment delimiters need to
+   *                  be added between sections.
+   * @param foldSegments
+   *                  Indicates the type of delimiter to use (blank or newline sequence).
+   * @param specials  The set of special characters that we require to encoded.
+   */
+  private static void _encodeQuotedPrintable(
+    String       data, 
+    StringBuffer out, 
+    int          sizeLimit, 
+    String       charset, 
+    boolean      firstSegment, 
+    boolean      foldSegments, 
+    String       specials)
+    throws UnsupportedEncodingException
+  {
+    // this needs to be converted into the appropriate transfer encoding. 
+    // The Apache MimeUtility can support encoding and decoding of different 
+    // charsets, UTF-8, Shift-JIS, etc.  
+    // To support this, it requires a Java-IANA charset name mapping.  
+    // For example, Java name = 8859_1 vs IANA name = ISO-8859-1.
+    // In our case, we need only UTF-8 only.  
+    // The Java name and IANA name are same here.  
+    // So, simplified the logics and remove this. 
+    // byte [] bytes = data.getBytes(javaCharset(charset)); 
+    byte[] bytes = data.getBytes(charset);
+
+    int estimatedSize = _estimateQPEncodedLength(bytes, specials);
+
+    // if the estimated encoding size is over our segment limit, split the string in half and 
+    // recurse.  Eventually we'll reach a point where things are small enough.  
+    if (estimatedSize > sizeLimit)
+    {
+      // the first segment indicator travels with the left half. 
+      _encodeQuotedPrintable(data.substring(0, data.length() / 2), out, sizeLimit, charset, 
+                             firstSegment, foldSegments, specials);
+      // the second half can never be the first segment 
+      _encodeQuotedPrintable(data.substring(data.length() / 2), out, sizeLimit, charset, false, 
+                             foldSegments, specials);
+    }
+    else
+    {
+      // if this is not the first sement of the encoding, we need to add either a blank or 
+      // a newline sequence to the data 
+      if (!firstSegment)
+      {
+        if (foldSegments)
+        {
+          out.append("\r\n");
+        }
+        else
+        {
+          out.append(' ');
+        }
+      }
+      // do the encoding of the segment.
+      _encodeQPWord(bytes, out, charset, specials);
+    }
+  }
+
+  /**
+   * Perform RFC-2047 word encoding using Base64 data encoding.
+   * 
+   * @param data     The source for the encoded data.
+   * @param out      The output stream where the encoded data is to be written.
+   * @param charset  The charset tag to be added to each encoded data section.
+   * @param specials The set of special characters that we require to encoded.
+   * 
+   */
+  private static void _encodeQPWord(
+    byte[]       data, 
+    StringBuffer out, 
+    String       charset, 
+    String       specials)
+  {
+    // append the word header 
+    out.append("=?");
+    out.append(charset);
+    out.append("?Q?");
+    // add on the encodeded data       
+    _encodeQPWordData(data, out, specials);
+    // the end of the encoding marker 
+    out.append("?=");
+  }
+
+  /**
+   * Perform RFC-2047 word encoding using Q-P data encoding.
+   *
+   * @param data     The source for the encoded data.
+   * @param specials The set of special characters that we require to encoded.
+   * @param out      The StringBuffer where the encoded data is to be written.
+   */
+  private static void _encodeQPWordData(
+    byte[]       data, 
+    StringBuffer out, 
+    String       specials)
+  {
+    for (int i = 0; i < data.length; i++)
+    {
+      int ch = data[i] & 0xff;
+      ;
+
+      // spaces require special handling.  If the next character is a line terminator, then
+      // the space needs to be encoded.
+      if (ch == ' ')
+      {
+        // blanks get translated into underscores, 
+        // because the encoded tokens can't have embedded blanks.
+        out.append('_');
+      }
+      // non-ascii chars and the designated specials all get encoded.
+      else if (ch < 32 || ch >= 127 || specials.indexOf(ch) != -1)
+      {
+        out.append('=');
+        out.append((char) _ENCODING_TABLE[ch >> 4]);
+        out.append((char) _ENCODING_TABLE[ch & 0x0F]);
+      }
+      else
+      {
+        // good character, just use unchanged.
+        out.append((char) ch);
+      }
+    }
+  }
+
+
+  /**
+   * Estimate the final encoded size of a segment of data. 
+   * This is used to ensure that the encoded blocks do 
+   * not get split across a unicode character boundary and 
+   * that the encoding will fit within the bounds of 
+   * a mail header line. 
+   * 
+   * @param data     The data we're anticipating encoding.
+   * @param specials The set of special characters that we require to encoded.
+   * @return         The size of the byte data in encoded form. 
+   */
+  private static int _estimateQPEncodedLength(byte[] data, String specials)
+  {
+    int count = 0;
+
+    for (int i = 0; i < data.length; i++)
+    {
+      // make sure this is just a single byte value.
+      int ch = data[i] & 0xff;
+
+      // non-ascii chars and the designated specials all get encoded.
+      if (ch < 32 || ch >= 127 || specials.indexOf(ch) != -1)
+      {
+        // Q encoding translates a single char into 3 characters 
+        count += 3;
+      }
+      else
+      {
+        // non-encoded character 
+        count++;
+      }
+    }
+    return count;
+  }
+  
+  private static final String _QP_WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~";
+  private static final byte[] _ENCODING_TABLE =
+  { (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', 
+    (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', 
+    (byte) 'F' };
+}