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          */
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