1 /* 2 * Copyright (c) 2000, 2003, 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.print; 27 28 import java.io.Serializable; 29 30 import java.util.AbstractMap; 31 import java.util.AbstractSet; 32 import java.util.Iterator; 33 import java.util.Map; 34 import java.util.NoSuchElementException; 35 import java.util.Set; 36 import java.util.Vector; 37 38 /** 39 * Class MimeType encapsulates a Multipurpose Internet Mail Extensions (MIME) 40 * media type as defined in <A HREF="http://www.ietf.org/rfc/rfc2045.txt">RFC 41 * 2045</A> and <A HREF="http://www.ietf.org/rfc/rfc2046.txt">RFC 2046</A>. A 42 * MIME type object is part of a {@link DocFlavor DocFlavor} object and 43 * specifies the format of the print data. 44 * <P> 45 * Class MimeType is similar to the like-named 46 * class in package {@link java.awt.datatransfer java.awt.datatransfer}. Class 47 * java.awt.datatransfer.MimeType is not used in the Jini Print Service API 48 * for two reasons: 49 * <OL TYPE=1> 50 * <LI> 51 * Since not all Java profiles include the AWT, the Jini Print Service should 52 * not depend on an AWT class. 53 * <P> 54 * <LI> 55 * The implementation of class java.awt.datatransfer.MimeType does not 56 * guarantee 57 * that equivalent MIME types will have the same serialized representation. 58 * Thus, since the Jini Lookup Service (JLUS) matches service attributes based 59 * on equality of serialized representations, JLUS searches involving MIME 60 * types encapsulated in class java.awt.datatransfer.MimeType may incorrectly 61 * fail to match. 62 * </OL> 63 * <P> 64 * Class MimeType's serialized representation is based on the following 65 * canonical form of a MIME type string. Thus, two MIME types that are not 66 * identical but that are equivalent (that have the same canonical form) will 67 * be considered equal by the JLUS's matching algorithm. 68 * <UL> 69 * <LI> The media type, media subtype, and parameters are retained, but all 70 * comments and whitespace characters are discarded. 71 * <LI> The media type, media subtype, and parameter names are converted to 72 * lowercase. 73 * <LI> The parameter values retain their original case, except a charset 74 * parameter value for a text media type is converted to lowercase. 75 * <LI> Quote characters surrounding parameter values are removed. 76 * <LI> Quoting backslash characters inside parameter values are removed. 77 * <LI> The parameters are arranged in ascending order of parameter name. 78 * </UL> 79 * <P> 80 * 81 * @author Alan Kaminsky 82 */ 83 class MimeType implements Serializable, Cloneable { 84 85 private static final long serialVersionUID = -2785720609362367683L; 86 87 /** 88 * Array of strings that hold pieces of this MIME type's canonical form. 89 * If the MIME type has <I>n</I> parameters, <I>n</I> >= 0, then the 90 * strings in the array are: 91 * <BR>Index 0 -- Media type. 92 * <BR>Index 1 -- Media subtype. 93 * <BR>Index 2<I>i</I>+2 -- Name of parameter <I>i</I>, 94 * <I>i</I>=0,1,...,<I>n</I>-1. 95 * <BR>Index 2<I>i</I>+3 -- Value of parameter <I>i</I>, 96 * <I>i</I>=0,1,...,<I>n</I>-1. 97 * <BR>Parameters are arranged in ascending order of parameter name. 98 * @serial 99 */ 100 private String[] myPieces; 101 102 /** 103 * String value for this MIME type. Computed when needed and cached. 104 */ 105 private transient String myStringValue = null; 106 107 /** 108 * Parameter map entry set. Computed when needed and cached. 109 */ 110 private transient ParameterMapEntrySet myEntrySet = null; 111 112 /** 113 * Parameter map. Computed when needed and cached. 114 */ 115 private transient ParameterMap myParameterMap = null; 116 117 /** 118 * Parameter map entry. 119 */ 120 private class ParameterMapEntry implements Map.Entry { 121 private int myIndex; 122 public ParameterMapEntry(int theIndex) { 123 myIndex = theIndex; 124 } 125 public Object getKey(){ 126 return myPieces[myIndex]; 127 } 128 public Object getValue(){ 129 return myPieces[myIndex+1]; 130 } 131 public Object setValue (Object value) { 132 throw new UnsupportedOperationException(); 133 } 134 public boolean equals(Object o) { 135 return (o != null && 136 o instanceof Map.Entry && 137 getKey().equals (((Map.Entry) o).getKey()) && 138 getValue().equals(((Map.Entry) o).getValue())); 139 } 140 public int hashCode() { 141 return getKey().hashCode() ^ getValue().hashCode(); 142 } 143 } 144 145 /** 146 * Parameter map entry set iterator. 147 */ 148 private class ParameterMapEntrySetIterator implements Iterator { 149 private int myIndex = 2; 150 public boolean hasNext() { 151 return myIndex < myPieces.length; 152 } 153 public Object next() { 154 if (hasNext()) { 155 ParameterMapEntry result = new ParameterMapEntry (myIndex); 156 myIndex += 2; 157 return result; 158 } else { 159 throw new NoSuchElementException(); 160 } 161 } 162 public void remove() { 163 throw new UnsupportedOperationException(); 164 } 165 } 166 167 /** 168 * Parameter map entry set. 169 */ 170 private class ParameterMapEntrySet extends AbstractSet { 171 public Iterator iterator() { 172 return new ParameterMapEntrySetIterator(); 173 } 174 public int size() { 175 return (myPieces.length - 2) / 2; 176 } 177 } 178 179 /** 180 * Parameter map. 181 */ 182 private class ParameterMap extends AbstractMap { 183 public Set entrySet() { 184 if (myEntrySet == null) { 185 myEntrySet = new ParameterMapEntrySet(); 186 } 187 return myEntrySet; 188 } 189 } 190 191 /** 192 * Construct a new MIME type object from the given string. The given 193 * string is converted into canonical form and stored internally. 194 * 195 * @param s MIME media type string. 196 * 197 * @exception NullPointerException 198 * (unchecked exception) Thrown if <CODE>s</CODE> is null. 199 * @exception IllegalArgumentException 200 * (unchecked exception) Thrown if <CODE>s</CODE> does not obey the 201 * syntax for a MIME media type string. 202 */ 203 public MimeType(String s) { 204 parse (s); 205 } 206 207 /** 208 * Returns this MIME type object's MIME type string based on the canonical 209 * form. Each parameter value is enclosed in quotes. 210 */ 211 public String getMimeType() { 212 return getStringValue(); 213 } 214 215 /** 216 * Returns this MIME type object's media type. 217 */ 218 public String getMediaType() { 219 return myPieces[0]; 220 } 221 222 /** 223 * Returns this MIME type object's media subtype. 224 */ 225 public String getMediaSubtype() { 226 return myPieces[1]; 227 } 228 229 /** 230 * Returns an unmodifiable map view of the parameters in this MIME type 231 * object. Each entry in the parameter map view consists of a parameter 232 * name String (key) mapping to a parameter value String. If this MIME 233 * type object has no parameters, an empty map is returned. 234 * 235 * @return Parameter map for this MIME type object. 236 */ 237 public Map getParameterMap() { 238 if (myParameterMap == null) { 239 myParameterMap = new ParameterMap(); 240 } 241 return myParameterMap; 242 } 243 244 /** 245 * Converts this MIME type object to a string. 246 * 247 * @return MIME type string based on the canonical form. Each parameter 248 * value is enclosed in quotes. 249 */ 250 public String toString() { 251 return getStringValue(); 252 } 253 254 /** 255 * Returns a hash code for this MIME type object. 256 */ 257 public int hashCode() { 258 return getStringValue().hashCode(); 259 } 260 261 /** 262 * Determine if this MIME type object is equal to the given object. The two 263 * are equal if the given object is not null, is an instance of class 264 * net.jini.print.data.MimeType, and has the same canonical form as this 265 * MIME type object (that is, has the same type, subtype, and parameters). 266 * Thus, if two MIME type objects are the same except for comments, they are 267 * considered equal. However, "text/plain" and "text/plain; 268 * charset=us-ascii" are not considered equal, even though they represent 269 * the same media type (because the default character set for plain text is 270 * US-ASCII). 271 * 272 * @param obj Object to test. 273 * 274 * @return True if this MIME type object equals <CODE>obj</CODE>, false 275 * otherwise. 276 */ 277 public boolean equals (Object obj) { 278 return(obj != null && 279 obj instanceof MimeType && 280 getStringValue().equals(((MimeType) obj).getStringValue())); 281 } 282 283 /** 284 * Returns this MIME type's string value in canonical form. 285 */ 286 private String getStringValue() { 287 if (myStringValue == null) { 288 StringBuffer result = new StringBuffer(); 289 result.append (myPieces[0]); 290 result.append ('/'); 291 result.append (myPieces[1]); 292 int n = myPieces.length; 293 for (int i = 2; i < n; i += 2) { 294 result.append(';'); 295 result.append(' '); 296 result.append(myPieces[i]); 297 result.append('='); 298 result.append(addQuotes (myPieces[i+1])); 299 } 300 myStringValue = result.toString(); 301 } 302 return myStringValue; 303 } 304 305 // Hidden classes, constants, and operations for parsing a MIME media type 306 // string. 307 308 // Lexeme types. 309 private static final int TOKEN_LEXEME = 0; 310 private static final int QUOTED_STRING_LEXEME = 1; 311 private static final int TSPECIAL_LEXEME = 2; 312 private static final int EOF_LEXEME = 3; 313 private static final int ILLEGAL_LEXEME = 4; 314 315 // Class for a lexical analyzer. 316 private static class LexicalAnalyzer { 317 protected String mySource; 318 protected int mySourceLength; 319 protected int myCurrentIndex; 320 protected int myLexemeType; 321 protected int myLexemeBeginIndex; 322 protected int myLexemeEndIndex; 323 324 public LexicalAnalyzer(String theSource) { 325 mySource = theSource; 326 mySourceLength = theSource.length(); 327 myCurrentIndex = 0; 328 nextLexeme(); 329 } 330 331 public int getLexemeType() { 332 return myLexemeType; 333 } 334 335 public String getLexeme() { 336 return(myLexemeBeginIndex >= mySourceLength ? 337 null : 338 mySource.substring(myLexemeBeginIndex, myLexemeEndIndex)); 339 } 340 341 public char getLexemeFirstCharacter() { 342 return(myLexemeBeginIndex >= mySourceLength ? 343 '\u0000' : 344 mySource.charAt(myLexemeBeginIndex)); 345 } 346 347 public void nextLexeme() { 348 int state = 0; 349 int commentLevel = 0; 350 char c; 351 while (state >= 0) { 352 switch (state) { 353 // Looking for a token, quoted string, or tspecial 354 case 0: 355 if (myCurrentIndex >= mySourceLength) { 356 myLexemeType = EOF_LEXEME; 357 myLexemeBeginIndex = mySourceLength; 358 myLexemeEndIndex = mySourceLength; 359 state = -1; 360 } else if (Character.isWhitespace 361 (c = mySource.charAt (myCurrentIndex ++))) { 362 state = 0; 363 } else if (c == '\"') { 364 myLexemeType = QUOTED_STRING_LEXEME; 365 myLexemeBeginIndex = myCurrentIndex; 366 state = 1; 367 } else if (c == '(') { 368 ++ commentLevel; 369 state = 3; 370 } else if (c == '/' || c == ';' || c == '=' || 371 c == ')' || c == '<' || c == '>' || 372 c == '@' || c == ',' || c == ':' || 373 c == '\\' || c == '[' || c == ']' || 374 c == '?') { 375 myLexemeType = TSPECIAL_LEXEME; 376 myLexemeBeginIndex = myCurrentIndex - 1; 377 myLexemeEndIndex = myCurrentIndex; 378 state = -1; 379 } else { 380 myLexemeType = TOKEN_LEXEME; 381 myLexemeBeginIndex = myCurrentIndex - 1; 382 state = 5; 383 } 384 break; 385 // In a quoted string 386 case 1: 387 if (myCurrentIndex >= mySourceLength) { 388 myLexemeType = ILLEGAL_LEXEME; 389 myLexemeBeginIndex = mySourceLength; 390 myLexemeEndIndex = mySourceLength; 391 state = -1; 392 } else if ((c = mySource.charAt (myCurrentIndex ++)) == '\"') { 393 myLexemeEndIndex = myCurrentIndex - 1; 394 state = -1; 395 } else if (c == '\\') { 396 state = 2; 397 } else { 398 state = 1; 399 } 400 break; 401 // In a quoted string, backslash seen 402 case 2: 403 if (myCurrentIndex >= mySourceLength) { 404 myLexemeType = ILLEGAL_LEXEME; 405 myLexemeBeginIndex = mySourceLength; 406 myLexemeEndIndex = mySourceLength; 407 state = -1; 408 } else { 409 ++ myCurrentIndex; 410 state = 1; 411 } break; 412 // In a comment 413 case 3: if (myCurrentIndex >= mySourceLength) { 414 myLexemeType = ILLEGAL_LEXEME; 415 myLexemeBeginIndex = mySourceLength; 416 myLexemeEndIndex = mySourceLength; 417 state = -1; 418 } else if ((c = mySource.charAt (myCurrentIndex ++)) == '(') { 419 ++ commentLevel; 420 state = 3; 421 } else if (c == ')') { 422 -- commentLevel; 423 state = commentLevel == 0 ? 0 : 3; 424 } else if (c == '\\') { 425 state = 4; 426 } else { state = 3; 427 } 428 break; 429 // In a comment, backslash seen 430 case 4: 431 if (myCurrentIndex >= mySourceLength) { 432 myLexemeType = ILLEGAL_LEXEME; 433 myLexemeBeginIndex = mySourceLength; 434 myLexemeEndIndex = mySourceLength; 435 state = -1; 436 } else { 437 ++ myCurrentIndex; 438 state = 3; 439 } 440 break; 441 // In a token 442 case 5: 443 if (myCurrentIndex >= mySourceLength) { 444 myLexemeEndIndex = myCurrentIndex; 445 state = -1; 446 } else if (Character.isWhitespace 447 (c = mySource.charAt (myCurrentIndex ++))) { 448 myLexemeEndIndex = myCurrentIndex - 1; 449 state = -1; 450 } else if (c == '\"' || c == '(' || c == '/' || 451 c == ';' || c == '=' || c == ')' || 452 c == '<' || c == '>' || c == '@' || 453 c == ',' || c == ':' || c == '\\' || 454 c == '[' || c == ']' || c == '?') { 455 -- myCurrentIndex; 456 myLexemeEndIndex = myCurrentIndex; 457 state = -1; 458 } else { 459 state = 5; 460 } 461 break; 462 } 463 } 464 465 } 466 467 } 468 469 /** 470 * Returns a lowercase version of the given string. The lowercase version 471 * is constructed by applying Character.toLowerCase() to each character of 472 * the given string, which maps characters to lowercase using the rules of 473 * Unicode. This mapping is the same regardless of locale, whereas the 474 * mapping of String.toLowerCase() may be different depending on the 475 * default locale. 476 */ 477 private static String toUnicodeLowerCase(String s) { 478 int n = s.length(); 479 char[] result = new char [n]; 480 for (int i = 0; i < n; ++ i) { 481 result[i] = Character.toLowerCase (s.charAt (i)); 482 } 483 return new String (result); 484 } 485 486 /** 487 * Returns a version of the given string with backslashes removed. 488 */ 489 private static String removeBackslashes(String s) { 490 int n = s.length(); 491 char[] result = new char [n]; 492 int i; 493 int j = 0; 494 char c; 495 for (i = 0; i < n; ++ i) { 496 c = s.charAt (i); 497 if (c == '\\') { 498 c = s.charAt (++ i); 499 } 500 result[j++] = c; 501 } 502 return new String (result, 0, j); 503 } 504 505 /** 506 * Returns a version of the string surrounded by quotes and with interior 507 * quotes preceded by a backslash. 508 */ 509 private static String addQuotes(String s) { 510 int n = s.length(); 511 int i; 512 char c; 513 StringBuffer result = new StringBuffer (n+2); 514 result.append ('\"'); 515 for (i = 0; i < n; ++ i) { 516 c = s.charAt (i); 517 if (c == '\"') { 518 result.append ('\\'); 519 } 520 result.append (c); 521 } 522 result.append ('\"'); 523 return result.toString(); 524 } 525 526 /** 527 * Parses the given string into canonical pieces and stores the pieces in 528 * {@link #myPieces <CODE>myPieces</CODE>}. 529 * <P> 530 * Special rules applied: 531 * <UL> 532 * <LI> If the media type is text, the value of a charset parameter is 533 * converted to lowercase. 534 * </UL> 535 * 536 * @param s MIME media type string. 537 * 538 * @exception NullPointerException 539 * (unchecked exception) Thrown if <CODE>s</CODE> is null. 540 * @exception IllegalArgumentException 541 * (unchecked exception) Thrown if <CODE>s</CODE> does not obey the 542 * syntax for a MIME media type string. 543 */ 544 private void parse(String s) { 545 // Initialize. 546 if (s == null) { 547 throw new NullPointerException(); 548 } 549 LexicalAnalyzer theLexer = new LexicalAnalyzer (s); 550 int theLexemeType; 551 Vector thePieces = new Vector(); 552 boolean mediaTypeIsText = false; 553 boolean parameterNameIsCharset = false; 554 555 // Parse media type. 556 if (theLexer.getLexemeType() == TOKEN_LEXEME) { 557 String mt = toUnicodeLowerCase (theLexer.getLexeme()); 558 thePieces.add (mt); 559 theLexer.nextLexeme(); 560 mediaTypeIsText = mt.equals ("text"); 561 } else { 562 throw new IllegalArgumentException(); 563 } 564 // Parse slash. 565 if (theLexer.getLexemeType() == TSPECIAL_LEXEME && 566 theLexer.getLexemeFirstCharacter() == '/') { 567 theLexer.nextLexeme(); 568 } else { 569 throw new IllegalArgumentException(); 570 } 571 if (theLexer.getLexemeType() == TOKEN_LEXEME) { 572 thePieces.add (toUnicodeLowerCase (theLexer.getLexeme())); 573 theLexer.nextLexeme(); 574 } else { 575 throw new IllegalArgumentException(); 576 } 577 // Parse zero or more parameters. 578 while (theLexer.getLexemeType() == TSPECIAL_LEXEME && 579 theLexer.getLexemeFirstCharacter() == ';') { 580 // Parse semicolon. 581 theLexer.nextLexeme(); 582 583 // Parse parameter name. 584 if (theLexer.getLexemeType() == TOKEN_LEXEME) { 585 String pn = toUnicodeLowerCase (theLexer.getLexeme()); 586 thePieces.add (pn); 587 theLexer.nextLexeme(); 588 parameterNameIsCharset = pn.equals ("charset"); 589 } else { 590 throw new IllegalArgumentException(); 591 } 592 593 // Parse equals. 594 if (theLexer.getLexemeType() == TSPECIAL_LEXEME && 595 theLexer.getLexemeFirstCharacter() == '=') { 596 theLexer.nextLexeme(); 597 } else { 598 throw new IllegalArgumentException(); 599 } 600 601 // Parse parameter value. 602 if (theLexer.getLexemeType() == TOKEN_LEXEME) { 603 String pv = theLexer.getLexeme(); 604 thePieces.add(mediaTypeIsText && parameterNameIsCharset ? 605 toUnicodeLowerCase (pv) : 606 pv); 607 theLexer.nextLexeme(); 608 } else if (theLexer.getLexemeType() == QUOTED_STRING_LEXEME) { 609 String pv = removeBackslashes (theLexer.getLexeme()); 610 thePieces.add(mediaTypeIsText && parameterNameIsCharset ? 611 toUnicodeLowerCase (pv) : 612 pv); 613 theLexer.nextLexeme(); 614 } else { 615 throw new IllegalArgumentException(); 616 } 617 } 618 619 // Make sure we've consumed everything. 620 if (theLexer.getLexemeType() != EOF_LEXEME) { 621 throw new IllegalArgumentException(); 622 } 623 624 // Save the pieces. Parameters are not in ascending order yet. 625 int n = thePieces.size(); 626 myPieces = (String[]) thePieces.toArray (new String [n]); 627 628 // Sort the parameters into ascending order using an insertion sort. 629 int i, j; 630 String temp; 631 for (i = 4; i < n; i += 2) { 632 j = 2; 633 while (j < i && myPieces[j].compareTo (myPieces[i]) <= 0) { 634 j += 2; 635 } 636 while (j < i) { 637 temp = myPieces[j]; 638 myPieces[j] = myPieces[i]; 639 myPieces[i] = temp; 640 temp = myPieces[j+1]; 641 myPieces[j+1] = myPieces[i+1]; 642 myPieces[i+1] = temp; 643 j += 2; 644 } 645 } 646 } 647 }