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> &gt;= 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 }