1 /*
   2  * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.activation;
  27 
  28 import java.util.Hashtable;
  29 import java.util.Enumeration;
  30 import java.util.Locale;
  31 
  32 /**
  33  * A parameter list of a MimeType
  34  * as defined in RFC 2045 and 2046. The Primary type of the
  35  * object must already be stripped off.
  36  *
  37  * @see javax.activation.MimeType
  38  *
  39  * @since 1.6
  40  */
  41 public class MimeTypeParameterList {
  42     private Hashtable parameters;
  43 
  44     /**
  45      * A string that holds all the special chars.
  46      */
  47     private static final String TSPECIALS = "()<>@,;:/[]?=\\\"";
  48 
  49 
  50     /**
  51      * Default constructor.
  52      */
  53     public MimeTypeParameterList() {
  54         parameters = new Hashtable();
  55     }
  56 
  57     /**
  58      * Constructs a new MimeTypeParameterList with the passed in data.
  59      *
  60      * @param parameterList an RFC 2045, 2046 compliant parameter list.
  61      * @exception       MimeTypeParseException  if the MIME type can't be parsed
  62      */
  63     public MimeTypeParameterList(String parameterList)
  64                                         throws MimeTypeParseException {
  65         parameters = new Hashtable();
  66 
  67         //    now parse rawdata
  68         parse(parameterList);
  69     }
  70 
  71     /**
  72      * A routine for parsing the parameter list out of a String.
  73      *
  74      * @param parameterList an RFC 2045, 2046 compliant parameter list.
  75      * @exception       MimeTypeParseException  if the MIME type can't be parsed
  76      */
  77     protected void parse(String parameterList) throws MimeTypeParseException {
  78         if (parameterList == null)
  79             return;
  80 
  81         int length = parameterList.length();
  82         if (length <= 0)
  83             return;
  84 
  85         int i;
  86         char c;
  87         for (i = skipWhiteSpace(parameterList, 0);
  88                 i < length && (c = parameterList.charAt(i)) == ';';
  89                 i = skipWhiteSpace(parameterList, i)) {
  90             int lastIndex;
  91             String name;
  92             String value;
  93 
  94             //    eat the ';'
  95             i++;
  96 
  97             //    now parse the parameter name
  98 
  99             //    skip whitespace
 100             i = skipWhiteSpace(parameterList, i);
 101 
 102             // tolerate trailing semicolon, even though it violates the spec
 103             if (i >= length)
 104                 return;
 105 
 106             //    find the end of the token char run
 107             lastIndex = i;
 108             while ((i < length) && isTokenChar(parameterList.charAt(i)))
 109                 i++;
 110 
 111             name = parameterList.substring(lastIndex, i).
 112                                                 toLowerCase(Locale.ENGLISH);
 113 
 114             //    now parse the '=' that separates the name from the value
 115             i = skipWhiteSpace(parameterList, i);
 116 
 117             if (i >= length || parameterList.charAt(i) != '=')
 118                 throw new MimeTypeParseException(
 119                     "Couldn't find the '=' that separates a " +
 120                     "parameter name from its value.");
 121 
 122             //    eat it and parse the parameter value
 123             i++;
 124             i = skipWhiteSpace(parameterList, i);
 125 
 126             if (i >= length)
 127                 throw new MimeTypeParseException(
 128                         "Couldn't find a value for parameter named " + name);
 129 
 130             //    now find out whether or not we have a quoted value
 131             c = parameterList.charAt(i);
 132             if (c == '"') {
 133                 //    yup it's quoted so eat it and capture the quoted string
 134                 i++;
 135                 if (i >= length)
 136                     throw new MimeTypeParseException(
 137                             "Encountered unterminated quoted parameter value.");
 138 
 139                 lastIndex = i;
 140 
 141                 //    find the next unescaped quote
 142                 while (i < length) {
 143                     c = parameterList.charAt(i);
 144                     if (c == '"')
 145                         break;
 146                     if (c == '\\') {
 147                         //    found an escape sequence
 148                         //    so skip this and the
 149                         //    next character
 150                         i++;
 151                     }
 152                     i++;
 153                 }
 154                 if (c != '"')
 155                     throw new MimeTypeParseException(
 156                         "Encountered unterminated quoted parameter value.");
 157 
 158                 value = unquote(parameterList.substring(lastIndex, i));
 159                 //    eat the quote
 160                 i++;
 161             } else if (isTokenChar(c)) {
 162                 //    nope it's an ordinary token so it
 163                 //    ends with a non-token char
 164                 lastIndex = i;
 165                 while (i < length && isTokenChar(parameterList.charAt(i)))
 166                     i++;
 167                 value = parameterList.substring(lastIndex, i);
 168             } else {
 169                 //    it ain't a value
 170                 throw new MimeTypeParseException(
 171                         "Unexpected character encountered at index " + i);
 172             }
 173 
 174             //    now put the data into the hashtable
 175             parameters.put(name, value);
 176         }
 177         if (i < length) {
 178             throw new MimeTypeParseException(
 179                 "More characters encountered in input than expected.");
 180         }
 181     }
 182 
 183     /**
 184      * Return the number of name-value pairs in this list.
 185      *
 186      * @return  the number of parameters
 187      */
 188     public int size() {
 189         return parameters.size();
 190     }
 191 
 192     /**
 193      * Determine whether or not this list is empty.
 194      *
 195      * @return  true if there are no parameters
 196      */
 197     public boolean isEmpty() {
 198         return parameters.isEmpty();
 199     }
 200 
 201     /**
 202      * Retrieve the value associated with the given name, or null if there
 203      * is no current association.
 204      *
 205      * @param name      the parameter name
 206      * @return          the parameter's value
 207      */
 208     public String get(String name) {
 209         return (String)parameters.get(name.trim().toLowerCase(Locale.ENGLISH));
 210     }
 211 
 212     /**
 213      * Set the value to be associated with the given name, replacing
 214      * any previous association.
 215      *
 216      * @param name      the parameter name
 217      * @param value     the parameter's value
 218      */
 219     public void set(String name, String value) {
 220         parameters.put(name.trim().toLowerCase(Locale.ENGLISH), value);
 221     }
 222 
 223     /**
 224      * Remove any value associated with the given name.
 225      *
 226      * @param name      the parameter name
 227      */
 228     public void remove(String name) {
 229         parameters.remove(name.trim().toLowerCase(Locale.ENGLISH));
 230     }
 231 
 232     /**
 233      * Retrieve an enumeration of all the names in this list.
 234      *
 235      * @return  an enumeration of all parameter names
 236      */
 237     public Enumeration getNames() {
 238         return parameters.keys();
 239     }
 240 
 241     /**
 242      * Return a string representation of this object.
 243      */
 244     public String toString() {
 245         StringBuffer buffer = new StringBuffer();
 246         buffer.ensureCapacity(parameters.size() * 16);
 247                         //    heuristic: 8 characters per field
 248 
 249         Enumeration keys = parameters.keys();
 250         while (keys.hasMoreElements()) {
 251             String key = (String)keys.nextElement();
 252             buffer.append("; ");
 253             buffer.append(key);
 254             buffer.append('=');
 255             buffer.append(quote((String)parameters.get(key)));
 256         }
 257 
 258         return buffer.toString();
 259     }
 260 
 261     //    below here be scary parsing related things
 262 
 263     /**
 264      * Determine whether or not a given character belongs to a legal token.
 265      */
 266     private static boolean isTokenChar(char c) {
 267         return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0);
 268     }
 269 
 270     /**
 271      * return the index of the first non white space character in
 272      * rawdata at or after index i.
 273      */
 274     private static int skipWhiteSpace(String rawdata, int i) {
 275         int length = rawdata.length();
 276         while ((i < length) && Character.isWhitespace(rawdata.charAt(i)))
 277             i++;
 278         return i;
 279     }
 280 
 281     /**
 282      * A routine that knows how and when to quote and escape the given value.
 283      */
 284     private static String quote(String value) {
 285         boolean needsQuotes = false;
 286 
 287         //    check to see if we actually have to quote this thing
 288         int length = value.length();
 289         for (int i = 0; (i < length) && !needsQuotes; i++) {
 290             needsQuotes = !isTokenChar(value.charAt(i));
 291         }
 292 
 293         if (needsQuotes) {
 294             StringBuffer buffer = new StringBuffer();
 295             buffer.ensureCapacity((int)(length * 1.5));
 296 
 297             //    add the initial quote
 298             buffer.append('"');
 299 
 300             //    add the properly escaped text
 301             for (int i = 0; i < length; ++i) {
 302                 char c = value.charAt(i);
 303                 if ((c == '\\') || (c == '"'))
 304                     buffer.append('\\');
 305                 buffer.append(c);
 306             }
 307 
 308             //    add the closing quote
 309             buffer.append('"');
 310 
 311             return buffer.toString();
 312         } else {
 313             return value;
 314         }
 315     }
 316 
 317     /**
 318      * A routine that knows how to strip the quotes and
 319      * escape sequences from the given value.
 320      */
 321     private static String unquote(String value) {
 322         int valueLength = value.length();
 323         StringBuffer buffer = new StringBuffer();
 324         buffer.ensureCapacity(valueLength);
 325 
 326         boolean escaped = false;
 327         for (int i = 0; i < valueLength; ++i) {
 328             char currentChar = value.charAt(i);
 329             if (!escaped && (currentChar != '\\')) {
 330                 buffer.append(currentChar);
 331             } else if (escaped) {
 332                 buffer.append(currentChar);
 333                 escaped = false;
 334             } else {
 335                 escaped = true;
 336             }
 337         }
 338 
 339         return buffer.toString();
 340     }
 341 }