/* * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.print; import java.io.Serializable; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.Vector; /** * Class {@code MimeType} encapsulates a Multipurpose Internet Mail Extensions * (MIME) media type as defined in * RFC 2045 and * RFC 2046. A MIME type * object is part of a {@link DocFlavor DocFlavor} object and specifies the * format of the print data. *

* Class {@code MimeType} is similar to the like-named class in package * {@link java.awt.datatransfer java.awt.datatransfer}. Class * {@link java.awt.datatransfer.MimeType} is not used in the Jini Print Service * API for two reasons: *

    *
  1. Since not all Java profiles include the AWT, the Jini Print Service * should not depend on an AWT class. *
  2. The implementation of class {@code java.awt.datatransfer.MimeType} does * not guarantee that equivalent MIME types will have the same serialized * representation. Thus, since the Jini Lookup Service (JLUS) matches service * attributes based on equality of serialized representations, JLUS searches * involving MIME types encapsulated in class * {@code java.awt.datatransfer.MimeType} may incorrectly fail to match. *
* Class MimeType's serialized representation is based on the following * canonical form of a MIME type string. Thus, two MIME types that are not * identical but that are equivalent (that have the same canonical form) will be * considered equal by the JLUS's matching algorithm. * * * @author Alan Kaminsky */ class MimeType implements Serializable, Cloneable { /** * Use serialVersionUID from JDK 1.4 for interoperability. */ private static final long serialVersionUID = -2785720609362367683L; /** * Array of strings that hold pieces of this MIME type's canonical form. If * the MIME type has n parameters, n >= 0, then the * strings in the array are: *
Index 0 -- Media type. *
Index 1 -- Media subtype. *
Index 2i+2 -- Name of parameter i, * i=0,1,...,n-1. *
Index 2i+3 -- Value of parameter i, * i=0,1,...,n-1. *
Parameters are arranged in ascending order of parameter name. * @serial */ private String[] myPieces; /** * String value for this MIME type. Computed when needed and cached. */ private transient String myStringValue = null; /** * Parameter map entry set. Computed when needed and cached. */ private transient ParameterMapEntrySet myEntrySet = null; /** * Parameter map. Computed when needed and cached. */ private transient ParameterMap myParameterMap = null; /** * Parameter map entry. */ private class ParameterMapEntry implements Map.Entry { /** * The index of the entry. */ private int myIndex; /** * Constructs a new parameter map entry. * * @param theIndex the index of the entry */ public ParameterMapEntry(int theIndex) { myIndex = theIndex; } public String getKey(){ return myPieces[myIndex]; } public String getValue(){ return myPieces[myIndex+1]; } public String setValue (String value) { throw new UnsupportedOperationException(); } public boolean equals(Object o) { return (o != null && o instanceof Map.Entry && getKey().equals (((Map.Entry) o).getKey()) && getValue().equals(((Map.Entry) o).getValue())); } public int hashCode() { return getKey().hashCode() ^ getValue().hashCode(); } } /** * Parameter map entry set iterator. */ private class ParameterMapEntrySetIterator implements Iterator> { /** * The current index of the iterator. */ private int myIndex = 2; public boolean hasNext() { return myIndex < myPieces.length; } public Map.Entry next() { if (hasNext()) { ParameterMapEntry result = new ParameterMapEntry (myIndex); myIndex += 2; return result; } else { throw new NoSuchElementException(); } } public void remove() { throw new UnsupportedOperationException(); } } /** * Parameter map entry set. */ private class ParameterMapEntrySet extends AbstractSet> { public Iterator> iterator() { return new ParameterMapEntrySetIterator(); } public int size() { return (myPieces.length - 2) / 2; } } /** * Parameter map. */ private class ParameterMap extends AbstractMap { public Set> entrySet() { if (myEntrySet == null) { myEntrySet = new ParameterMapEntrySet(); } return myEntrySet; } } /** * Construct a new MIME type object from the given string. The given string * is converted into canonical form and stored internally. * * @param s MIME media type string * @throws NullPointerException if {@code s} is {@code null} * @throws IllegalArgumentException if {@code s} does not obey the syntax * for a MIME media type string */ public MimeType(String s) { parse (s); } /** * Returns this MIME type object's MIME type string based on the canonical * form. Each parameter value is enclosed in quotes. * * @return the mime type */ public String getMimeType() { return getStringValue(); } /** * Returns this MIME type object's media type. * * @return the media type */ public String getMediaType() { return myPieces[0]; } /** * Returns this MIME type object's media subtype. * * @return the media subtype */ public String getMediaSubtype() { return myPieces[1]; } /** * Returns an unmodifiable map view of the parameters in this MIME type * object. Each entry in the parameter map view consists of a parameter name * {@code String} (key) mapping to a parameter value {@code String}. If this * MIME type object has no parameters, an empty map is returned. * * @return parameter map for this MIME type object */ public Map getParameterMap() { if (myParameterMap == null) { myParameterMap = new ParameterMap(); } return myParameterMap; } /** * Converts this MIME type object to a string. * * @return MIME type string based on the canonical form. Each parameter * value is enclosed in quotes. */ public String toString() { return getStringValue(); } /** * Returns a hash code for this MIME type object. */ public int hashCode() { return getStringValue().hashCode(); } /** * Determine if this MIME type object is equal to the given object. The two * are equal if the given object is not {@code null}, is an instance of * class {@code javax.print.data.MimeType}, and has the same canonical form * as this MIME type object (that is, has the same type, subtype, and * parameters). Thus, if two MIME type objects are the same except for * comments, they are considered equal. However, "text/plain" and * "text/plain; charset=us-ascii" are not considered equal, even though they * represent the same media type (because the default character set for * plain text is US-ASCII). * * @param obj {@code object} to test * @return {@code true} if this MIME type object equals {@code obj}, * {@code false} otherwise */ public boolean equals (Object obj) { return(obj != null && obj instanceof MimeType && getStringValue().equals(((MimeType) obj).getStringValue())); } /** * Returns this MIME type's string value in canonical form. * * @return the MIME type's string value in canonical form */ private String getStringValue() { if (myStringValue == null) { StringBuilder result = new StringBuilder(); result.append (myPieces[0]); result.append ('/'); result.append (myPieces[1]); int n = myPieces.length; for (int i = 2; i < n; i += 2) { result.append(';'); result.append(' '); result.append(myPieces[i]); result.append('='); result.append(addQuotes (myPieces[i+1])); } myStringValue = result.toString(); } return myStringValue; } // Hidden classes, constants, and operations for parsing a MIME media type // string. // Lexeme types. private static final int TOKEN_LEXEME = 0; private static final int QUOTED_STRING_LEXEME = 1; private static final int TSPECIAL_LEXEME = 2; private static final int EOF_LEXEME = 3; private static final int ILLEGAL_LEXEME = 4; /** *Class for a lexical analyzer. */ private static class LexicalAnalyzer { protected String mySource; protected int mySourceLength; protected int myCurrentIndex; protected int myLexemeType; protected int myLexemeBeginIndex; protected int myLexemeEndIndex; public LexicalAnalyzer(String theSource) { mySource = theSource; mySourceLength = theSource.length(); myCurrentIndex = 0; nextLexeme(); } public int getLexemeType() { return myLexemeType; } public String getLexeme() { return(myLexemeBeginIndex >= mySourceLength ? null : mySource.substring(myLexemeBeginIndex, myLexemeEndIndex)); } public char getLexemeFirstCharacter() { return(myLexemeBeginIndex >= mySourceLength ? '\u0000' : mySource.charAt(myLexemeBeginIndex)); } public void nextLexeme() { int state = 0; int commentLevel = 0; char c; while (state >= 0) { switch (state) { // Looking for a token, quoted string, or tspecial case 0: if (myCurrentIndex >= mySourceLength) { myLexemeType = EOF_LEXEME; myLexemeBeginIndex = mySourceLength; myLexemeEndIndex = mySourceLength; state = -1; } else if (Character.isWhitespace (c = mySource.charAt (myCurrentIndex ++))) { state = 0; } else if (c == '\"') { myLexemeType = QUOTED_STRING_LEXEME; myLexemeBeginIndex = myCurrentIndex; state = 1; } else if (c == '(') { ++ commentLevel; state = 3; } else if (c == '/' || c == ';' || c == '=' || c == ')' || c == '<' || c == '>' || c == '@' || c == ',' || c == ':' || c == '\\' || c == '[' || c == ']' || c == '?') { myLexemeType = TSPECIAL_LEXEME; myLexemeBeginIndex = myCurrentIndex - 1; myLexemeEndIndex = myCurrentIndex; state = -1; } else { myLexemeType = TOKEN_LEXEME; myLexemeBeginIndex = myCurrentIndex - 1; state = 5; } break; // In a quoted string case 1: if (myCurrentIndex >= mySourceLength) { myLexemeType = ILLEGAL_LEXEME; myLexemeBeginIndex = mySourceLength; myLexemeEndIndex = mySourceLength; state = -1; } else if ((c = mySource.charAt (myCurrentIndex ++)) == '\"') { myLexemeEndIndex = myCurrentIndex - 1; state = -1; } else if (c == '\\') { state = 2; } else { state = 1; } break; // In a quoted string, backslash seen case 2: if (myCurrentIndex >= mySourceLength) { myLexemeType = ILLEGAL_LEXEME; myLexemeBeginIndex = mySourceLength; myLexemeEndIndex = mySourceLength; state = -1; } else { ++ myCurrentIndex; state = 1; } break; // In a comment case 3: if (myCurrentIndex >= mySourceLength) { myLexemeType = ILLEGAL_LEXEME; myLexemeBeginIndex = mySourceLength; myLexemeEndIndex = mySourceLength; state = -1; } else if ((c = mySource.charAt (myCurrentIndex ++)) == '(') { ++ commentLevel; state = 3; } else if (c == ')') { -- commentLevel; state = commentLevel == 0 ? 0 : 3; } else if (c == '\\') { state = 4; } else { state = 3; } break; // In a comment, backslash seen case 4: if (myCurrentIndex >= mySourceLength) { myLexemeType = ILLEGAL_LEXEME; myLexemeBeginIndex = mySourceLength; myLexemeEndIndex = mySourceLength; state = -1; } else { ++ myCurrentIndex; state = 3; } break; // In a token case 5: if (myCurrentIndex >= mySourceLength) { myLexemeEndIndex = myCurrentIndex; state = -1; } else if (Character.isWhitespace (c = mySource.charAt (myCurrentIndex ++))) { myLexemeEndIndex = myCurrentIndex - 1; state = -1; } else if (c == '\"' || c == '(' || c == '/' || c == ';' || c == '=' || c == ')' || c == '<' || c == '>' || c == '@' || c == ',' || c == ':' || c == '\\' || c == '[' || c == ']' || c == '?') { -- myCurrentIndex; myLexemeEndIndex = myCurrentIndex; state = -1; } else { state = 5; } break; } } } } /** * Returns a lowercase version of the given string. The lowercase version is * constructed by applying {@code Character.toLowerCase()} to each character * of the given string, which maps characters to lowercase using the rules * of Unicode. This mapping is the same regardless of locale, whereas the * mapping of {@code String.toLowerCase()} may be different depending on the * default locale. * * @param s the string * @return the lowercase version of the string */ private static String toUnicodeLowerCase(String s) { int n = s.length(); char[] result = new char [n]; for (int i = 0; i < n; ++ i) { result[i] = Character.toLowerCase (s.charAt (i)); } return new String (result); } /** * Returns a version of the given string with backslashes removed. * * @param s the string * @return the string with backslashes removed */ private static String removeBackslashes(String s) { int n = s.length(); char[] result = new char [n]; int i; int j = 0; char c; for (i = 0; i < n; ++ i) { c = s.charAt (i); if (c == '\\') { c = s.charAt (++ i); } result[j++] = c; } return new String (result, 0, j); } /** * Returns a version of the string surrounded by quotes and with interior * quotes preceded by a backslash. * * @param s the string * @return the string surrounded by quotes and with interior quotes preceded * by a backslash */ private static String addQuotes(String s) { int n = s.length(); int i; char c; StringBuilder result = new StringBuilder (n+2); result.append ('\"'); for (i = 0; i < n; ++ i) { c = s.charAt (i); if (c == '\"') { result.append ('\\'); } result.append (c); } result.append ('\"'); return result.toString(); } /** * Parses the given string into canonical pieces and stores the pieces in * {@link #myPieces myPieces}. *

* Special rules applied: *

    *
  • If the media type is text, the value of a charset parameter is * converted to lowercase. *
* * @param s MIME media type string * @throws NullPointerException if {@code s} is {@code null} * @throws IllegalArgumentException if {@code s} does not obey the syntax * for a MIME media type string */ private void parse(String s) { // Initialize. if (s == null) { throw new NullPointerException(); } LexicalAnalyzer theLexer = new LexicalAnalyzer (s); int theLexemeType; Vector thePieces = new Vector<>(); boolean mediaTypeIsText = false; boolean parameterNameIsCharset = false; // Parse media type. if (theLexer.getLexemeType() == TOKEN_LEXEME) { String mt = toUnicodeLowerCase (theLexer.getLexeme()); thePieces.add (mt); theLexer.nextLexeme(); mediaTypeIsText = mt.equals ("text"); } else { throw new IllegalArgumentException(); } // Parse slash. if (theLexer.getLexemeType() == TSPECIAL_LEXEME && theLexer.getLexemeFirstCharacter() == '/') { theLexer.nextLexeme(); } else { throw new IllegalArgumentException(); } if (theLexer.getLexemeType() == TOKEN_LEXEME) { thePieces.add (toUnicodeLowerCase (theLexer.getLexeme())); theLexer.nextLexeme(); } else { throw new IllegalArgumentException(); } // Parse zero or more parameters. while (theLexer.getLexemeType() == TSPECIAL_LEXEME && theLexer.getLexemeFirstCharacter() == ';') { // Parse semicolon. theLexer.nextLexeme(); // Parse parameter name. if (theLexer.getLexemeType() == TOKEN_LEXEME) { String pn = toUnicodeLowerCase (theLexer.getLexeme()); thePieces.add (pn); theLexer.nextLexeme(); parameterNameIsCharset = pn.equals ("charset"); } else { throw new IllegalArgumentException(); } // Parse equals. if (theLexer.getLexemeType() == TSPECIAL_LEXEME && theLexer.getLexemeFirstCharacter() == '=') { theLexer.nextLexeme(); } else { throw new IllegalArgumentException(); } // Parse parameter value. if (theLexer.getLexemeType() == TOKEN_LEXEME) { String pv = theLexer.getLexeme(); thePieces.add(mediaTypeIsText && parameterNameIsCharset ? toUnicodeLowerCase (pv) : pv); theLexer.nextLexeme(); } else if (theLexer.getLexemeType() == QUOTED_STRING_LEXEME) { String pv = removeBackslashes (theLexer.getLexeme()); thePieces.add(mediaTypeIsText && parameterNameIsCharset ? toUnicodeLowerCase (pv) : pv); theLexer.nextLexeme(); } else { throw new IllegalArgumentException(); } } // Make sure we've consumed everything. if (theLexer.getLexemeType() != EOF_LEXEME) { throw new IllegalArgumentException(); } // Save the pieces. Parameters are not in ascending order yet. int n = thePieces.size(); myPieces = thePieces.toArray (new String [n]); // Sort the parameters into ascending order using an insertion sort. int i, j; String temp; for (i = 4; i < n; i += 2) { j = 2; while (j < i && myPieces[j].compareTo (myPieces[i]) <= 0) { j += 2; } while (j < i) { temp = myPieces[j]; myPieces[j] = myPieces[i]; myPieces[i] = temp; temp = myPieces[j+1]; myPieces[j+1] = myPieces[i+1]; myPieces[i+1] = temp; j += 2; } } } }