1. /*
  2. * @(#)MimeType.java 1.6 03/12/19
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package javax.print;
  8. import java.io.Serializable;
  9. import java.util.AbstractMap;
  10. import java.util.AbstractSet;
  11. import java.util.Iterator;
  12. import java.util.Map;
  13. import java.util.NoSuchElementException;
  14. import java.util.Set;
  15. import java.util.Vector;
  16. /**
  17. * Class MimeType encapsulates a Multipurpose Internet Mail Extensions (MIME)
  18. * media type as defined in <A HREF="http://www.ietf.org/rfc/rfc2045.txt">RFC
  19. * 2045</A> and <A HREF="http://www.ietf.org/rfc/rfc2046.txt">RFC 2046</A>. A
  20. * MIME type object is part of a {@link DocFlavor DocFlavor} object and
  21. * specifies the format of the print data.
  22. * <P>
  23. * Class MimeType is similar to the like-named
  24. * class in package {@link java.awt.datatransfer java.awt.datatransfer}. Class
  25. * java.awt.datatransfer.MimeType is not used in the Jini Print Service API
  26. * for two reasons:
  27. * <OL TYPE=1>
  28. * <LI>
  29. * Since not all Java profiles include the AWT, the Jini Print Service should
  30. * not depend on an AWT class.
  31. * <P>
  32. * <LI>
  33. * The implementation of class java.awt.datatransfer.MimeType does not
  34. * guarantee
  35. * that equivalent MIME types will have the same serialized representation.
  36. * Thus, since the Jini Lookup Service (JLUS) matches service attributes based
  37. * on equality of serialized representations, JLUS searches involving MIME
  38. * types encapsulated in class java.awt.datatransfer.MimeType may incorrectly
  39. * fail to match.
  40. * </OL>
  41. * <P>
  42. * Class MimeType's serialized representation is based on the following
  43. * canonical form of a MIME type string. Thus, two MIME types that are not
  44. * identical but that are equivalent (that have the same canonical form) will
  45. * be considered equal by the JLUS's matching algorithm.
  46. * <UL>
  47. * <LI> The media type, media subtype, and parameters are retained, but all
  48. * comments and whitespace characters are discarded.
  49. * <LI> The media type, media subtype, and parameter names are converted to
  50. * lowercase.
  51. * <LI> The parameter values retain their original case, except a charset
  52. * parameter value for a text media type is converted to lowercase.
  53. * <LI> Quote characters surrounding parameter values are removed.
  54. * <LI> Quoting backslash characters inside parameter values are removed.
  55. * <LI> The parameters are arranged in ascending order of parameter name.
  56. * </UL>
  57. * <P>
  58. *
  59. * @author Alan Kaminsky
  60. */
  61. class MimeType implements Serializable, Cloneable {
  62. private static final long serialVersionUID = -2785720609362367683L;
  63. /**
  64. * Array of strings that hold pieces of this MIME type's canonical form.
  65. * If the MIME type has <I>n</I> parameters, <I>n</I> >= 0, then the
  66. * strings in the array are:
  67. * <BR>Index 0 -- Media type.
  68. * <BR>Index 1 -- Media subtype.
  69. * <BR>Index 2<I>i</I>+2 -- Name of parameter <I>i</I>,
  70. * <I>i</I>=0,1,...,<I>n</I>-1.
  71. * <BR>Index 2<I>i</I>+3 -- Value of parameter <I>i</I>,
  72. * <I>i</I>=0,1,...,<I>n</I>-1.
  73. * <BR>Parameters are arranged in ascending order of parameter name.
  74. * @serial
  75. */
  76. private String[] myPieces;
  77. /**
  78. * String value for this MIME type. Computed when needed and cached.
  79. */
  80. private transient String myStringValue = null;
  81. /**
  82. * Parameter map entry set. Computed when needed and cached.
  83. */
  84. private transient ParameterMapEntrySet myEntrySet = null;
  85. /**
  86. * Parameter map. Computed when needed and cached.
  87. */
  88. private transient ParameterMap myParameterMap = null;
  89. /**
  90. * Parameter map entry.
  91. */
  92. private class ParameterMapEntry implements Map.Entry {
  93. private int myIndex;
  94. public ParameterMapEntry(int theIndex) {
  95. myIndex = theIndex;
  96. }
  97. public Object getKey(){
  98. return myPieces[myIndex];
  99. }
  100. public Object getValue(){
  101. return myPieces[myIndex+1];
  102. }
  103. public Object setValue (Object value) {
  104. throw new UnsupportedOperationException();
  105. }
  106. public boolean equals(Object o) {
  107. return (o != null &&
  108. o instanceof Map.Entry &&
  109. getKey().equals (((Map.Entry) o).getKey()) &&
  110. getValue().equals(((Map.Entry) o).getValue()));
  111. }
  112. public int hashCode() {
  113. return getKey().hashCode() ^ getValue().hashCode();
  114. }
  115. }
  116. /**
  117. * Parameter map entry set iterator.
  118. */
  119. private class ParameterMapEntrySetIterator implements Iterator {
  120. private int myIndex = 2;
  121. public boolean hasNext() {
  122. return myIndex < myPieces.length;
  123. }
  124. public Object next() {
  125. if (hasNext()) {
  126. ParameterMapEntry result = new ParameterMapEntry (myIndex);
  127. myIndex += 2;
  128. return result;
  129. } else {
  130. throw new NoSuchElementException();
  131. }
  132. }
  133. public void remove() {
  134. throw new UnsupportedOperationException();
  135. }
  136. }
  137. /**
  138. * Parameter map entry set.
  139. */
  140. private class ParameterMapEntrySet extends AbstractSet {
  141. public Iterator iterator() {
  142. return new ParameterMapEntrySetIterator();
  143. }
  144. public int size() {
  145. return (myPieces.length - 2) / 2;
  146. }
  147. }
  148. /**
  149. * Parameter map.
  150. */
  151. private class ParameterMap extends AbstractMap {
  152. public Set entrySet() {
  153. if (myEntrySet == null) {
  154. myEntrySet = new ParameterMapEntrySet();
  155. }
  156. return myEntrySet;
  157. }
  158. }
  159. /**
  160. * Construct a new MIME type object from the given string. The given
  161. * string is converted into canonical form and stored internally.
  162. *
  163. * @param s MIME media type string.
  164. *
  165. * @exception NullPointerException
  166. * (unchecked exception) Thrown if <CODE>s</CODE> is null.
  167. * @exception IllegalArgumentException
  168. * (unchecked exception) Thrown if <CODE>s</CODE> does not obey the
  169. * syntax for a MIME media type string.
  170. */
  171. public MimeType(String s) {
  172. parse (s);
  173. }
  174. /**
  175. * Returns this MIME type object's MIME type string based on the canonical
  176. * form. Each parameter value is enclosed in quotes.
  177. */
  178. public String getMimeType() {
  179. return getStringValue();
  180. }
  181. /**
  182. * Returns this MIME type object's media type.
  183. */
  184. public String getMediaType() {
  185. return myPieces[0];
  186. }
  187. /**
  188. * Returns this MIME type object's media subtype.
  189. */
  190. public String getMediaSubtype() {
  191. return myPieces[1];
  192. }
  193. /**
  194. * Returns an unmodifiable map view of the parameters in this MIME type
  195. * object. Each entry in the parameter map view consists of a parameter
  196. * name String (key) mapping to a parameter value String. If this MIME
  197. * type object has no parameters, an empty map is returned.
  198. *
  199. * @return Parameter map for this MIME type object.
  200. */
  201. public Map getParameterMap() {
  202. if (myParameterMap == null) {
  203. myParameterMap = new ParameterMap();
  204. }
  205. return myParameterMap;
  206. }
  207. /**
  208. * Converts this MIME type object to a string.
  209. *
  210. * @return MIME type string based on the canonical form. Each parameter
  211. * value is enclosed in quotes.
  212. */
  213. public String toString() {
  214. return getStringValue();
  215. }
  216. /**
  217. * Returns a hash code for this MIME type object.
  218. */
  219. public int hashCode() {
  220. return getStringValue().hashCode();
  221. }
  222. /**
  223. * Determine if this MIME type object is equal to the given object. The two
  224. * are equal if the given object is not null, is an instance of class
  225. * net.jini.print.data.MimeType, and has the same canonical form as this
  226. * MIME type object (that is, has the same type, subtype, and parameters).
  227. * Thus, if two MIME type objects are the same except for comments, they are
  228. * considered equal. However, "text/plain" and "text/plain;
  229. * charset=us-ascii" are not considered equal, even though they represent
  230. * the same media type (because the default character set for plain text is
  231. * US-ASCII).
  232. *
  233. * @param obj Object to test.
  234. *
  235. * @return True if this MIME type object equals <CODE>obj</CODE>, false
  236. * otherwise.
  237. */
  238. public boolean equals (Object obj) {
  239. return(obj != null &&
  240. obj instanceof MimeType &&
  241. getStringValue().equals(((MimeType) obj).getStringValue()));
  242. }
  243. /**
  244. * Returns this MIME type's string value in canonical form.
  245. */
  246. private String getStringValue() {
  247. if (myStringValue == null) {
  248. StringBuffer result = new StringBuffer();
  249. result.append (myPieces[0]);
  250. result.append ('/');
  251. result.append (myPieces[1]);
  252. int n = myPieces.length;
  253. for (int i = 2; i < n; i += 2) {
  254. result.append(';');
  255. result.append(' ');
  256. result.append(myPieces[i]);
  257. result.append('=');
  258. result.append(addQuotes (myPieces[i+1]));
  259. }
  260. myStringValue = result.toString();
  261. }
  262. return myStringValue;
  263. }
  264. // Hidden classes, constants, and operations for parsing a MIME media type
  265. // string.
  266. // Lexeme types.
  267. private static final int TOKEN_LEXEME = 0;
  268. private static final int QUOTED_STRING_LEXEME = 1;
  269. private static final int TSPECIAL_LEXEME = 2;
  270. private static final int EOF_LEXEME = 3;
  271. private static final int ILLEGAL_LEXEME = 4;
  272. // Class for a lexical analyzer.
  273. private static class LexicalAnalyzer {
  274. protected String mySource;
  275. protected int mySourceLength;
  276. protected int myCurrentIndex;
  277. protected int myLexemeType;
  278. protected int myLexemeBeginIndex;
  279. protected int myLexemeEndIndex;
  280. public LexicalAnalyzer(String theSource) {
  281. mySource = theSource;
  282. mySourceLength = theSource.length();
  283. myCurrentIndex = 0;
  284. nextLexeme();
  285. }
  286. public int getLexemeType() {
  287. return myLexemeType;
  288. }
  289. public String getLexeme() {
  290. return(myLexemeBeginIndex >= mySourceLength ?
  291. null :
  292. mySource.substring(myLexemeBeginIndex, myLexemeEndIndex));
  293. }
  294. public char getLexemeFirstCharacter() {
  295. return(myLexemeBeginIndex >= mySourceLength ?
  296. '\u0000' :
  297. mySource.charAt(myLexemeBeginIndex));
  298. }
  299. public void nextLexeme() {
  300. int state = 0;
  301. int commentLevel = 0;
  302. char c;
  303. while (state >= 0) {
  304. switch (state) {
  305. // Looking for a token, quoted string, or tspecial
  306. case 0:
  307. if (myCurrentIndex >= mySourceLength) {
  308. myLexemeType = EOF_LEXEME;
  309. myLexemeBeginIndex = mySourceLength;
  310. myLexemeEndIndex = mySourceLength;
  311. state = -1;
  312. } else if (Character.isWhitespace
  313. (c = mySource.charAt (myCurrentIndex ++))) {
  314. state = 0;
  315. } else if (c == '\"') {
  316. myLexemeType = QUOTED_STRING_LEXEME;
  317. myLexemeBeginIndex = myCurrentIndex;
  318. state = 1;
  319. } else if (c == '(') {
  320. ++ commentLevel;
  321. state = 3;
  322. } else if (c == '/' || c == ';' || c == '=' ||
  323. c == ')' || c == '<' || c == '>' ||
  324. c == '@' || c == ',' || c == ':' ||
  325. c == '\\' || c == '[' || c == ']' ||
  326. c == '?') {
  327. myLexemeType = TSPECIAL_LEXEME;
  328. myLexemeBeginIndex = myCurrentIndex - 1;
  329. myLexemeEndIndex = myCurrentIndex;
  330. state = -1;
  331. } else {
  332. myLexemeType = TOKEN_LEXEME;
  333. myLexemeBeginIndex = myCurrentIndex - 1;
  334. state = 5;
  335. }
  336. break;
  337. // In a quoted string
  338. case 1:
  339. if (myCurrentIndex >= mySourceLength) {
  340. myLexemeType = ILLEGAL_LEXEME;
  341. myLexemeBeginIndex = mySourceLength;
  342. myLexemeEndIndex = mySourceLength;
  343. state = -1;
  344. } else if ((c = mySource.charAt (myCurrentIndex ++)) == '\"') {
  345. myLexemeEndIndex = myCurrentIndex - 1;
  346. state = -1;
  347. } else if (c == '\\') {
  348. state = 2;
  349. } else {
  350. state = 1;
  351. }
  352. break;
  353. // In a quoted string, backslash seen
  354. case 2:
  355. if (myCurrentIndex >= mySourceLength) {
  356. myLexemeType = ILLEGAL_LEXEME;
  357. myLexemeBeginIndex = mySourceLength;
  358. myLexemeEndIndex = mySourceLength;
  359. state = -1;
  360. } else {
  361. ++ myCurrentIndex;
  362. state = 1;
  363. } break;
  364. // In a comment
  365. case 3: if (myCurrentIndex >= mySourceLength) {
  366. myLexemeType = ILLEGAL_LEXEME;
  367. myLexemeBeginIndex = mySourceLength;
  368. myLexemeEndIndex = mySourceLength;
  369. state = -1;
  370. } else if ((c = mySource.charAt (myCurrentIndex ++)) == '(') {
  371. ++ commentLevel;
  372. state = 3;
  373. } else if (c == ')') {
  374. -- commentLevel;
  375. state = commentLevel == 0 ? 0 : 3;
  376. } else if (c == '\\') {
  377. state = 4;
  378. } else { state = 3;
  379. }
  380. break;
  381. // In a comment, backslash seen
  382. case 4:
  383. if (myCurrentIndex >= mySourceLength) {
  384. myLexemeType = ILLEGAL_LEXEME;
  385. myLexemeBeginIndex = mySourceLength;
  386. myLexemeEndIndex = mySourceLength;
  387. state = -1;
  388. } else {
  389. ++ myCurrentIndex;
  390. state = 3;
  391. }
  392. break;
  393. // In a token
  394. case 5:
  395. if (myCurrentIndex >= mySourceLength) {
  396. myLexemeEndIndex = myCurrentIndex;
  397. state = -1;
  398. } else if (Character.isWhitespace
  399. (c = mySource.charAt (myCurrentIndex ++))) {
  400. myLexemeEndIndex = myCurrentIndex - 1;
  401. state = -1;
  402. } else if (c == '\"' || c == '(' || c == '/' ||
  403. c == ';' || c == '=' || c == ')' ||
  404. c == '<' || c == '>' || c == '@' ||
  405. c == ',' || c == ':' || c == '\\' ||
  406. c == '[' || c == ']' || c == '?') {
  407. -- myCurrentIndex;
  408. myLexemeEndIndex = myCurrentIndex;
  409. state = -1;
  410. } else {
  411. state = 5;
  412. }
  413. break;
  414. }
  415. }
  416. }
  417. }
  418. /**
  419. * Returns a lowercase version of the given string. The lowercase version
  420. * is constructed by applying Character.toLowerCase() to each character of
  421. * the given string, which maps characters to lowercase using the rules of
  422. * Unicode. This mapping is the same regardless of locale, whereas the
  423. * mapping of String.toLowerCase() may be different depending on the
  424. * default locale.
  425. */
  426. private static String toUnicodeLowerCase(String s) {
  427. int n = s.length();
  428. char[] result = new char [n];
  429. for (int i = 0; i < n; ++ i) {
  430. result[i] = Character.toLowerCase (s.charAt (i));
  431. }
  432. return new String (result);
  433. }
  434. /**
  435. * Returns a version of the given string with backslashes removed.
  436. */
  437. private static String removeBackslashes(String s) {
  438. int n = s.length();
  439. char[] result = new char [n];
  440. int i;
  441. int j = 0;
  442. char c;
  443. for (i = 0; i < n; ++ i) {
  444. c = s.charAt (i);
  445. if (c == '\\') {
  446. c = s.charAt (++ i);
  447. }
  448. result[j++] = c;
  449. }
  450. return new String (result, 0, j);
  451. }
  452. /**
  453. * Returns a version of the string surrounded by quotes and with interior
  454. * quotes preceded by a backslash.
  455. */
  456. private static String addQuotes(String s) {
  457. int n = s.length();
  458. int i;
  459. char c;
  460. StringBuffer result = new StringBuffer (n+2);
  461. result.append ('\"');
  462. for (i = 0; i < n; ++ i) {
  463. c = s.charAt (i);
  464. if (c == '\"') {
  465. result.append ('\\');
  466. }
  467. result.append (c);
  468. }
  469. result.append ('\"');
  470. return result.toString();
  471. }
  472. /**
  473. * Parses the given string into canonical pieces and stores the pieces in
  474. * {@link #myPieces <CODE>myPieces</CODE>}.
  475. * <P>
  476. * Special rules applied:
  477. * <UL>
  478. * <LI> If the media type is text, the value of a charset parameter is
  479. * converted to lowercase.
  480. * </UL>
  481. *
  482. * @param s MIME media type string.
  483. *
  484. * @exception NullPointerException
  485. * (unchecked exception) Thrown if <CODE>s</CODE> is null.
  486. * @exception IllegalArgumentException
  487. * (unchecked exception) Thrown if <CODE>s</CODE> does not obey the
  488. * syntax for a MIME media type string.
  489. */
  490. private void parse(String s) {
  491. // Initialize.
  492. if (s == null) {
  493. throw new NullPointerException();
  494. }
  495. LexicalAnalyzer theLexer = new LexicalAnalyzer (s);
  496. int theLexemeType;
  497. Vector thePieces = new Vector();
  498. boolean mediaTypeIsText = false;
  499. boolean parameterNameIsCharset = false;
  500. // Parse media type.
  501. if (theLexer.getLexemeType() == TOKEN_LEXEME) {
  502. String mt = toUnicodeLowerCase (theLexer.getLexeme());
  503. thePieces.add (mt);
  504. theLexer.nextLexeme();
  505. mediaTypeIsText = mt.equals ("text");
  506. } else {
  507. throw new IllegalArgumentException();
  508. }
  509. // Parse slash.
  510. if (theLexer.getLexemeType() == TSPECIAL_LEXEME &&
  511. theLexer.getLexemeFirstCharacter() == '/') {
  512. theLexer.nextLexeme();
  513. } else {
  514. throw new IllegalArgumentException();
  515. }
  516. if (theLexer.getLexemeType() == TOKEN_LEXEME) {
  517. thePieces.add (toUnicodeLowerCase (theLexer.getLexeme()));
  518. theLexer.nextLexeme();
  519. } else {
  520. throw new IllegalArgumentException();
  521. }
  522. // Parse zero or more parameters.
  523. while (theLexer.getLexemeType() == TSPECIAL_LEXEME &&
  524. theLexer.getLexemeFirstCharacter() == ';') {
  525. // Parse semicolon.
  526. theLexer.nextLexeme();
  527. // Parse parameter name.
  528. if (theLexer.getLexemeType() == TOKEN_LEXEME) {
  529. String pn = toUnicodeLowerCase (theLexer.getLexeme());
  530. thePieces.add (pn);
  531. theLexer.nextLexeme();
  532. parameterNameIsCharset = pn.equals ("charset");
  533. } else {
  534. throw new IllegalArgumentException();
  535. }
  536. // Parse equals.
  537. if (theLexer.getLexemeType() == TSPECIAL_LEXEME &&
  538. theLexer.getLexemeFirstCharacter() == '=') {
  539. theLexer.nextLexeme();
  540. } else {
  541. throw new IllegalArgumentException();
  542. }
  543. // Parse parameter value.
  544. if (theLexer.getLexemeType() == TOKEN_LEXEME) {
  545. String pv = theLexer.getLexeme();
  546. thePieces.add(mediaTypeIsText && parameterNameIsCharset ?
  547. toUnicodeLowerCase (pv) :
  548. pv);
  549. theLexer.nextLexeme();
  550. } else if (theLexer.getLexemeType() == QUOTED_STRING_LEXEME) {
  551. String pv = removeBackslashes (theLexer.getLexeme());
  552. thePieces.add(mediaTypeIsText && parameterNameIsCharset ?
  553. toUnicodeLowerCase (pv) :
  554. pv);
  555. theLexer.nextLexeme();
  556. } else {
  557. throw new IllegalArgumentException();
  558. }
  559. }
  560. // Make sure we've consumed everything.
  561. if (theLexer.getLexemeType() != EOF_LEXEME) {
  562. throw new IllegalArgumentException();
  563. }
  564. // Save the pieces. Parameters are not in ascending order yet.
  565. int n = thePieces.size();
  566. myPieces = (String[]) thePieces.toArray (new String [n]);
  567. // Sort the parameters into ascending order using an insertion sort.
  568. int i, j;
  569. String temp;
  570. for (i = 4; i < n; i += 2) {
  571. j = 2;
  572. while (j < i && myPieces[j].compareTo (myPieces[i]) <= 0) {
  573. j += 2;
  574. }
  575. while (j < i) {
  576. temp = myPieces[j];
  577. myPieces[j] = myPieces[i];
  578. myPieces[i] = temp;
  579. temp = myPieces[j+1];
  580. myPieces[j+1] = myPieces[i+1];
  581. myPieces[i+1] = temp;
  582. j += 2;
  583. }
  584. }
  585. }
  586. }