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