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> &gt;= 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          */
 129         public ParameterMapEntry(int theIndex) {
 130             myIndex = theIndex;
 131         }
 132         public String getKey(){
 133             return myPieces[myIndex];
 134         }
 135         public String getValue(){
 136             return myPieces[myIndex+1];
 137         }
 138         public String setValue (String value) {
 139             throw new UnsupportedOperationException();
 140         }
 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         }
 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;
 161         public boolean hasNext() {
 162             return myIndex < myPieces.length;
 163         }
 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         }
 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>> {
 182         public Iterator<Map.Entry<String, String>> iterator() {
 183             return new ParameterMapEntrySetIterator();
 184         }
 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> {
 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      */
 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      */
 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      */
 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      */
 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      */
 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      */
 264     public String toString() {
 265         return getStringValue();
 266     }
 267 
 268     /**
 269      * Returns a hash code for this MIME type object.
 270      */
 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      */
 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      */
 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 
 341         public LexicalAnalyzer(String theSource) {
 342             mySource = theSource;
 343             mySourceLength = theSource.length();
 344             myCurrentIndex = 0;
 345             nextLexeme();
 346         }
 347 
 348         public int getLexemeType() {
 349             return myLexemeType;
 350         }
 351 
 352         public String getLexeme() {
 353             return(myLexemeBeginIndex >= mySourceLength ?
 354                    null :
 355                    mySource.substring(myLexemeBeginIndex, myLexemeEndIndex));
 356         }
 357 
 358         public char getLexemeFirstCharacter() {
 359             return(myLexemeBeginIndex >= mySourceLength ?
 360                    '\u0000' :
 361                    mySource.charAt(myLexemeBeginIndex));
 362         }
 363 
 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      */
 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      */
 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      */
 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      */
 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 }