/*
* Copyright (c) 2001-2007 Sun Microsystems, Inc. All rights reserved.
*
* The Sun Project JXTA(TM) Software License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. The end-user documentation included with the redistribution, if any, must
* include the following acknowledgment: "This product includes software
* developed by Sun Microsystems, Inc. for JXTA(TM) technology."
* Alternately, this acknowledgment may appear in the software itself, if
* and wherever such third-party acknowledgments normally appear.
*
* 4. The names "Sun", "Sun Microsystems, Inc.", "JXTA" and "Project JXTA" must
* not be used to endorse or promote products derived from this software
* without prior written permission. For written permission, please contact
* Project JXTA at http://www.jxta.org.
*
* 5. Products derived from this software may not be called "JXTA", nor may
* "JXTA" appear in their name, without prior written permission of Sun.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
* FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SUN
* MICROSYSTEMS OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* JXTA is a registered trademark of Sun Microsystems, Inc. in the United
* States and other countries.
*
* Please see the license information page at :
* <http://www.jxta.org/project/www/license.html> for instructions on use of
* the license in source files.
*
* ====================================================================
*
* This software consists of voluntary contributions made by many individuals
* on behalf of Project JXTA. For more information on Project JXTA, please see
* http://www.jxta.org.
*
* This license is based on the BSD license adopted by the Apache Foundation.
*/
package net.jxta.document;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ConcurrentMap;
import net.jxta.util.ConcurrentWeakHashMap;
/**
* MIME Media Types are used to describe the format of data streams. MIME
* Media Types are defined by
* {@link <a href="http://www.ietf.org/rfc/rfc2046.txt" target="_blank">IETF RFC 2046 <i>MIME : Media Types</i></a>}.
* This class manages parsing of Mime Media Types from strings and piecemeal
* construction of Mime Media Type descriptors.
* <p/>
* <p/>Note : This implementation does not include support for the character
* encoding techniques described by :
* {@link <a href="http://www.ietf.org/rfc/rfc2046.txt" target="_blank">IETF RFC 2046 <i>MIME : Media Types</i></a>}.
*
* @see net.jxta.document.Document
* @see net.jxta.document.StructuredDocument
* @see net.jxta.document.StructuredDocumentFactory
* @see net.jxta.document.StructuredTextDocument
*/
public class MimeMediaType implements Serializable {
/**
* Magic value for this format of serialization version.
*/
private final static long serialVersionUID = 7546247036878523161L;
private final static String CTL = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007"
+ "\u0008\u0009\n\u000b\u000c\r\u000e\u000f" + "\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017"
+ "\u0018\u0019\u001a\u001b\u001c\u001d\u001e\u001f" + "\u007f";
private final static String space = "\u0020";
private final static String LWSP_char = space + "\u0009";
private final static String param_sep = LWSP_char + ";";
private final static String tspecials = "()<>@,;:\\\"/[]?=";
private final static String terminator = CTL + space + tspecials;
/**
* A canonical map of Mime Media Types.
*
* @since 2.6 This map uses a feature called ConcurrentWeakHashMap which will only be
* part of Java 7. It should not be replaced with a call to Collection.synchronizedMap()
* in the mean time.
*
*/
private static final ConcurrentMap<MimeMediaType, MimeMediaType> interned = new ConcurrentWeakHashMap<MimeMediaType, MimeMediaType>();
/**
* Common Mime Media Type for arbitrary unparsed binary data.
*/
public static final MimeMediaType AOS = new MimeMediaType("application", "octet-stream").intern();
/**
* Common Mime Media Type for text encoded using the default character
* encoding for this JVM. The default character encoding is specified by
* the JDK System property "<code>file.encoding</code>".
* <p/>
* <p/>The default encoding varies with host platform and locale. This
* media type <b>must not</b> be used for <b>any</b> documents which
* will be exchanged with other peers (as they may be using different
* default character encodings).
*/
public static final MimeMediaType TEXT_DEFAULTENCODING = new MimeMediaType("text", "plain").intern();
/**
* Common Mime Media Type for plain text encoded as UTF-8 characters. This
* type is used by JXTA for all strings.
*/
public static final MimeMediaType TEXTUTF8 = new MimeMediaType("text", "plain", "charset=\"UTF-8\"").intern();
/**
* Common Mime Media Type for XML encoded using the default character
* encoding for this JVM. The default character encoding is specified by
* the JDK System property "<code>file.encoding</code>".
* <p/>
* <p/>The default encoding varies with host platform and locale. This
* media type <b>must not</b> be used for <b>any</b> documents which
* will be exchanged with other peers (as they may be using different
* default character encodings).
*/
public static final MimeMediaType XML_DEFAULTENCODING = new MimeMediaType("text", "xml").intern();
/**
* Common Mime Media Type for XML encoded using the default character
* encoding for this JVM. The default character encoding is specified by
* the JDK System property "<code>file.encoding</code>".
* <p/>
* <p/>The default encoding varies with host platform and locale. This
* media type <b>must not</b> be used for <b>any</b> documents which
* will be exchanged with other peers (as they may be using different
* default character encodings).
*/
public static final MimeMediaType APPLICATION_XML_DEFAULTENCODING = new MimeMediaType("application", "xml").intern();
/**
* Common Mime Media Type for XML encoded as UTF-8 characters. This type is
* used by JXTA for all protocol messages and metadata.
*/
public static final MimeMediaType XMLUTF8 = new MimeMediaType("text", "xml", "charset=\"UTF-8\"").intern();
/**
* The primary media type
*/
private transient String type = null;
/**
* The specific media sub-type
*/
private transient String subtype = null;
/**
* The parameters for this media type
*/
private transient List<parameter> parameters = new ArrayList<parameter>();
/**
* The hashcode value for this mime media type.
*/
private transient int cachedHashCode = 0;
/**
* manages a media type parameter.
*/
private static class parameter implements Comparable<parameter> {
/**
* Attribute name.
*/
final String attribute;
/**
* Value for the attribute. <b>Includes quoting characters if they are
* needed for outputting this value.</b>
*/
final String value;
parameter(String attr, String val) {
attribute = attr.intern();
value = val;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof parameter)) {
return false;
}
parameter asParameter = (parameter) obj;
return attribute.equalsIgnoreCase(asParameter.attribute) && asParameter.value.equals(value);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return attribute.toLowerCase().hashCode() * 6037 + value.hashCode();
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return attribute + "=" + outputForm(value);
}
/**
* {@inheritDoc}
*/
public int compareTo(parameter asParameter) {
if (this == asParameter) {
return 0;
}
int result = attribute.compareToIgnoreCase(asParameter.attribute);
if (0 != result) {
return result;
}
return value.compareTo(asParameter.value);
}
private static String outputForm(String val) {
StringBuilder result = new StringBuilder();
if (-1 == findNextSeperator(val)) {
result.append(val);
} else {
// needs quoting
result.append('\"');
for (int eachChar = 0; eachChar < val.length(); eachChar++) {
char aChar = val.charAt(eachChar);
if (('\\' == aChar) || ('\"' == aChar) || ('\r' == aChar)) {
// needs escaping.
result.append('\\');
}
result.append(aChar);
}
result.append('\"');
}
return result.toString();
}
}
/**
* Creates a new MimeMediaType
*
* @param mimetype string representing a mime-type
*/
public MimeMediaType(String mimetype) {
String cleaned = mimetype.trim();
if (0 == cleaned.length()) {
throw new IllegalArgumentException("input cannot be empty");
}
// determine the type
int typeSepAt = findNextSeperator(cleaned);
if ((-1 == typeSepAt) || (0 == typeSepAt) || ('/' != cleaned.charAt(typeSepAt))) {
throw new IllegalArgumentException("expected seperator or seperator in unexpected location");
}
setType(cleaned.substring(0, typeSepAt));
// determine the sub-type
int subtypeSepAt = findNextSeperator(cleaned, typeSepAt + 1);
String itsParams = "";
if (-1 == subtypeSepAt) {
setSubtype(cleaned.substring(typeSepAt + 1));
} else {
setSubtype(cleaned.substring(typeSepAt + 1, subtypeSepAt));
itsParams = cleaned.substring(subtypeSepAt);
// include the seperator, its significant
}
parseParams(itsParams, false);
}
/**
* Creates a new type/subtype MimeMediaType
*
* @param type string representing a mime type
* @param subtype string representing a mime subtype
*/
public MimeMediaType(String type, String subtype) {
this(type, subtype, null);
}
/**
* Creates a new type/subtype MimeMediaType
*
* @param type string representing a mime type
* @param subtype string representing a mime subtype
* @param parameters parameters to the mime-type constructor
*/
public MimeMediaType(String type, String subtype, String parameters) {
setType(type);
setSubtype(subtype);
if (null != parameters) {
parseParams(parameters, false);
}
}
/**
* Creates a new type/subtype MimeMediaType with the specified parameters.
* The parameters are copied from the source mime type and additional params
* are added. If replace is true, then the provided params will overwrite
* the params from the source mime type.
*
* @param type the source mime type
* @param params parameters to the mime-type constructor
* @param replace parameters if true then provided params should replace
* existing params else they are accumulated.
*/
public MimeMediaType(MimeMediaType type, String params, boolean replace) {
setType(type.getType());
setSubtype(type.getSubtype());
parameters.addAll(type.parameters);
parseParams(params, replace);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MimeMediaType)) {
return false;
}
MimeMediaType asMMT = (MimeMediaType) obj;
if (!type.equalsIgnoreCase(asMMT.type)) {
return false;
}
if (!subtype.equalsIgnoreCase(asMMT.subtype)) {
return false;
}
List<parameter> myParams = new ArrayList<parameter>(parameters);
List<parameter> itsParams = new ArrayList<parameter>(asMMT.parameters);
Collections.sort(myParams);
Collections.sort(itsParams);
return myParams.equals(itsParams);
}
/**
* Similar to {@link #equals(Object)}, but ignores any parameters. Compares
* only the type and sub-type.
*
* @param obj the object to compare
* @return true if equal
*/
public boolean equalsIngoringParams(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof MimeMediaType)) {
return false;
}
MimeMediaType likeMe = (MimeMediaType) obj;
boolean retValue = getType().equalsIgnoreCase(likeMe.getType()) && getSubtype().equalsIgnoreCase(likeMe.getSubtype());
return retValue;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
if (0 == cachedHashCode) {
List<parameter> myParams = new ArrayList<parameter>(parameters);
Collections.sort(myParams);
int calcedHash = type.hashCode() * 2467 + subtype.hashCode() * 3943 + myParams.hashCode();
cachedHashCode = (0 != calcedHash) ? calcedHash : 1;
}
return cachedHashCode;
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
StringBuilder retValue = new StringBuilder(getMimeMediaType());
for (parameter parameter : parameters) {
retValue.append(';');
parameter aParam = parameter;
retValue.append(aParam.toString());
}
return retValue.toString();
}
/**
* Returns the "base" MIME media type of this type. ie. with no parameters.
*
* @return The "base" MIME media type of this MimeMediaType.
*/
public MimeMediaType getBaseMimeMediaType() {
return new MimeMediaType(type, subtype).intern();
}
/**
* Returns the "base" MIME media type of this type. ie. with no parameters.
*
* @return The "base" MIME media type of this type. ie. with no parameters.
*/
public String getMimeMediaType() {
StringBuilder retValue = new StringBuilder(type.length() + 1 + subtype.length());
retValue.append(type);
retValue.append('/');
retValue.append(subtype);
return retValue.toString();
}
/**
* Get type of the mime-type
*
* @return type of the mime-type
*/
public String getType() {
return type;
}
/**
* Check if the mime-type is for provisional. See Section 2.1 of
* {@link <a href=http://www.ietf.org/rfc/rfc2048.txt">IETF RFC 2048 <i>MIME : Registration Procedures</i></a>}
*
* @return boolean true if it is a provisional type
*/
public boolean isExperimentalType() {
if ((null == type) || (type.length() < 2)) {
return false;
}
if (type.startsWith("x-") || type.startsWith("x.")) {
return true;
}
return null != subtype && subtype.length() >= 2 && (subtype.startsWith("x-") || subtype.startsWith("x."));
}
/**
* Set the type of MimeMediaType
*
* @param type type value
*/
private void setType(String type) {
if (null == type) {
throw new IllegalArgumentException("type cannot be null");
}
String cleaned = type.trim().toLowerCase(Locale.US);
if (0 == cleaned.length()) {
throw new IllegalArgumentException("type cannot be null");
}
if (-1 != findNextSeperator(cleaned)) {
throw new IllegalArgumentException("type cannot contain a seperator");
}
this.type = cleaned.intern();
}
/**
* Get the Subtype of the mime-type
*
* @return subtype of the mime-type
*/
public String getSubtype() {
return subtype;
}
/**
* Check if the mime-type is for debugging. This method will be
* removed
*
* @return boolean true if it is a debugging type
*/
public boolean isExperimentalSubtype() {
if ((null == subtype) || (subtype.length() < 2)) {
return false;
}
return (('x' == subtype.charAt(0)) && ('-' == subtype.charAt(1)));
}
/**
* Set the subtype of MimeMediaType
*
* @param subtype subtype value
*/
private void setSubtype(String subtype) {
if (null == subtype) {
throw new IllegalArgumentException("subtype cannot be null");
}
String cleaned = subtype.trim().toLowerCase(Locale.US);
if (0 == cleaned.length()) {
throw new IllegalArgumentException("subtype cannot be null");
}
if (-1 != findNextSeperator(cleaned)) {
throw new IllegalArgumentException("subtype cannot contain a seperator");
}
this.subtype = cleaned.intern();
}
/**
* Get the value of the first occurance of the specified parameter from the
* parameter list.
*
* @param param the parameter to retrieve.
* @return the value of the specifid parameter or null if the parameter was
* not found.
*/
public String getParameter(String param) {
for (parameter parameter : parameters) {
parameter aParam = parameter;
if (aParam.attribute.equalsIgnoreCase(param)) {
return aParam.value;
}
}
return null;
}
/**
* Parses the parametes portion of a MIME Media Type specification.
*
* @param itsParams parse a string for mime parameters.
* @param replace parameters if true then provided params should replace
* existing params else they are accumulated.
*/
private void parseParams(String itsParams, boolean replace) {
int currentCharIdx = 0;
String currentAttribute = null;
boolean inSeperator = true;
boolean inComment = false;
boolean inAttribute = false;
StringBuffer currentValue = null;
boolean inValue = false;
boolean inQuoted = false;
boolean nextEscaped = false;
while (currentCharIdx < itsParams.length()) {
char currentChar = itsParams.charAt(currentCharIdx);
if (inSeperator) {
if ('(' == currentChar) {
inSeperator = false;
inComment = true;
} else if (-1 == param_sep.indexOf(currentChar)) {
inSeperator = false;
inAttribute = true;
currentCharIdx--; // unget
}
} else if (inComment) {
if (nextEscaped) {
nextEscaped = false;
} else {
if ('\\' == currentChar) {
nextEscaped = true;
} else if (')' == currentChar) {
inComment = false;
inSeperator = true;
} else if ('\r' == currentChar) {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
}
}
} else if (inAttribute) {
int endAttr = findNextSeperator(itsParams, currentCharIdx);
if ((-1 == endAttr) || ('=' != itsParams.charAt(endAttr)) || (0 == (endAttr - currentCharIdx))) {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
}
currentAttribute = itsParams.substring(currentCharIdx, endAttr).toLowerCase(Locale.US);
currentCharIdx = endAttr; // skip the equals.
inAttribute = false;
inValue = true;
inQuoted = false;
nextEscaped = false;
currentValue = new StringBuffer();
} else if (inValue) {
if (inQuoted) {
if (nextEscaped) {
currentValue.append(currentChar);
nextEscaped = false;
} else {
if ('\\' == currentChar) {
nextEscaped = true;
} else if ('"' == currentChar) {
inQuoted = false;
} else if ('\r' == currentChar) {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
} else {
currentValue.append(currentChar);
}
}
} else if (-1 == terminator.indexOf(currentChar)) {
currentValue.append(currentChar);
} else {
if ('"' == currentChar) {
inQuoted = true;
} else {
parameter newparam = new parameter(currentAttribute, currentValue.toString());
if (replace) {
while (parameters.remove(newparam)) {}
}
parameters.add(newparam);
inValue = false;
inSeperator = true;
currentCharIdx--; // unget
}
}
} else {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
}
currentCharIdx++;
}
// finish off the last value.
if (inValue) {
if (nextEscaped || inQuoted) {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
}
parameter newparam = new parameter(currentAttribute, currentValue.toString());
if (replace) {
while (parameters.remove(newparam)) {}
}
parameters.add(newparam);
inValue = false;
inSeperator = true;
}
if (!inSeperator) {
throw new IllegalArgumentException("malformed mime parameter at idx = " + currentCharIdx);
}
}
/**
* Find next separator position in mime-type
*
* @param source source location
* @return int separator location
*/
private static int findNextSeperator(String source) {
return findNextSeperator(source, 0);
}
/**
* Find next separator position in mime-type
*
* @param source source location
* @param from from location
* @return int separator location
*/
private static int findNextSeperator(String source, int from) {
int seperator = -1;
// find a seperator
for (int eachChar = from; eachChar < source.length(); eachChar++) {
if (-1 != terminator.indexOf(source.charAt(eachChar))) {
seperator = eachChar;
break;
}
}
return seperator;
}
/**
* Returns a MimeMediaType with a value represented by the specified String.
* <p/>
* <b>This method may produce better results than using the constructor
* the same parameter set in that</b>:
* <p/>
* <code>
* new MimeMediaType( string ) != new MimeMediaType( string )
* </code>
* <p/>
* while for common types:
* <p/>
* <code>
* MimeMediaType.valueOf( string ) == MimeMediaType.valueOf( string )
* </code>
*/
public static MimeMediaType valueOf(String mimetype) {
return new MimeMediaType(mimetype).intern();
}
/**
* Read this Object in for Java Serialization
*
* @param s The stream from which the Object will be read.
* @throws IOException for errors reading from the input stream.
* @throws ClassNotFoundException if the serialized representation contains
* references to classes which cannot be found.
*/
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
s.defaultReadObject();
MimeMediaType readType = MimeMediaType.valueOf(s.readUTF());
type = readType.type;
subtype = readType.subtype;
parameters = readType.parameters;
}
/**
* Return the interned form of the ID.
*/
private Object readResolve() throws ObjectStreamException {
return intern();
}
/**
* Write this Object out for Java Serialization
*
* @param s The stream to which the Object will be written.
* @throws IOException for errors writing to the output stream.
*/
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
s.writeUTF(toString());
}
/**
* Returns a canonical representation for the MimeMediaType object.
* <p/>
* <p/>A pool of MimeMediaType, is maintained privately by the class.
* <p/>
* <p/>When the intern method is invoked, if the pool already contains a
* MimeMediaType equal to this MimeMediaType object as determined by the
* equals(Object) method, then the MimeMediaType from the pool is returned.
* Otherwise, this MimeMediaType object is added to the pool and a reference
* to this MimeMediaType object is returned.
* <p/>
* <p/>It follows that for any two MimeMediaType <tt>s</tt> and <tt>t</tt>,
* <tt>s.intern() == t.intern()</tt> is true if and only if <tt>s.equals(t)</tt>
* is true.
*
* @return a MimeMediaType that has the same value as this type, but is
* guaranteed to be from a pool of unique types.
*/
public MimeMediaType intern() {
MimeMediaType singleton = interned.putIfAbsent(this, this);
if(singleton == null) {
return this;
}
return singleton;
}
/**
* This method should only be used for testing purposes.
*/
static void clearInternMap() {
interned.clear();
}
}