/* * Copyright 2000-2003 Sun Microsystems, Inc. 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. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.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 MimeType encapsulates a Multipurpose Internet Mail Extensions (MIME) * media type as defined in <A HREF="http://www.ietf.org/rfc/rfc2045.txt">RFC * 2045</A> and <A HREF="http://www.ietf.org/rfc/rfc2046.txt">RFC 2046</A>. A * MIME type object is part of a {@link DocFlavor DocFlavor} object and * specifies the format of the print data. * <P> * Class MimeType is similar to the like-named * class in package {@link java.awt.datatransfer java.awt.datatransfer}. Class * java.awt.datatransfer.MimeType is not used in the Jini Print Service API * for two reasons: * <OL TYPE=1> * <LI> * Since not all Java profiles include the AWT, the Jini Print Service should * not depend on an AWT class. * <P> * <LI> * The implementation of class 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 java.awt.datatransfer.MimeType may incorrectly * fail to match. * </OL> * <P> * 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. * <UL> * <LI> The media type, media subtype, and parameters are retained, but all * comments and whitespace characters are discarded. * <LI> The media type, media subtype, and parameter names are converted to * lowercase. * <LI> The parameter values retain their original case, except a charset * parameter value for a text media type is converted to lowercase. * <LI> Quote characters surrounding parameter values are removed. * <LI> Quoting backslash characters inside parameter values are removed. * <LI> The parameters are arranged in ascending order of parameter name. * </UL> * <P> * * @author Alan Kaminsky */ class MimeType implements Serializable, Cloneable { private static final long serialVersionUID = -2785720609362367683L; /** * Array of strings that hold pieces of this MIME type's canonical form. * If the MIME type has <I>n</I> parameters, <I>n</I> >= 0, then the * strings in the array are: * <BR>Index 0 -- Media type. * <BR>Index 1 -- Media subtype. * <BR>Index 2<I>i</I>+2 -- Name of parameter <I>i</I>, * <I>i</I>=0,1,...,<I>n</I>-1. * <BR>Index 2<I>i</I>+3 -- Value of parameter <I>i</I>, * <I>i</I>=0,1,...,<I>n</I>-1. * <BR>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 { private int myIndex; public ParameterMapEntry(int theIndex) { myIndex = theIndex; } public Object getKey(){ return myPieces[myIndex]; } public Object getValue(){ return myPieces[myIndex+1]; } public Object setValue (Object 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 { private int myIndex = 2; public boolean hasNext() { return myIndex < myPieces.length; } public Object 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. * * @exception NullPointerException * (unchecked exception) Thrown if <CODE>s</CODE> is null. * @exception IllegalArgumentException * (unchecked exception) Thrown if <CODE>s</CODE> 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. */ public String getMimeType() { return getStringValue(); } /** * Returns this MIME type object's media type. */ public String getMediaType() { return myPieces[0]; } /** * Returns this MIME type object's 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 String (key) mapping to a parameter value 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 null, is an instance of class * net.jini.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 Object to test. * * @return 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. */ private String getStringValue() { if (myStringValue == null) { StringBuffer result = new StringBuffer(); 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 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 String.toLowerCase() may be different depending on the * default locale. */ 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. */ 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. */ private static String addQuotes(String s) { int n = s.length(); int i; char c; StringBuffer result = new StringBuffer (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 <CODE>myPieces</CODE>}. * <P> * Special rules applied: * <UL> * <LI> If the media type is text, the value of a charset parameter is * converted to lowercase. * </UL> * * @param s MIME media type string. * * @exception NullPointerException * (unchecked exception) Thrown if <CODE>s</CODE> is null. * @exception IllegalArgumentException * (unchecked exception) Thrown if <CODE>s</CODE> 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 = (String[]) 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; } } } }