You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@chemistry.apache.org by fg...@apache.org on 2011/02/10 15:37:18 UTC

svn commit: r1069413 - in /incubator/chemistry/opencmis/trunk: chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ chemistry-opencmis-commons/chemistry-opencmis-commons-i...

Author: fguillaume
Date: Thu Feb 10 14:37:17 2011
New Revision: 1069413

URL: http://svn.apache.org/viewvc?rev=1069413&view=rev
Log:
CMIS-301: pass filename for setContentStream PUT using Content-Disposition header

Added:
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java   (with props)
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java   (with props)
Modified:
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/AbstractAtomPubService.java
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/HttpUtils.java
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ObjectServiceImpl.java
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-bindings/src/main/java/org/apache/chemistry/opencmis/server/impl/atompub/ObjectService.java
    incubator/chemistry/opencmis/trunk/chemistry-opencmis-test/chemistry-opencmis-test-fit/src/test/java/org/apache/chemistry/opencmis/fit/runtime/AbstractWriteObjectIT.java

Modified: incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/AbstractAtomPubService.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/AbstractAtomPubService.java?rev=1069413&r1=1069412&r2=1069413&view=diff
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/AbstractAtomPubService.java (original)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/AbstractAtomPubService.java Thu Feb 10 14:37:17 2011
@@ -504,8 +504,16 @@ public class AbstractAtomPubService {
      * result.
      */
     protected HttpUtils.Response put(UrlBuilder url, String contentType, HttpUtils.Output writer) {
+        return put(url, contentType, null, writer);
+    }
+
+    /**
+     * Performs a PUT on an URL, checks the response code and returns the
+     * result.
+     */
+    protected HttpUtils.Response put(UrlBuilder url, String contentType, Map<String, String> headers, HttpUtils.Output writer) {
         // make the call
-        HttpUtils.Response resp = HttpUtils.invokePUT(url, contentType, writer, session);
+        HttpUtils.Response resp = HttpUtils.invokePUT(url, contentType, headers, writer, session);
 
         // check response code
         if ((resp.getResponseCode() < 200) || (resp.getResponseCode() > 299)) {

Modified: incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/HttpUtils.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/HttpUtils.java?rev=1069413&r1=1069412&r2=1069413&view=diff
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/HttpUtils.java (original)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/HttpUtils.java Thu Feb 10 14:37:17 2011
@@ -57,27 +57,28 @@ public class HttpUtils {
     }
 
     public static Response invokeGET(UrlBuilder url, Session session) {
-        return invoke(url, "GET", null, null, session, null, null);
+        return invoke(url, "GET", null, null, null, session, null, null);
     }
 
     public static Response invokeGET(UrlBuilder url, Session session, BigInteger offset, BigInteger length) {
-        return invoke(url, "GET", null, null, session, offset, length);
+        return invoke(url, "GET", null, null, null, session, offset, length);
     }
 
     public static Response invokePOST(UrlBuilder url, String contentType, Output writer, Session session) {
-        return invoke(url, "POST", contentType, writer, session, null, null);
+        return invoke(url, "POST", contentType, null, writer, session, null, null);
     }
 
-    public static Response invokePUT(UrlBuilder url, String contentType, Output writer, Session session) {
-        return invoke(url, "PUT", contentType, writer, session, null, null);
+    public static Response invokePUT(UrlBuilder url, String contentType, Map<String, String> headers, Output writer, Session session) {
+        return invoke(url, "PUT", contentType, headers, writer, session, null, null);
     }
 
     public static Response invokeDELETE(UrlBuilder url, Session session) {
-        return invoke(url, "DELETE", null, null, session, null, null);
+        return invoke(url, "DELETE", null, null, null, session, null, null);
     }
 
-    private static Response invoke(UrlBuilder url, String method, String contentType, Output writer, Session session,
-            BigInteger offset, BigInteger length) {
+    private static Response invoke(UrlBuilder url, String method,
+            String contentType, Map<String, String> headers, Output writer,
+            Session session, BigInteger offset, BigInteger length) {
         try {
             // log before connect
             if (log.isDebugEnabled()) {
@@ -95,6 +96,12 @@ public class HttpUtils {
             if (contentType != null) {
                 conn.setRequestProperty("Content-Type", contentType);
             }
+            // set other headers
+            if (headers != null) {
+                for (Map.Entry<String, String> header : headers.entrySet()) {
+                    conn.setRequestProperty(header.getKey(), header.getValue());
+                }
+            }
 
             // authenticate
             AbstractAuthenticationProvider authProvider = CmisBindingsHelper.getAuthenticationProvider(session);

Modified: incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ObjectServiceImpl.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ObjectServiceImpl.java?rev=1069413&r1=1069412&r2=1069413&view=diff
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ObjectServiceImpl.java (original)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-client/chemistry-opencmis-client-bindings/src/main/java/org/apache/chemistry/opencmis/client/bindings/spi/atompub/ObjectServiceImpl.java Thu Feb 10 14:37:17 2011
@@ -26,6 +26,7 @@ import java.io.OutputStream;
 import java.math.BigInteger;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.chemistry.opencmis.client.bindings.spi.Session;
 import org.apache.chemistry.opencmis.client.bindings.spi.atompub.objects.AtomAllowableActions;
@@ -51,6 +52,7 @@ import org.apache.chemistry.opencmis.com
 import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
 import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
 import org.apache.chemistry.opencmis.commons.impl.Constants;
+import org.apache.chemistry.opencmis.commons.impl.MimeHelper;
 import org.apache.chemistry.opencmis.commons.impl.ReturnVersion;
 import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
@@ -523,8 +525,15 @@ public class ObjectServiceImpl extends A
 
         final InputStream stream = contentStream.getStream();
 
+        // Content-Disposition header for the filename
+        Map<String, String> headers = Collections.singletonMap(
+                MimeHelper.CONTENT_DISPOSITION,
+                MimeHelper.encodeContentDisposition(
+                        MimeHelper.DISPOSITION_ATTACHMENT,
+                        contentStream.getFileName()));
+
         // send content
-        HttpUtils.Response resp = HttpUtils.invokePUT(url, contentStream.getMimeType(), new HttpUtils.Output() {
+        HttpUtils.Response resp = put(url, contentStream.getMimeType(), headers, new HttpUtils.Output() {
             public void write(OutputStream out) throws Exception {
                 int b;
                 byte[] buffer = new byte[4096];
@@ -535,9 +544,9 @@ public class ObjectServiceImpl extends A
 
                 stream.close();
             }
-        }, getSession());
+        });
 
-        // check response code
+        // check response code further
         if ((resp.getResponseCode() != 200) && (resp.getResponseCode() != 201) && (resp.getResponseCode() != 204)) {
             throw convertStatusCode(resp.getResponseCode(), resp.getResponseMessage(), resp.getErrorContent(), null);
         }

Added: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java?rev=1069413&view=auto
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java (added)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java Thu Feb 10 14:37:17 2011
@@ -0,0 +1,560 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Contributors:
+ *     Original contributors from geronimo-javamail_1.4_spec-1.7.1
+ *     Florent Guillaume
+ */
+package org.apache.chemistry.opencmis.commons.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * MIME helper class.
+ */
+public class MimeHelper {
+
+    public static final String CONTENT_DISPOSITION = "Content-Disposition";
+
+    public static final String DISPOSITION_ATTACHMENT = "attachment";
+
+    public static final String DISPOSITION_FILENAME = "filename";
+
+    // RFC 2045
+    private static final String MIME_SPECIALS = "()<>@,;:\\\"/[]?=" + "\t ";
+
+    private static final String RFC2231_SPECIALS = "*'%" + MIME_SPECIALS;
+
+    private static final String WHITE = " \t\n\r";
+
+    private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
+
+    private static final byte[] HEX_DECODE = new byte[0x80];
+    static {
+        for (int i = 0; i < HEX_DIGITS.length; i++) {
+            HEX_DECODE[HEX_DIGITS[i]] = (byte) i;
+        }
+    }
+
+    private MimeHelper() {
+    }
+
+    /**
+     * Encodes a value per RFC 2231.
+     * <p>
+     * This is used to pass non-ASCII parameters to MIME parameter lists.
+     * <p>
+     * This implementation always uses UTF-8 and no language.
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc2231">RFC 2231</a> for
+     * details.
+     *
+     * @param value the value to encode
+     * @param buf the buffer to fill
+     * @return {@code true} if an encoding was needed, or {@code false} if no
+     *         encoding was actually needed
+     */
+    protected static boolean encodeRFC2231value(String value, StringBuilder buf) {
+        String charset = "UTF-8";
+        buf.append(charset);
+        buf.append("''"); // no language
+        byte[] bytes;
+        try {
+            bytes = value.getBytes(charset);
+        } catch (UnsupportedEncodingException e) {
+            return true;
+        }
+        boolean encoded = false;
+        for (int i = 0; i < bytes.length; i++) {
+            int ch = bytes[i] & 0xff;
+            if (ch <= 32 || ch >= 127 || RFC2231_SPECIALS.indexOf(ch) != -1) {
+                buf.append('%');
+                buf.append(HEX_DIGITS[ch >> 4]);
+                buf.append(HEX_DIGITS[ch & 0xf]);
+                encoded = true;
+            } else {
+                buf.append((char) ch);
+            }
+        }
+        return encoded;
+    }
+
+    /**
+     * Encodes a MIME parameter per RFC 2231.
+     * <p>
+     * This implementation always uses UTF-8 and no language.
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc2231">RFC 2231</a> for
+     * details.
+     *
+     * @param value the string to encode
+     * @return the encoded string
+     */
+    protected static String encodeRFC2231(String key, String value) {
+        StringBuilder buf = new StringBuilder();
+        boolean encoded = encodeRFC2231value(value, buf);
+        if (encoded) {
+            return "; " + key + "*=" + buf.toString();
+        } else {
+            return "; " + key + "=" + value;
+        }
+    }
+
+    /**
+     * Encodes the Content-Disposition header value according to RFC 2183 and
+     * RFC 2231.
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc2231">RFC 2231</a> for
+     * details.
+     *
+     * @param disposition the disposition
+     * @param filename the file name
+     * @return the encoded header value
+     */
+    public static String encodeContentDisposition(String disposition,
+            String filename) {
+        if (disposition == null) {
+            disposition = DISPOSITION_ATTACHMENT;
+        }
+        return disposition + encodeRFC2231(DISPOSITION_FILENAME, filename);
+    }
+
+    /**
+     * Decodes a filename from the Content-Disposition header value according to
+     * RFC 2183 and RFC 2231.
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc2231">RFC 2231</a> for
+     * details.
+     *
+     * @param value the header value to decode
+     * @return the filename
+     */
+    public static String decodeContentDispositionFilename(String value) {
+        Map<String, String> params = new HashMap<String, String>();
+        decodeContentDisposition(value, params);
+        return params.get(DISPOSITION_FILENAME);
+    }
+
+    /**
+     * Decodes the Content-Disposition header value according to RFC 2183 and
+     * RFC 2231.
+     * <p>
+     * Does not deal with continuation lines.
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc2231">RFC 2231</a> for
+     * details.
+     *
+     * @param value the header value to decode
+     * @param params the map of parameters to fill
+     * @return the disposition
+     *
+     */
+    public static String decodeContentDisposition(String value,
+            Map<String, String> params) {
+        try {
+            HeaderTokenizer tokenizer = new HeaderTokenizer(value);
+            // get the first token, which must be an ATOM
+            Token token = tokenizer.next();
+            if (token.getType() != Token.ATOM) {
+                return null;
+            }
+            String disposition = token.getValue();
+            // value ignored in this method
+
+            // the remainder is the parameters
+            String remainder = tokenizer.getRemainder();
+            if (remainder != null) {
+                getParameters(remainder, params);
+            }
+            return disposition;
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    protected static class ParseException extends Exception {
+        private static final long serialVersionUID = 1L;
+
+        public ParseException() {
+            super();
+        }
+
+        public ParseException(String message) {
+            super(message);
+        }
+    }
+
+    /*
+     * From geronimo-javamail_1.4_spec-1.7.1. Token
+     */
+    protected static class Token {
+        // Constant values from J2SE 1.4 API Docs (Constant values)
+        public static final int ATOM = -1;
+
+        public static final int COMMENT = -3;
+
+        public static final int EOF = -4;
+
+        public static final int QUOTEDSTRING = -2;
+
+        private int _type;
+
+        private String _value;
+
+        public Token(int type, String value) {
+            _type = type;
+            _value = value;
+        }
+
+        public int getType() {
+            return _type;
+        }
+
+        public String getValue() {
+            return _value;
+        }
+    }
+
+    /*
+     * Tweaked from geronimo-javamail_1.4_spec-1.7.1. HeaderTokenizer
+     */
+    protected static class HeaderTokenizer {
+
+        private static final Token EOF = new Token(Token.EOF, null);
+
+        private final String header;
+
+        private final String delimiters;
+
+        private final boolean skipComments;
+
+        private int pos;
+
+        public HeaderTokenizer(String header) {
+            this(header, MIME_SPECIALS, true);
+        }
+
+        protected HeaderTokenizer(String header, String delimiters,
+                boolean skipComments) {
+            this.header = header;
+            this.delimiters = delimiters;
+            this.skipComments = skipComments;
+        }
+
+        public String getRemainder() {
+            return header.substring(pos);
+        }
+
+        public Token next() throws ParseException {
+            return readToken();
+        }
+
+        /**
+         * Read an ATOM token from the parsed header.
+         *
+         * @return A token containing the value of the atom token.
+         */
+        private Token readAtomicToken() {
+            // skip to next delimiter
+            int start = pos;
+            while (++pos < header.length()) {
+                // break on the first non-atom character.
+                char ch = header.charAt(pos);
+                if (delimiters.indexOf(header.charAt(pos)) != -1 || ch < 32
+                        || ch >= 127) {
+                    break;
+                }
+            }
+            return new Token(Token.ATOM, header.substring(start, pos));
+        }
+
+        /**
+         * Read the next token from the header.
+         *
+         * @return The next token from the header. White space is skipped, and
+         *         comment tokens are also skipped if indicated.
+         */
+        private Token readToken() throws ParseException {
+            if (pos >= header.length()) {
+                return EOF;
+            } else {
+                char c = header.charAt(pos);
+                // comment token...read and skip over this
+                if (c == '(') {
+                    Token comment = readComment();
+                    if (skipComments) {
+                        return readToken();
+                    } else {
+                        return comment;
+                    }
+                    // quoted literal
+                } else if (c == '\"') {
+                    return readQuotedString();
+                    // white space, eat this and find a real token.
+                } else if (WHITE.indexOf(c) != -1) {
+                    eatWhiteSpace();
+                    return readToken();
+                    // either a CTL or special. These characters have a
+                    // self-defining token type.
+                } else if (c < 32 || c >= 127 || delimiters.indexOf(c) != -1) {
+                    pos++;
+                    return new Token((int) c, String.valueOf(c));
+                } else {
+                    // start of an atom, parse it off.
+                    return readAtomicToken();
+                }
+            }
+        }
+
+        /**
+         * Extract a substring from the header string and apply any
+         * escaping/folding rules to the string.
+         *
+         * @param start The starting offset in the header.
+         * @param end The header end offset + 1.
+         * @return The processed string value.
+         */
+        private String getEscapedValue(int start, int end)
+                throws ParseException {
+            StringBuffer value = new StringBuffer();
+            for (int i = start; i < end; i++) {
+                char ch = header.charAt(i);
+                // is this an escape character?
+                if (ch == '\\') {
+                    i++;
+                    if (i == end) {
+                        throw new ParseException("Invalid escape character");
+                    }
+                    value.append(header.charAt(i));
+                }
+                // line breaks are ignored, except for naked '\n' characters,
+                // which are consider
+                // parts of linear whitespace.
+                else if (ch == '\r') {
+                    // see if this is a CRLF sequence, and skip the second if it
+                    // is.
+                    if (i < end - 1 && header.charAt(i + 1) == '\n') {
+                        i++;
+                    }
+                } else {
+                    // just append the ch value.
+                    value.append(ch);
+                }
+            }
+            return value.toString();
+        }
+
+        /**
+         * Read a comment from the header, applying nesting and escape rules to
+         * the content.
+         *
+         * @return A comment token with the token value.
+         */
+        private Token readComment() throws ParseException {
+            int start = pos + 1;
+            int nesting = 1;
+            boolean requiresEscaping = false;
+            // skip to end of comment/string
+            while (++pos < header.length()) {
+                char ch = header.charAt(pos);
+                if (ch == ')') {
+                    nesting--;
+                    if (nesting == 0) {
+                        break;
+                    }
+                } else if (ch == '(') {
+                    nesting++;
+                } else if (ch == '\\') {
+                    pos++;
+                    requiresEscaping = true;
+                }
+                // we need to process line breaks also
+                else if (ch == '\r') {
+                    requiresEscaping = true;
+                }
+            }
+            if (nesting != 0) {
+                throw new ParseException("Unbalanced comments");
+            }
+            String value;
+            if (requiresEscaping) {
+                value = getEscapedValue(start, pos);
+            } else {
+                value = header.substring(start, pos++);
+            }
+            return new Token(Token.COMMENT, value);
+        }
+
+        /**
+         * Parse out a quoted string from the header, applying escaping rules to
+         * the value.
+         *
+         * @return The QUOTEDSTRING token with the value.
+         * @exception ParseException
+         */
+        private Token readQuotedString() throws ParseException {
+            int start = pos + 1;
+            boolean requiresEscaping = false;
+            // skip to end of comment/string
+            while (++pos < header.length()) {
+                char ch = header.charAt(pos);
+                if (ch == '"') {
+                    String value;
+                    if (requiresEscaping) {
+                        value = getEscapedValue(start, pos++);
+                    } else {
+                        value = header.substring(start, pos++);
+                    }
+                    return new Token(Token.QUOTEDSTRING, value);
+                } else if (ch == '\\') {
+                    pos++;
+                    requiresEscaping = true;
+                }
+                // we need to process line breaks also
+                else if (ch == '\r') {
+                    requiresEscaping = true;
+                }
+            }
+            throw new ParseException("Missing '\"'");
+        }
+
+        /**
+         * Skip white space in the token string.
+         */
+        private void eatWhiteSpace() {
+            // skip to end of whitespace
+            while (++pos < header.length()
+                    && WHITE.indexOf(header.charAt(pos)) != -1)
+                ;
+        }
+    }
+
+    /*
+     * Tweaked from geronimo-javamail_1.4_spec-1.7.1. ParameterList
+     */
+    protected static Map<String, String> getParameters(String list,
+            Map<String, String> params) throws ParseException {
+        HeaderTokenizer tokenizer = new HeaderTokenizer(list);
+        while (true) {
+            Token token = tokenizer.next();
+            switch (token.getType()) {
+            case Token.EOF:
+                // the EOF token terminates parsing.
+                return params;
+
+            case ';':
+                // each new parameter is separated by a semicolon, including
+                // the first, which separates
+                // the parameters from the main part of the header.
+                // the next token needs to be a parameter name
+                token = tokenizer.next();
+                // allow a trailing semicolon on the parameters.
+                if (token.getType() == Token.EOF) {
+                    return params;
+                }
+
+                if (token.getType() != Token.ATOM) {
+                    throw new ParseException("Invalid parameter name: "
+                            + token.getValue());
+                }
+
+                // get the parameter name as a lower case version for better
+                // mapping.
+                String name = token.getValue().toLowerCase();
+
+                token = tokenizer.next();
+
+                // parameters are name=value, so we must have the "=" here.
+                if (token.getType() != '=') {
+                    throw new ParseException("Missing '='");
+                }
+
+                // now the value, which may be an atom or a literal
+                token = tokenizer.next();
+
+                if (token.getType() != Token.ATOM
+                        && token.getType() != Token.QUOTEDSTRING) {
+                    throw new ParseException("Invalid parameter value: "
+                            + token.getValue());
+                }
+
+                String value = token.getValue();
+
+                // we might have to do some additional decoding. A name that
+                // ends with "*" is marked as being encoded, so if requested, we
+                // decode the value.
+                if (name.endsWith("*")) {
+                    name = name.substring(0, name.length() - 1);
+                    value = decodeRFC2231value(value);
+                }
+                params.put(name, value);
+                break;
+            default:
+                throw new ParseException("Missing ';'");
+            }
+        }
+    }
+
+    protected static String decodeRFC2231value(String value) {
+        int q1 = value.indexOf('\'');
+        if (q1 == -1) {
+            // missing charset
+            return value;
+        }
+        String mimeCharset = value.substring(0, q1);
+        int q2 = value.indexOf('\'', q1 + 1);
+        if (q2 == -1) {
+            // missing language
+            return value;
+        }
+        byte[] bytes = fromHex(value.substring(q2 + 1));
+        try {
+            return new String(bytes, getJavaCharset(mimeCharset));
+        } catch (UnsupportedEncodingException e) {
+            // incorrect encoding
+            return value;
+        }
+    }
+
+    protected static byte[] fromHex(String data) {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        for (int i = 0; i < data.length();) {
+            char c = data.charAt(i++);
+            if (c == '%') {
+                if (i > data.length() - 2) {
+                    break; // unterminated sequence
+                }
+                byte b1 = HEX_DECODE[data.charAt(i++) & 0x7f];
+                byte b2 = HEX_DECODE[data.charAt(i++) & 0x7f];
+                out.write((b1 << 4) | b2);
+            } else {
+                out.write((byte) c);
+            }
+        }
+        return out.toByteArray();
+    }
+
+    protected static String getJavaCharset(String mimeCharset) {
+        // good enough for standard values
+        return mimeCharset;
+    }
+
+}

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java
------------------------------------------------------------------------------
    svn:keywords = Id

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/main/java/org/apache/chemistry/opencmis/commons/impl/MimeHelper.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java?rev=1069413&view=auto
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java (added)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java Thu Feb 10 14:37:17 2011
@@ -0,0 +1,102 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ *
+ * Contributors:
+ *     Florent Guillaume
+ */
+package org.apache.chemistry.opencmis.commons.impl.misc;
+
+import static org.apache.chemistry.opencmis.commons.impl.MimeHelper.decodeContentDisposition;
+import static org.apache.chemistry.opencmis.commons.impl.MimeHelper.decodeContentDispositionFilename;
+import static org.apache.chemistry.opencmis.commons.impl.MimeHelper.encodeContentDisposition;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+public class MimeHelperTest extends TestCase {
+
+    public void testEncodeContentDisposition() {
+        assertEquals("inline; filename=foo.bar",
+                encodeContentDisposition("inline", "foo.bar"));
+        assertEquals("attachment; filename=foo.bar",
+                encodeContentDisposition(null, "foo.bar"));
+        assertEquals("attachment; filename*=UTF-8''caf%C3%A9.pdf",
+                encodeContentDisposition(null, "caf\u00e9.pdf"));
+        assertEquals(
+                "attachment; filename*=UTF-8''%20%27%2A%25%20abc%20%C2%81%C2%82%0D%0A%09",
+                encodeContentDisposition(null, " '*% abc \u0081\u0082\r\n\t"));
+    }
+
+    public void testDecodeContentDisposition() {
+        Map<String, String> params = new HashMap<String, String>();
+        Map<String, String> expected = new HashMap<String, String>();
+
+        assertEquals("attachment",
+                decodeContentDisposition("attachment; a=b; c=d", params));
+        expected.put("a", "b");
+        expected.put("c", "d");
+        assertEquals(expected, params);
+        params.clear();
+        expected.clear();
+
+        assertEquals(
+                "inline",
+                decodeContentDisposition(
+                        "  inline ; a = \"b b\" (this is a comment) ; c =d;",
+                        params));
+        expected.put("a", "b b");
+        expected.put("c", "d");
+        assertEquals(expected, params);
+        params.clear();
+        expected.clear();
+
+        assertEquals(
+                "inline",
+                decodeContentDisposition(
+                        "inline; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
+                        params));
+        assertEquals(Collections.singletonMap("modification-date",
+                "Wed, 12 Feb 1997 16:29:51 -0500"), params);
+        params.clear();
+    }
+
+    public void testDecodeContentDispositionFilename() {
+        assertNull(decodeContentDispositionFilename("attachment; a=b; c=d;"));
+        assertNull(decodeContentDispositionFilename("inline"));
+        assertNull(decodeContentDispositionFilename("inline; modification-date=\"Wed, 12 Feb 1997 16:29:51 -0500\""));
+        assertEquals(
+                "foo.bar",
+                decodeContentDispositionFilename("attachment; filename=foo.bar"));
+        assertEquals(
+                "foo.bar",
+                decodeContentDispositionFilename("attachment; filename = \"foo.bar\""));
+        assertEquals(
+                "foo.bar",
+                decodeContentDispositionFilename(" guess ; filename = (this is rfc822 a comment) \"foo.bar\""));
+        assertEquals(
+                "caf\u00e9.pdf",
+                decodeContentDispositionFilename("foo; filename*=UTF-8''caf%C3%A9.pdf"));
+        assertEquals(
+                "caf\u00e9.pdf",
+                decodeContentDispositionFilename("bar; filename*=ISO-8859-1''caf%E9.pdf"));
+    }
+
+}

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java
------------------------------------------------------------------------------
    svn:keywords = Id

Propchange: incubator/chemistry/opencmis/trunk/chemistry-opencmis-commons/chemistry-opencmis-commons-impl/src/test/java/org/apache/chemistry/opencmis/commons/impl/misc/MimeHelperTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: incubator/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-bindings/src/main/java/org/apache/chemistry/opencmis/server/impl/atompub/ObjectService.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-bindings/src/main/java/org/apache/chemistry/opencmis/server/impl/atompub/ObjectService.java?rev=1069413&r1=1069412&r2=1069413&view=diff
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-bindings/src/main/java/org/apache/chemistry/opencmis/server/impl/atompub/ObjectService.java (original)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-server/chemistry-opencmis-server-bindings/src/main/java/org/apache/chemistry/opencmis/server/impl/atompub/ObjectService.java Thu Feb 10 14:37:17 2011
@@ -51,6 +51,7 @@ import org.apache.chemistry.opencmis.com
 import org.apache.chemistry.opencmis.commons.enums.VersioningState;
 import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
 import org.apache.chemistry.opencmis.commons.impl.Constants;
+import org.apache.chemistry.opencmis.commons.impl.MimeHelper;
 import org.apache.chemistry.opencmis.commons.impl.ReturnVersion;
 import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
@@ -213,6 +214,10 @@ public final class ObjectService {
             } catch (NumberFormatException e) {
             }
         }
+        String contentDisposition = request.getHeader(MimeHelper.CONTENT_DISPOSITION);
+        if (contentDisposition != null) {
+            contentStream.setFileName(MimeHelper.decodeContentDispositionFilename(contentDisposition));
+        }
 
         // execute
         Holder<String> objectIdHolder = new Holder<String>(objectId);

Modified: incubator/chemistry/opencmis/trunk/chemistry-opencmis-test/chemistry-opencmis-test-fit/src/test/java/org/apache/chemistry/opencmis/fit/runtime/AbstractWriteObjectIT.java
URL: http://svn.apache.org/viewvc/incubator/chemistry/opencmis/trunk/chemistry-opencmis-test/chemistry-opencmis-test-fit/src/test/java/org/apache/chemistry/opencmis/fit/runtime/AbstractWriteObjectIT.java?rev=1069413&r1=1069412&r2=1069413&view=diff
==============================================================================
--- incubator/chemistry/opencmis/trunk/chemistry-opencmis-test/chemistry-opencmis-test-fit/src/test/java/org/apache/chemistry/opencmis/fit/runtime/AbstractWriteObjectIT.java (original)
+++ incubator/chemistry/opencmis/trunk/chemistry-opencmis-test/chemistry-opencmis-test-fit/src/test/java/org/apache/chemistry/opencmis/fit/runtime/AbstractWriteObjectIT.java Thu Feb 10 14:37:17 2011
@@ -20,11 +20,8 @@ package org.apache.chemistry.opencmis.fi
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.fail;
 
 import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.HashMap;
@@ -33,15 +30,12 @@ import java.util.UUID;
 
 import org.apache.chemistry.opencmis.client.api.Document;
 import org.apache.chemistry.opencmis.client.api.ObjectId;
-import org.apache.chemistry.opencmis.client.api.OperationContext;
 import org.apache.chemistry.opencmis.client.api.Property;
-import org.apache.chemistry.opencmis.client.api.TransientDocument;
 import org.apache.chemistry.opencmis.commons.PropertyIds;
 import org.apache.chemistry.opencmis.commons.data.ContentStream;
 import org.apache.chemistry.opencmis.commons.enums.VersioningState;
 import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
 import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
-import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
 import org.junit.Ignore;
 import org.junit.Test;
 
@@ -301,9 +295,10 @@ public abstract class AbstractWriteObjec
         assertNotNull(doc);
         doc.setContentStream(contentStream, true);
 
-        // Assert.assertEquals(buf1.length, doc.getContentStreamLength());
-        // Assert.assertEquals(mimetype, doc.getContentStreamMimeType());
-        // Assert.assertEquals(filename, doc.getContentStreamFileName());
+        doc.refresh();
+        assertEquals(buf1.length, doc.getContentStreamLength());
+        assertEquals(mimetype, doc.getContentStreamMimeType());
+        assertEquals(filename, doc.getContentStreamFileName());
         String content2 = this.getContentAsString(doc.getContentStream());
         assertEquals(content1, content2);
     }
@@ -337,9 +332,10 @@ public abstract class AbstractWriteObjec
         assertNotNull(doc);
         doc.setContentStream(contentStream, false);
 
-        // Assert.assertEquals(buf1.length, doc.getContentStreamLength());
-        // Assert.assertEquals(mimetype, doc.getContentStreamMimeType());
-        // Assert.assertEquals(filename, doc.getContentStreamFileName());
+        doc.refresh();
+        assertEquals(buf1.length, doc.getContentStreamLength());
+        assertEquals(mimetype, doc.getContentStreamMimeType());
+        assertEquals(filename, doc.getContentStreamFileName());
         String content2 = this.getContentAsString(doc.getContentStream());
         assertEquals(content1, content2);
     }
@@ -381,9 +377,10 @@ public abstract class AbstractWriteObjec
         assertNotNull(doc);
         doc.setContentStream(contentStream2, true);
 
-        // Assert.assertEquals(buf1.length, doc.getContentStreamLength());
-        // Assert.assertEquals(mimetype, doc.getContentStreamMimeType());
-        // Assert.assertEquals(filename, doc.getContentStreamFileName());
+        doc.refresh();
+        assertEquals(buf2.length, doc.getContentStreamLength());
+        assertEquals(mimetype, doc.getContentStreamMimeType());
+        assertEquals(filename2, doc.getContentStreamFileName());
         String content3 = this.getContentAsString(doc.getContentStream());
         assertEquals(content2, content3);
     }
@@ -426,9 +423,10 @@ public abstract class AbstractWriteObjec
         assertNotNull(doc);
         doc.setContentStream(contentStream2, false);
 
-        // Assert.assertEquals(buf1.length, doc.getContentStreamLength());
-        // Assert.assertEquals(mimetype, doc.getContentStreamMimeType());
-        // Assert.assertEquals(filename, doc.getContentStreamFileName());
+        doc.refresh();
+        assertEquals(buf2.length, doc.getContentStreamLength());
+        assertEquals(mimetype, doc.getContentStreamMimeType());
+        assertEquals(filename2, doc.getContentStreamFileName());
         String content3 = this.getContentAsString(doc.getContentStream());
         assertEquals(content2, content3);
     }