001/** 002 * Java Web Archive Toolkit - Software to read and validate ARC, WARC 003 * and GZip files. (http://jwat.org/) 004 * Copyright 2011-2012 Netarkivet.dk (http://netarkivet.dk/) 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.jwat.common; 019 020import java.util.HashMap; 021import java.util.Iterator; 022import java.util.Map; 023import java.util.Map.Entry; 024 025/** 026 * This class represents a content-type string parsed, validated and decomposed 027 * into its separate components. It's based on the rfc2616 text and accordingly 028 * fairly strict concerning whitespaces. Whitespace is only permissible after 029 * a ';' and before and after the whole content-type string. 030 * ContentType, MediaType and parameter names are all converted to lower case. 031 * 032 * @author nicl 033 */ 034public class ContentType { 035 036 /** Beginning state. */ 037 protected static final int S_START = 0; 038 /** Parsing content-type state. */ 039 protected static final int S_CONTENTTYPE = 1; 040 /** Parsing media-type state. */ 041 protected static final int S_MEDIATYPE = 2; 042 /** Lenient parsing of trailing whitespace after media-type state. */ 043 protected static final int S_MEDIATYPE_WHITESPACE = 3; 044 /** Parsing after a ';' character state. */ 045 protected static final int S_SEMICOLON = 4; 046 /** Parsing the parameter name state. */ 047 protected static final int S_PARAM_NAME = 5; 048 /** Parsing after a '=' character state. */ 049 protected static final int S_PARAM_EQ = 6; 050 /** Parsing a parameter value token state. */ 051 protected static final int S_PARAM_VALUE = 7; 052 /** Parsing a quote parameter value state. */ 053 protected static final int S_PARAM_QUOTED_VALUE = 8; 054 /** Parsing a quoted pair character state. */ 055 protected static final int S_PARAM_QUOTED_PAIR = 9; 056 /** Lenient parsing of trailing whitespace after argument value state. */ 057 protected static final int S_PARAM_VALUE_WHITESPACE = 10; 058 059 /** Control character characteristic. */ 060 protected static final int CC_CONTROL = 1; 061 /** Separator character characteristic. */ 062 protected static final int CC_SEPARATOR_WS = 2; 063 064 /** Parsed Content-type. */ 065 public String contentType; 066 067 /** Parsed Media-type. */ 068 public String mediaType; 069 070 /** Optional <code>Map</code> of parameters. */ 071 public Map<String, String> parameters; 072 073 /** rfc2616 separator minus space and tab. */ 074 protected static final String separators = "()<>@,;:\\\"/[]?={} \t"; 075 076 /** Table of separator and control characters. */ 077 protected static final byte[] charCharacteristicsTab = new byte[256]; 078 079 /* 080 * Populate table with separator and control characters. 081 */ 082 static { 083 for (int i=0; i<separators.length(); ++i) { 084 charCharacteristicsTab[separators.charAt(i)] = CC_SEPARATOR_WS; 085 } 086 for (int i=0; i<32; ++i) { 087 if (i != '\t') { 088 charCharacteristicsTab[i] = CC_CONTROL; 089 } 090 } 091 } 092 093 /** 094 * Check whether character is a valid token. 095 * @param c character to check 096 * @return boolean indicating whether character is a valid token 097 */ 098 public static boolean isTokenCharacter(int c) { 099 return (c >= 0 && c < 256 && charCharacteristicsTab[c] == 0) || c >= 256; 100 } 101 102 /** 103 * Tries to parse and validate the given content-type string. Also tries 104 * to parse and validate any optional parameters present in the string. 105 * @param contentTypeStr content-type string 106 * @return a content-type object with all information in separate 107 * structures. 108 */ 109 public static ContentType parseContentType(String contentTypeStr) { 110 if (contentTypeStr == null || contentTypeStr.length() == 0) { 111 return null; 112 } 113 ContentType ct = new ContentType(); 114 StringBuffer nameSb = new StringBuffer(); 115 StringBuffer valueSb = null; 116 int state = S_START; 117 int idx = 0; 118 int c; 119 boolean bLoop = true; 120 while (bLoop) { 121 if (idx < contentTypeStr.length()) { 122 c = contentTypeStr.charAt(idx); 123 } else { 124 c = -1; 125 bLoop = false; 126 } 127 switch (state) { 128 case S_START: 129 if (c == ' ' || c == '\t') { 130 ++idx; 131 } else if (isTokenCharacter(c)) { 132 nameSb.setLength(0); 133 nameSb.append((char) c); 134 ++idx; 135 state = S_CONTENTTYPE; 136 } else { 137 // /;= (-1) etc. 138 return null; 139 } 140 break; 141 case S_CONTENTTYPE: 142 if (isTokenCharacter(c)) { 143 nameSb.append((char) c); 144 ++idx; 145 } else if (c == '/') { 146 // Name always at least one. 147 ct.contentType = nameSb.toString().toLowerCase(); 148 ++idx; 149 nameSb.setLength(0); 150 state = S_MEDIATYPE; 151 } else { 152 // ;= (-1) etc. 153 return null; 154 } 155 break; 156 case S_MEDIATYPE: 157 if (isTokenCharacter(c)) { 158 nameSb.append((char) c); 159 ++idx; 160 } else if (c == -1) { 161 if (nameSb.length() == 0) { 162 return null; 163 } 164 ct.mediaType = nameSb.toString().toLowerCase(); 165 } else if (c == ';') { 166 if (nameSb.length() == 0) { 167 return null; 168 } 169 ct.mediaType = nameSb.toString().toLowerCase(); 170 ++idx; 171 valueSb = new StringBuffer(); 172 ct.parameters = new HashMap<String, String>(); 173 state = S_SEMICOLON; 174 } else if (c == ' ' || c == '\t') { 175 if (nameSb.length() == 0) { 176 return null; 177 } 178 ct.mediaType = nameSb.toString().toLowerCase(); 179 ++idx; 180 state = S_MEDIATYPE_WHITESPACE; 181 } else { 182 return null; 183 } 184 break; 185 case S_MEDIATYPE_WHITESPACE: 186 if (c == ' ' || c == '\t') { 187 ++idx; 188 } else if (c != -1) { 189 return null; 190 } 191 break; 192 case S_SEMICOLON: 193 if (c == ' ' || c == '\t') { 194 ++idx; 195 } else if (isTokenCharacter(c)) { 196 nameSb.setLength(0); 197 valueSb.setLength(0); 198 nameSb.append((char) c); 199 ++idx; 200 state = S_PARAM_NAME; 201 } else if (c == -1) { 202 // Allow contenttype/mediatype; and optional parameters 203 } else { 204 // /;= etc. 205 return null; 206 } 207 break; 208 case S_PARAM_NAME: 209 if (isTokenCharacter(c)) { 210 nameSb.append((char) c); 211 ++idx; 212 } else if (c == '=') { 213 // Name always at least one. 214 ++idx; 215 state = S_PARAM_EQ; 216 } else { 217 // (-1) etc. 218 return null; 219 } 220 break; 221 case S_PARAM_EQ: 222 if (isTokenCharacter(c)) { 223 valueSb.append((char) c); 224 ++idx; 225 state = S_PARAM_VALUE; 226 } else if (c == '"') { 227 ++idx; 228 state = S_PARAM_QUOTED_VALUE; 229 } 230 else { 231 // (-1) etc. 232 return null; 233 } 234 break; 235 case S_PARAM_VALUE: 236 if (isTokenCharacter(c)) { 237 valueSb.append((char) c); 238 ++idx; 239 } else if (c == -1) { 240 ct.parameters.put(nameSb.toString().toLowerCase(), 241 valueSb.toString()); 242 } else if (c == ';') { 243 ct.parameters.put(nameSb.toString().toLowerCase(), 244 valueSb.toString()); 245 ++idx; 246 state = S_SEMICOLON; 247 } else if (c == ' ' || c == '\t') { 248 ct.parameters.put(nameSb.toString().toLowerCase(), 249 valueSb.toString()); 250 ++idx; 251 state = S_PARAM_VALUE_WHITESPACE; 252 } else { 253 // etc. 254 return null; 255 } 256 break; 257 case S_PARAM_QUOTED_VALUE: 258 if (c == '"') { 259 ct.parameters.put(nameSb.toString().toLowerCase(), 260 valueSb.toString()); 261 ++idx; 262 state = S_PARAM_VALUE_WHITESPACE; 263 } else if (c == '\\') { 264 ++idx; 265 state = S_PARAM_QUOTED_PAIR; 266 } else if (c != -1) { 267 valueSb.append((char) c); 268 ++idx; 269 } else { 270 // (-1) 271 return null; 272 } 273 break; 274 case S_PARAM_QUOTED_PAIR: 275 if (c != -1) { 276 valueSb.append((char) c); 277 ++idx; 278 state = S_PARAM_QUOTED_VALUE; 279 } else { 280 // (-1) 281 return null; 282 } 283 break; 284 case S_PARAM_VALUE_WHITESPACE: 285 if (c == ' ' || c == '\t') { 286 ++idx; 287 } else if (c == ';') { 288 ++idx; 289 state = S_SEMICOLON; 290 } else if (c != -1) { 291 return null; 292 } 293 break; 294 } 295 } 296 return ct; 297 } 298 299 /** 300 * Return parameter value associated with supplied parameter name. 301 * @param name parameter name 302 * @return parameter value or null 303 */ 304 public String getParameter(String name) { 305 if (name == null || name.length() == 0 || parameters == null) { 306 return null; 307 } 308 return parameters.get(name.toLowerCase()); 309 } 310 311 /** 312 * Set a parameter with the given (name, value) pair. 313 * @param name parameter name 314 * @param value parameter value 315 */ 316 public void setParameter(String name, String value) { 317 if (name != null && name.length() > 0 && value != null) { 318 if (parameters == null) { 319 parameters = new HashMap<String, String>(); 320 } 321 parameters.put(name.toLowerCase(), value); 322 } 323 } 324 325 /** 326 * Determines whether a string should be quoted by examining if the string 327 * contains tabs and/or spaces. 328 * @param str input string 329 * @return boolean indicating if the string should be quoted 330 */ 331 public static boolean quote(String str) { 332 boolean quote = false; 333 if (str != null && str.length() > 0) { 334 int idx = 0; 335 char c; 336 while (idx<str.length() && !quote) { 337 c = str.charAt(idx++); 338 if (c < 256 && (charCharacteristicsTab[c] & CC_SEPARATOR_WS) != 0) { 339 quote = true; 340 } 341 } 342 } 343 return quote; 344 } 345 346 /** 347 * Returns the content-type omitting any parameters present. 348 * @return content-type without any parameters included 349 */ 350 public String toStringShort() { 351 StringBuffer sb = new StringBuffer(); 352 sb.append(contentType); 353 sb.append('/'); 354 sb.append(mediaType); 355 return sb.toString(); 356 } 357 358 @Override 359 public String toString() { 360 StringBuffer sb = new StringBuffer(); 361 sb.append(contentType); 362 sb.append('/'); 363 sb.append(mediaType); 364 if (parameters != null) { 365 Iterator<Entry<String, String>> iter = parameters.entrySet().iterator(); 366 Entry<String, String> entry; 367 while (iter.hasNext()) { 368 sb.append("; "); 369 entry = iter.next(); 370 sb.append(entry.getKey()); 371 sb.append('='); 372 if (quote(entry.getValue())) { 373 sb.append('"'); 374 sb.append(entry.getValue()); 375 sb.append('"'); 376 } 377 else { 378 sb.append(entry.getValue()); 379 } 380 } 381 } 382 return sb.toString(); 383 } 384 385 @Override 386 public boolean equals(Object obj) { 387 if (obj == null || !(obj instanceof ContentType)) { 388 return false; 389 } 390 ContentType contentTypeObj = (ContentType)obj; 391 if (contentType != null) { 392 if (!contentType.equals(contentTypeObj.contentType)) { 393 return false; 394 } 395 } else if (contentTypeObj.contentType != null) { 396 return false; 397 } 398 if (mediaType != null) { 399 if (!mediaType.equals(contentTypeObj.mediaType)) { 400 return false; 401 } 402 } else if (contentTypeObj.mediaType != null) { 403 return false; 404 } 405 if (parameters != null) { 406 if (contentTypeObj.parameters == null) { 407 return false; 408 } 409 if (parameters.size() != contentTypeObj.parameters.size()) { 410 return false; 411 } 412 if (parameters.size() > 0) { 413 Iterator<Entry<String, String>> iter = parameters.entrySet().iterator(); 414 Entry<String, String> entry; 415 String value; 416 while (iter.hasNext()) { 417 entry = iter.next(); 418 if (!contentTypeObj.parameters.containsKey(entry.getKey())) { 419 return false; 420 } 421 value = entry.getValue(); 422 if (value != null) { 423 if (!value.equals(contentTypeObj.parameters.get(entry.getKey()))) { 424 return false; 425 } 426 } else if (contentTypeObj.parameters.get(entry.getKey()) != null) { 427 return false; 428 } 429 } 430 } 431 } else if (contentTypeObj.parameters != null) { 432 return false; 433 } 434 return true; 435 } 436 437 @Override 438 public int hashCode() { 439 int hashCode = 0; 440 if (contentType != null) { 441 hashCode ^= contentType.hashCode(); 442 } 443 if (mediaType != null) { 444 hashCode ^= mediaType.hashCode(); 445 } 446 if (parameters != null) { 447 // Ensure null and empty parameters map does not mean object equality! 448 hashCode ^= 31331; 449 Iterator<Entry<String, String>> iter = parameters.entrySet().iterator(); 450 Entry<String, String> entry; 451 String value; 452 while (iter.hasNext()) { 453 entry = iter.next(); 454 hashCode ^= entry.getKey().hashCode(); 455 value = entry.getValue(); 456 if (value != null) { 457 hashCode ^= value.hashCode(); 458 } 459 } 460 } 461 return hashCode; 462 } 463 464}