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