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 java.awt.datatransfer;
27
28 import java.util.Enumeration;
29 import java.util.Hashtable;
30 import java.util.Iterator;
31 import java.util.Map;
32 import java.util.Set;
33
34 /**
35 * An object that encapsulates the parameter list of a MimeType as defined in
36 * RFC 2045 and 2046.
37 *
38 * @author jeff.dunn@eng.sun.com
39 */
40 class MimeTypeParameterList implements Cloneable {
41
42 /**
43 * Default constructor.
44 */
45 public MimeTypeParameterList() {
46 parameters = new Hashtable<>();
47 }
48
49 public MimeTypeParameterList(String rawdata)
50 throws MimeTypeParseException
51 {
52 parameters = new Hashtable<>();
53
54 // now parse rawdata
55 parse(rawdata);
56 }
57
58 public int hashCode() {
59 int code = Integer.MAX_VALUE/45; // "random" value for empty lists
60 String paramName = null;
61 Enumeration<String> enum_ = this.getNames();
62
63 while (enum_.hasMoreElements()) {
64 paramName = enum_.nextElement();
65 code += paramName.hashCode();
66 code += this.get(paramName).hashCode();
67 }
68
69 return code;
70 } // hashCode()
71
72 /**
73 * Two parameter lists are considered equal if they have exactly the same
74 * set of parameter names and associated values. The order of the parameters
75 * is not considered.
76 */
77 public boolean equals(Object thatObject) {
78 //System.out.println("MimeTypeParameterList.equals("+this+","+thatObject+")");
79 if (!(thatObject instanceof MimeTypeParameterList)) {
80 return false;
81 }
82 MimeTypeParameterList that = (MimeTypeParameterList)thatObject;
83 if (this.size() != that.size()) {
84 return false;
85 }
86 String name = null;
87 String thisValue = null;
88 String thatValue = null;
89 Set<Map.Entry<String, String>> entries = parameters.entrySet();
90 Iterator<Map.Entry<String, String>> iterator = entries.iterator();
91 Map.Entry<String, String> entry = null;
92 while (iterator.hasNext()) {
93 entry = iterator.next();
94 name = entry.getKey();
95 thisValue = entry.getValue();
96 thatValue = that.parameters.get(name);
97 if ((thisValue == null) || (thatValue == null)) {
98 // both null -> equal, only one null -> not equal
99 if (thisValue != thatValue) {
100 return false;
101 }
102 } else if (!thisValue.equals(thatValue)) {
103 return false;
104 }
105 } // while iterator
106
107 return true;
108 } // equals()
109
110 /**
111 * A routine for parsing the parameter list out of a String.
112 */
113 protected void parse(String rawdata) throws MimeTypeParseException {
114 int length = rawdata.length();
115 if(length > 0) {
116 int currentIndex = skipWhiteSpace(rawdata, 0);
117 int lastIndex = 0;
118
119 if(currentIndex < length) {
120 char currentChar = rawdata.charAt(currentIndex);
121 while ((currentIndex < length) && (currentChar == ';')) {
122 String name;
123 String value;
124 boolean foundit;
125
126 // eat the ';'
127 ++currentIndex;
128
129 // now parse the parameter name
130
131 // skip whitespace
132 currentIndex = skipWhiteSpace(rawdata, currentIndex);
133
134 if(currentIndex < length) {
135 // find the end of the token char run
136 lastIndex = currentIndex;
137 currentChar = rawdata.charAt(currentIndex);
138 while((currentIndex < length) && isTokenChar(currentChar)) {
139 ++currentIndex;
140 currentChar = rawdata.charAt(currentIndex);
141 }
142 name = rawdata.substring(lastIndex, currentIndex).toLowerCase();
143
144 // now parse the '=' that separates the name from the value
145
146 // skip whitespace
147 currentIndex = skipWhiteSpace(rawdata, currentIndex);
148
149 if((currentIndex < length) && (rawdata.charAt(currentIndex) == '=')) {
150 // eat it and parse the parameter value
151 ++currentIndex;
152
153 // skip whitespace
154 currentIndex = skipWhiteSpace(rawdata, currentIndex);
155
156 if(currentIndex < length) {
157 // now find out whether or not we have a quoted value
158 currentChar = rawdata.charAt(currentIndex);
159 if(currentChar == '"') {
160 // yup it's quoted so eat it and capture the quoted string
161 ++currentIndex;
162 lastIndex = currentIndex;
163
164 if(currentIndex < length) {
165 // find the next unescaped quote
166 foundit = false;
167 while((currentIndex < length) && !foundit) {
168 currentChar = rawdata.charAt(currentIndex);
169 if(currentChar == '\\') {
170 // found an escape sequence so pass this and the next character
171 currentIndex += 2;
172 } else if(currentChar == '"') {
173 // found it!
174 foundit = true;
175 } else {
176 ++currentIndex;
177 }
178 }
179 if(currentChar == '"') {
180 value = unquote(rawdata.substring(lastIndex, currentIndex));
181 // eat the quote
182 ++currentIndex;
183 } else {
184 throw new MimeTypeParseException("Encountered unterminated quoted parameter value.");
185 }
186 } else {
187 throw new MimeTypeParseException("Encountered unterminated quoted parameter value.");
188 }
189 } else if(isTokenChar(currentChar)) {
190 // nope it's an ordinary token so it ends with a non-token char
191 lastIndex = currentIndex;
192 foundit = false;
193 while((currentIndex < length) && !foundit) {
194 currentChar = rawdata.charAt(currentIndex);
195
196 if(isTokenChar(currentChar)) {
197 ++currentIndex;
198 } else {
199 foundit = true;
200 }
201 }
202 value = rawdata.substring(lastIndex, currentIndex);
203 } else {
204 // it ain't a value
205 throw new MimeTypeParseException("Unexpected character encountered at index " + currentIndex);
206 }
207
208 // now put the data into the hashtable
209 parameters.put(name, value);
210 } else {
211 throw new MimeTypeParseException("Couldn't find a value for parameter named " + name);
212 }
213 } else {
214 throw new MimeTypeParseException("Couldn't find the '=' that separates a parameter name from its value.");
215 }
216 } else {
217 throw new MimeTypeParseException("Couldn't find parameter name");
218 }
219
220 // setup the next iteration
221 currentIndex = skipWhiteSpace(rawdata, currentIndex);
222 if(currentIndex < length) {
223 currentChar = rawdata.charAt(currentIndex);
224 }
225 }
226 if(currentIndex < length) {
227 throw new MimeTypeParseException("More characters encountered in input than expected.");
228 }
229 }
230 }
231 }
232
233 /**
234 * return the number of name-value pairs in this list.
235 */
236 public int size() {
237 return parameters.size();
238 }
239
240 /**
241 * Determine whether or not this list is empty.
242 */
243 public boolean isEmpty() {
244 return parameters.isEmpty();
245 }
246
247 /**
248 * Retrieve the value associated with the given name, or {@code null} if
249 * there is no current association.
250 */
251 public String get(String name) {
252 return parameters.get(name.trim().toLowerCase());
253 }
254
255 /**
256 * Set the value to be associated with the given name, replacing any
257 * previous association.
258 */
259 public void set(String name, String value) {
260 parameters.put(name.trim().toLowerCase(), value);
261 }
262
263 /**
264 * Remove any value associated with the given name.
265 */
266 public void remove(String name) {
267 parameters.remove(name.trim().toLowerCase());
268 }
269
270 /**
271 * Retrieve an enumeration of all the names in this list.
272 */
273 public Enumeration<String> getNames() {
274 return parameters.keys();
275 }
276
277 public String toString() {
278 // Heuristic: 8 characters per field
279 StringBuilder buffer = new StringBuilder(parameters.size() * 16);
280
281 Enumeration<String> keys = parameters.keys();
282 while(keys.hasMoreElements())
283 {
284 buffer.append("; ");
285
286 String key = keys.nextElement();
287 buffer.append(key);
288 buffer.append('=');
289 buffer.append(quote(parameters.get(key)));
290 }
291
292 return buffer.toString();
293 }
294
295 /**
296 * Returns a clone of this object.
297 *
298 * @return a clone of this object
299 */
300 @SuppressWarnings("unchecked") // Cast from clone
301 public Object clone() {
302 MimeTypeParameterList newObj = null;
303 try {
304 newObj = (MimeTypeParameterList)super.clone();
305 } catch (CloneNotSupportedException cannotHappen) {
306 }
307 newObj.parameters = (Hashtable<String, String>)parameters.clone();
308 return newObj;
309 }
310
311 private Hashtable<String, String> parameters;
312
313 // below here be scary parsing related things
314
315 /**
316 * Determine whether or not a given character belongs to a legal token.
317 */
318 private static boolean isTokenChar(char c) {
319 return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0);
320 }
321
322 /**
323 * Returns the index of the first non white space character in
324 * {@code rawdata} at or after index {@code i}.
325 */
326 private static int skipWhiteSpace(String rawdata, int i) {
327 int length = rawdata.length();
328 if (i < length) {
329 char c = rawdata.charAt(i);
330 while ((i < length) && Character.isWhitespace(c)) {
331 ++i;
332 c = rawdata.charAt(i);
333 }
334 }
335
336 return i;
337 }
338
339 /**
340 * A routine that knows how and when to quote and escape the given value.
341 */
342 private static String quote(String value) {
343 boolean needsQuotes = false;
344
345 // check to see if we actually have to quote this thing
346 int length = value.length();
347 for(int i = 0; (i < length) && !needsQuotes; ++i) {
348 needsQuotes = !isTokenChar(value.charAt(i));
349 }
350
351 if(needsQuotes) {
352 StringBuilder buffer = new StringBuilder((int)(length * 1.5));
353
354 // add the initial quote
355 buffer.append('"');
356
357 // add the properly escaped text
358 for(int i = 0; i < length; ++i) {
359 char c = value.charAt(i);
360 if((c == '\\') || (c == '"')) {
361 buffer.append('\\');
362 }
363 buffer.append(c);
364 }
365
366 // add the closing quote
367 buffer.append('"');
368
369 return buffer.toString();
370 }
371 else
372 {
373 return value;
374 }
375 }
376
377 /**
378 * A routine that knows how to strip the quotes and escape sequences from
379 * the given value.
380 */
381 private static String unquote(String value) {
382 int valueLength = value.length();
383 StringBuilder buffer = new StringBuilder(valueLength);
384
385 boolean escaped = false;
386 for(int i = 0; i < valueLength; ++i) {
387 char currentChar = value.charAt(i);
388 if(!escaped && (currentChar != '\\')) {
389 buffer.append(currentChar);
390 } else if(escaped) {
391 buffer.append(currentChar);
392 escaped = false;
393 } else {
394 escaped = true;
395 }
396 }
397
398 return buffer.toString();
399 }
400
401 /**
402 * A string that holds all the special chars.
403 */
404 private static final String TSPECIALS = "()<>@,;:\\\"/[]?=";
405 }
406