001/* Copyright (c) 2006 Google Inc. With trivial modifications (marked MCA) for Votorola. 002 * 003 * Licensed under the Apache License, Version 2.0 (the "License"); 004 * you may not use this file except in compliance with the License. 005 * You may obtain a copy of the License at 006 * 007 * http://www.apache.org/licenses/LICENSE-2.0 008 * 009 * Unless required by applicable law or agreed to in writing, software 010 * distributed under the License is distributed on an "AS IS" BASIS, 011 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 * See the License for the specific language governing permissions and 013 * limitations under the License. 014 */ 015 016 017package votorola.g.web; 018 019//import com.google.gdata.client.Service; 020 021import java.util.HashMap; 022import java.util.List; 023import java.util.regex.Matcher; 024import java.util.regex.Pattern; 025 026/** 027 * Simple class for parsing and generating Content-Type header values, per 028 * RFC 2045 (MIME) and 2616 (HTTP 1.1). 029 * 030 * 031 */ 032public class ContentType { 033 034 private static String TOKEN = 035 "[\\p{ASCII}&&[^\\p{Cntrl} ;/=\\[\\]\\(\\)\\<\\>\\@\\,\\:\\\"\\?\\=]]+"; 036 037 // Precisely matches a token 038 private static Pattern TOKEN_PATTERN = Pattern.compile( 039 "^" + TOKEN + "$"); 040 041 // Matches a media type value 042 private static Pattern TYPE_PATTERN = Pattern.compile( 043 "(" + TOKEN + ")" + // type (G1) 044 "/" + // separator 045 "(" + TOKEN + ")" + // subtype (G2) 046 "\\s*(.*)\\s*", Pattern.DOTALL); 047 048 // Matches an attribute value 049 private static Pattern ATTR_PATTERN = Pattern.compile( 050 "\\s*;\\s*" + 051 "(" + TOKEN + ")" + // attr name (G1) 052 "\\s*=\\s*" + 053 "(?:" + 054 "\"([^\"]*)\"" + // value as quoted string (G3) 055 "|" + 056 "(" + TOKEN + ")?" + // value as token (G2) 057 ")" 058 ); 059 060 /** 061 * Name of the attribute that contains the encoding character set for 062 * the content type. 063 * @see #getCharset() 064 */ 065 public static final String ATTR_CHARSET = "charset"; 066 067 /** 068 * Special "*" character to match any type or subtype. 069 */ 070 private static final String STAR = "*"; 071 072 /** 073 * The UTF-8 charset encoding is used by default for all text and xml 074 * based MIME types. 075 */ 076 private static final String DEFAULT_CHARSET = ATTR_CHARSET + "=UTF-8"; 077 078 /** 079 * A ContentType constant that describes the base unqualified Atom content 080 * type. 081 */ 082 public static final ContentType ATOM = 083 new ContentType("application/atom+xml;" + DEFAULT_CHARSET); 084 085 /** 086 * A ContentType constant that describes the qualified Atom entry content 087 * type. 088 * 089 * @see #getAtomEntry() 090 */ 091 static final ContentType ATOM_ENTRY = 092 new ContentType("application/atom+xml;type=entry;" + DEFAULT_CHARSET) { 093 @Override 094 public boolean match(ContentType acceptedContentType) { 095 String type = acceptedContentType.getAttribute("type"); 096 return super.match(acceptedContentType) && 097 (type == null || type.equals("entry")); 098 } 099 }; 100 101 /** 102 * A ContentType constant that describes the qualified Atom feed content 103 * type. 104 * 105 * @see #getAtomFeed() 106 */ 107 static final ContentType ATOM_FEED = 108 new ContentType("application/atom+xml;type=feed;" + DEFAULT_CHARSET) { 109 @Override 110 public boolean match(ContentType acceptedContentType) { 111 String type = acceptedContentType.getAttribute("type"); 112 return super.match(acceptedContentType) && 113 (type == null || type.equals("feed")); 114 } 115 }; 116 117 /** 118 * Returns the ContentType that should be used in contexts that expect 119 * an Atom entry. 120 * 121 * @throws UnsupportedOperationException 122 */ 123 public static ContentType getAtomEntry() { 124 // Use the unqualifed type for v1, the qualifed one for later versions 125// return Service.getVersion().isCompatible(Service.Versions.V1) ? 126// ATOM : ATOM_ENTRY; 127//// MCA: 128 throw new UnsupportedOperationException(); 129 } 130 131 /** 132 * Returns the ContentType that should be used in contexts that expect 133 * an Atom feed. 134 * 135 * @throws UnsupportedOperationException 136 */ 137 public static ContentType getAtomFeed() { 138 // Use the unqualified type for v1, the qualified one for later versions 139// return Service.getVersion().isCompatible(Service.Versions.V1) ? 140// ATOM : ATOM_FEED; 141//// MCA: 142 throw new UnsupportedOperationException(); 143 } 144 145 /** 146 * A ContentType constant that describes the Atom Service content type. 147 */ 148 public static final ContentType ATOM_SERVICE = 149 new ContentType("application/atomsvc+xml;" + DEFAULT_CHARSET); 150 151 /** 152 * A ContentType constant that describes the RSS channel/item content type. 153 */ 154 public static final ContentType RSS = 155 new ContentType("application/rss+xml;" + DEFAULT_CHARSET); 156 157 /** 158 * A ContentType constant that describes the JSON content type. 159 */ 160 public static final ContentType JSON = 161 new ContentType("application/json;" + DEFAULT_CHARSET); 162 163 /** 164 * A ContentType constant that describes the JavaScript content type. 165 */ 166 public static final ContentType JAVASCRIPT = 167 new ContentType("text/javascript;" + DEFAULT_CHARSET); 168 169 /** 170 * A ContentType constant that describes the generic text/xml content type. 171 */ 172 public static final ContentType TEXT_XML = 173 new ContentType("text/xml;" + DEFAULT_CHARSET); 174 175 /** 176 * A ContentType constant that describes the generic text/html content type. 177 */ 178 public static final ContentType TEXT_HTML = 179 new ContentType("text/html;" + DEFAULT_CHARSET); 180 181 /** 182 * A ContentType constant that describes the generic text/plain content type. 183 */ 184 public static final ContentType TEXT_PLAIN = 185 new ContentType("text/plain;" + DEFAULT_CHARSET); 186 187 /** 188 * A ContentType constant that describes the MIME multipart/related content 189 * type. 190 */ 191 public static final ContentType MULTIPART_RELATED = 192 new ContentType("multipart/related"); 193 194 /** 195 * Determines the best "Content-Type" header to use in a servlet response 196 * based on the "Accept" header from a servlet request. 197 * 198 * @param acceptHeader "Accept" header value from a servlet request (not 199 * <code>null</code>) 200 * @param actualContentTypes actual content types in descending order of 201 * preference (non-empty, and each entry is of the 202 * form "type/subtype" without the wildcard char 203 * '*') or <code>null</code> if no "Accept" header 204 * was specified 205 * @return the best content type to use (or <code>null</code> on no match). 206 */ 207 public static ContentType getBestContentType(String acceptHeader, 208 List<ContentType> actualContentTypes) { 209 210 // If not accept header is specified, return the first actual type 211 if (acceptHeader == null) { 212 return actualContentTypes.get(0); 213 } 214 215 // iterate over all of the accepted content types to find the best match 216 float bestQ = 0; 217 ContentType bestContentType = null; 218 String[] acceptedTypes = acceptHeader.split(","); 219 for (String acceptedTypeString : acceptedTypes) { 220 221 // create the content type object 222 ContentType acceptedContentType; 223 try { 224 acceptedContentType = new ContentType(acceptedTypeString.trim()); 225 } catch (IllegalArgumentException ex) { 226 // ignore exception 227 continue; 228 } 229 230 // parse the "q" value (default of 1) 231 float curQ = 1; 232 try { 233 String qAttr = acceptedContentType.getAttribute("q"); 234 if (qAttr != null) { 235 float qValue = Float.valueOf(qAttr); 236 if (qValue <= 0 || qValue > 1) { 237 continue; 238 } 239 curQ = qValue; 240 } 241 } catch (NumberFormatException ex) { 242 // ignore exception 243 continue; 244 } 245 246 // only check it if it's at least as good ("q") as the best one so far 247 if (curQ < bestQ) { 248 continue; 249 } 250 251 /* iterate over the actual content types in order to find the best match 252 to the current accepted content type */ 253 for (ContentType actualContentType : actualContentTypes) { 254 255 /* if the "q" value is the same as the current best, only check for 256 better content types */ 257 if (curQ == bestQ && bestContentType == actualContentType) { 258 break; 259 } 260 261 /* check if the accepted content type matches the current actual 262 content type */ 263 if (actualContentType.match(acceptedContentType)) { 264 bestContentType = actualContentType; 265 bestQ = curQ; 266 break; 267 } 268 } 269 } 270 271 // if found an acceptable content type, return the best one 272 if (bestQ != 0) { 273 return bestContentType; 274 } 275 276 // Return null if no match 277 return null; 278 } 279 280 /** 281 * Constructs a new instance with default media type 282 */ 283 public ContentType() { 284 this(null); 285 } 286 287 /** 288 * Constructs a new instance from a content-type header value 289 * parsing the MIME content type (RFC2045) format. If the type 290 * is {@code null}, then media type and charset will be 291 * initialized to default values. 292 * 293 * @param typeHeader content type value in RFC2045 header format. 294 */ 295 public ContentType(String typeHeader) { 296 297 // If the type header is no provided, then use the HTTP defaults. 298 if (typeHeader == null) { 299 type = "application"; 300 subType = "octet-stream"; 301 attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default 302 return; 303 } 304 305 // Get type and subtype 306 Matcher typeMatch = TYPE_PATTERN.matcher(typeHeader); 307 if (!typeMatch.matches()) { 308 throw new IllegalArgumentException("Invalid media type:" + typeHeader); 309 } 310 311 type = typeMatch.group(1).toLowerCase(); 312 subType = typeMatch.group(2).toLowerCase(); 313 if (typeMatch.groupCount() < 3) { 314 return; 315 } 316 317 // Get attributes (if any) 318 Matcher attrMatch = ATTR_PATTERN.matcher(typeMatch.group(3)); 319 while (attrMatch.find()) { 320 321 String value = attrMatch.group(2); 322 if (value == null) { 323 value = attrMatch.group(3); 324 if (value == null) { 325 value = ""; 326 } 327 } 328 329 attributes.put(attrMatch.group(1).toLowerCase(), value); 330 } 331 332 // Infer a default charset encoding if unspecified. 333 if (!attributes.containsKey(ATTR_CHARSET)) { 334 inferredCharset = true; 335 if (subType.endsWith("xml")) { 336 if (type.equals("application")) { 337 // BUGBUG: Actually have need to look at the raw stream here, but 338 // if client omitted the charset for "application/xml", they are 339 // ignoring the STRONGLY RECOMMEND language in RFC 3023, sec 3.2. 340 // I have little sympathy. 341 attributes.put(ATTR_CHARSET, "utf-8"); // best guess 342 } else { 343 attributes.put(ATTR_CHARSET, "us-ascii"); // RFC3023, sec 3.1 344 } 345 } else if (subType.equals("json")) { 346 attributes.put(ATTR_CHARSET, "utf-8"); // RFC4627, sec 3 347 } else { 348 attributes.put(ATTR_CHARSET, "iso-8859-1"); // http default 349 } 350 } 351 } 352 353 /** {code True} if parsed input didn't contain charset encoding info */ 354 private boolean inferredCharset = false; 355 356 private String type; 357 public String getType() { return type; } 358 public void setType(String type) { this.type = type; } 359 360 361 private String subType; 362 public String getSubType() { return subType; } 363 public void setSubType(String subType) { this.subType = subType; } 364 365 /** Returns the full media type */ 366 public String getMediaType() { 367 StringBuilder sb = new StringBuilder(); // MCA instead of StringBuffer 368 sb.append(type); 369 sb.append("/"); 370 sb.append(subType); 371 return sb.toString(); 372 } 373 374 private HashMap<String, String> attributes = new HashMap<String, String>(); 375 376 /** 377 * Returns the additional attributes of the content type. 378 */ 379 public HashMap<String, String> getAttributes() { return attributes; } 380 381 382 /** 383 * Returns the additional attribute by name of the content type. 384 * 385 * @param name attribute name 386 */ 387 public String getAttribute(String name) { 388 return attributes.get(name); 389 } 390 391 /* 392 * Returns the charset attribute of the content type or null if the 393 * attribute has not been set. 394 */ 395 public String getCharset() { return attributes.get(ATTR_CHARSET); } 396 397 398 /** 399 * Returns whether this content type is match by the content type found in the 400 * "Accept" header field of an HTTP request. 401 * 402 * @param acceptedContentType content type found in the "Accept" header field 403 * of an HTTP request 404 */ 405 public boolean match(ContentType acceptedContentType) { 406 String acceptedType = acceptedContentType.getType(); 407 String acceptedSubType = acceptedContentType.getSubType(); 408 return STAR.equals(acceptedType) || type.equals(acceptedType) && 409 (STAR.equals(acceptedSubType) || subType.equals(acceptedSubType)); 410 } 411 412 413 /** 414 * Generates the Content-Type value 415 */ 416 @Override 417 public String toString() { 418 419 StringBuilder sb = new StringBuilder(); // MCA instead of StringBuffer 420 sb.append(type); 421 sb.append("/"); 422 sb.append(subType); 423 for (String name : attributes.keySet()) { 424 425 // Don't include any inferred charset attribute in output. 426 if (inferredCharset && ATTR_CHARSET.equals(name)) { 427 continue; 428 } 429 sb.append(";"); 430 sb.append(name); 431 sb.append("="); 432 String value = attributes.get(name); 433 Matcher tokenMatcher = TOKEN_PATTERN.matcher(value); 434 if (tokenMatcher.matches()) { 435 sb.append(value); 436 } else { 437 sb.append("\"" + value + "\""); 438 } 439 } 440 return sb.toString(); 441 } 442 443 444 @Override 445 public boolean equals(Object o) { 446 if (this == o) { 447 return true; 448 } 449 if (o == null || getClass() != o.getClass()) { 450 return false; 451 } 452 ContentType that = (ContentType) o; 453 return type.equals(that.type) && subType.equals(that.subType) && attributes 454 .equals(that.attributes); 455 } 456 457 458 @Override 459 public int hashCode() { 460 return (type.hashCode() * 31 + subType.hashCode()) * 31 + attributes 461 .hashCode(); 462 } 463}