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