/**
* Copyright (C) 2010 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xcmis.spi.utils;
import org.xcmis.spi.CmisConstants;
import org.xcmis.spi.CmisRuntimeException;
import java.text.ParseException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:andrey.parfonov@exoplatform.com">Andrey Parfonov</a>
* @version $Id: MimeType.java 2 2010-02-04 17:21:49Z andrew00x $
*/
public class MimeType
{
static class ParameterParser
{
/**
* Parameter separator.
*/
private static final char SEPARATOR = ';';
private static final String SEPARTORS = "()<>@,;:\"\\/[]?={}";
/**
* Current position in the parsed string.
*/
private int pos = 0;
/**
* Token's start.
*/
private int i1 = 0;
/**
* Token's end.
*/
private int i2 = 0;
/**
* String to be parsed.
*/
private char[] chars = null;
/**
* Parsed string length.
*/
private int length = 0;
/**
* Parse header string for parameters.
*
* @param source source header string
* @return header parameter
* @throws ParseException if string can't be parsed or contains illegal
* characters
*/
public Map<String, String> parse(String source) throws ParseException
{
init(source);
if (pos < 0)
{
return null;
}
pos++; // skip first ';'
Map<String, String> m = null;
while (hasChars())
{
String name = readToken(new char[]{'=', SEPARATOR});
String value = null;
if (hasChars() && chars[pos] == '=')
{
pos++; // skip '='
if (chars[pos] == '"')
{
value = readQuotedString();
}
else
{
value = readToken(new char[]{SEPARATOR});
}
}
if (hasChars() && chars[pos] == SEPARATOR)
{
pos++; // skip ';'
}
if (name != null && name.length() > 0)
{
if (m == null)
{
m = new HashMap<String, String>();
}
m.put(name, value);
}
}
return m;
}
/**
* Check does char array <tt>chs</tt> contains char <tt>c</tt>.
*
* @param c char
* @param chs char array
* @return true if char array contains character <tt>c</tt>, false
* otherwise
*/
private boolean checkChar(char c, char[] chs)
{
for (int i = 0; i < chs.length; i++)
{
if (c == chs[i])
{
return true;
}
}
return false;
}
private String filterEscape(String token)
{
StringBuffer sb = new StringBuffer();
// boolean escape = false;
int strlen = token.length();
for (int i = 0; i < strlen; i++)
{
char c = token.charAt(i);
// escape = !escape && c == '\\';
if (c == '\\' && i < strlen - 1 && token.charAt(i + 1) == '"')
{
continue;
}
sb.append(c);
}
return sb.toString();
}
/**
* @param removeQuotes must leading and trailing quotes be skipped
* @return parsed token
*/
private String getToken(boolean removeQuotes)
{
// leading whitespace
while ((i1 < i2) && Character.isWhitespace(chars[i1]))
{
i1++;
}
// tail whitespace
while ((i2 > i1) && Character.isWhitespace(chars[i2 - 1]))
{
i2--;
}
// remove quotes
if (removeQuotes && chars[i1] == '"' && chars[i2 - 1] == '"')
{
i1++;
i2--;
}
String token = null;
if (i2 > i1)
{
token = new String(chars, i1, i2 - i1);
}
return token;
}
/**
* Check are there any character to be parsed.
*
* @return true if there are unparsed characters, false otherwise
*/
private boolean hasChars()
{
return pos < length;
}
/**
* Initialize character array for parsing.
*
* @param source source string for parsing
*/
private void init(String source)
{
// looking for start parameters position
// e.g. text/plain ; charsert=utf-8
pos = source.indexOf(SEPARATOR);
if (pos < 0)
{
// header string does not contains parameters
return;
}
chars = source.toCharArray();
length = chars.length;
i1 = 0;
i2 = 0;
}
private int isToken(String token)
{
for (int i = 0; i < token.length(); i++)
{
char c = token.charAt(i);
if (c >= 127 || SEPARTORS.indexOf(c) != -1)
{
return i;
}
}
return -1;
}
/**
* Process quoted string, it minds remove escape characters for quotes.
*
* @return processed string
* @throws ParseException if string can't be parsed
*/
private String readQuotedString() throws ParseException
{
i1 = pos;
i2 = pos;
// indicate was previous character '\'
boolean escape = false;
// indicate is final '"' already found
boolean qoute = false;
while (hasChars())
{
char c = chars[pos];
if (c == SEPARATOR && !qoute)
{
break;
}
if (c == '"' && !escape)
{
qoute = !qoute;
}
escape = !escape && c == '\\';
pos++;
i2++;
}
if (qoute)
{
throw new ParseException("String must be ended with qoute.", pos);
}
String token = getToken(true);
if (token != null)
{
token = filterEscape(getToken(true));
}
return token;
}
/**
* Read token from source string, token is not quoted string and does not
* contains any separators. See <a
* href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec2.html">HTTP1.1
* specification</a>.
*
* @param terminators characters which indicate end of token
* @return token
* @throws ParseException if token contains illegal characters
*/
private String readToken(char[] terminators) throws ParseException
{
i1 = pos;
i2 = pos;
while (hasChars())
{
char c = chars[pos];
if (checkChar(c, terminators))
{
break;
}
pos++;
i2++;
}
String token = getToken(false);
if (token != null)
{
// check is it valid token
int err = -1;
if ((err = isToken(token)) != -1)
{
throw new ParseException("Token '" + token + "' contains not legal characters at " + err, err);
}
}
return token;
}
}
private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s");
private static final Pattern WHITESPACE_QOUTE_PATTERN = Pattern.compile("[\\s\"]");
/**
* Create instance of MimeType from the string.
*
* @param source string that represents media-type in form 'type/sub-type'.
* If <code>source</code> is <code>null</code> or empty then it is the
* same as pass '*/*'. All parameters after ';' in
* <code>source</code> will be ignored.
* @return MimeType
*/
public static MimeType fromString(String source)
{
if (source == null || source.length() == 0)
{
return new MimeType();
}
int p = source.indexOf('/');
int col = source.indexOf(';');
String type = null;
String subType = null;
if (p < 0 && col < 0)
{
return new MimeType(source, null);
}
else if (p > 0 && col < 0)
{
return new MimeType(removeWhitespaces(source.substring(0, p)), removeWhitespaces(source.substring(p + 1)));
}
else if (p < 0 && col > 0)
{ // there is no '/' but present ';'
type = removeWhitespaces(source.substring(0, col));
// sub-type is null
}
else
{ // presents '/' and ';'
type = removeWhitespaces(source.substring(0, p));
subType = source.substring(p + 1, col);
}
try
{
return new MimeType(type, subType, new ParameterParser().parse(source));
}
catch (ParseException pe)
{
throw new CmisRuntimeException(pe.getMessage(), pe);
}
}
private static String removeWhitespaces(String s)
{
Matcher m = WHITESPACE_PATTERN.matcher(s);
if (m.find())
{
return m.replaceAll("");
}
return s;
}
/** Type. */
private final String type;
/** Sub-type. */
private final String subType;
private final Map<String, String> parameters;
public MimeType()
{
this(null, null);
}
/**
* Create instance of MimeType.
*
* @param type the name of type
* @param subType the name of sub-type
*/
public MimeType(String type, String subType)
{
this.type =
type == null || type.length() == 0 ? CmisConstants.WILDCARD : ((type = type.trim()).length() == 0
? CmisConstants.WILDCARD : type.toLowerCase());
this.subType =
subType == null || subType.length() == 0 ? CmisConstants.WILDCARD : ((subType = subType.trim()).length() == 0
? CmisConstants.WILDCARD : subType.toLowerCase());
this.parameters = new HashMap<String, String>();
}
public MimeType(String type, String subType, Map<String, String> parameters)
{
this.type =
type == null || type.length() == 0 ? CmisConstants.WILDCARD : ((type = type.trim()).length() == 0
? CmisConstants.WILDCARD : type.toLowerCase());
this.subType =
subType == null || subType.length() == 0 ? CmisConstants.WILDCARD : ((subType = subType.trim()).length() == 0
? CmisConstants.WILDCARD : subType.toLowerCase());
if (parameters == null)
{
this.parameters = new HashMap<String, String>();
}
else
{
Map<String, String> map = new TreeMap<String, String>(new Comparator<String>()
{
public int compare(String o1, String o2)
{
return o1.compareToIgnoreCase(o2);
}
});
for (Map.Entry<String, String> e : parameters.entrySet())
{
map.put(e.getKey().toLowerCase(), e.getValue());
}
this.parameters = Collections.unmodifiableMap(map);
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object other)
{
if (other == null)
{
return false;
}
if (!(other instanceof MimeType))
{
return false;
}
MimeType otherMimeType = (MimeType)other;
return type.equalsIgnoreCase(otherMimeType.type) && subType.equalsIgnoreCase(otherMimeType.subType)
&& parameters.equals(otherMimeType.parameters);
}
/**
* @return get type
*/
public String getSubType()
{
return subType;
}
/**
* @return get sub-type
*/
public String getType()
{
return type;
}
/**
* @return mime type parameters
*/
public Map<String, String> getParameters()
{
return parameters;
}
public String getParameter(String name)
{
return parameters.get(name);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode()
{
int hash = 9;
hash = hash * 31 + type.hashCode();
hash = hash * 31 + subType.hashCode();
hash = hash * 31 + parameters.hashCode();
return hash;
}
/**
* Check is one mime-type compatible to other. Function is not commutative.
* E.g. image/* compatible with image/png, image/jpeg, but image/png is not
* compatible with image/*.
*
* @param other MimeType to be checked for compatible with this.
* @return TRUE if MimeTypes compatible FALSE otherwise
*/
public boolean match(MimeType other)
{
if (other == null)
{
return false;
}
return type.equals(CmisConstants.WILDCARD) //
|| (type.equalsIgnoreCase(other.type) //
&& (subType.equals(CmisConstants.WILDCARD) || subType.equalsIgnoreCase(other.subType)));
}
/**
* {@inheritDoc}
*/
public String getBaseType()
{
return type + "/" + subType;
}
public String toString()
{
StringBuffer sb = new StringBuffer();
sb.append(getBaseType());
for (Map.Entry<String, String> entry : parameters.entrySet())
{
sb.append(';').append(entry.getKey()).append('=');
appendWithQuote(sb, entry.getValue());
}
return sb.toString();
}
private void appendWithQuote(StringBuffer sb, String s)
{
if (s == null)
{
return;
}
Matcher m = WHITESPACE_QOUTE_PATTERN.matcher(s);
if (m.find())
{
sb.append('"');
appendEscapeQuote(sb, s);
sb.append('"');
return;
}
sb.append(s);
}
private void appendEscapeQuote(StringBuffer sb, String s)
{
for (int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == '"')
{
sb.append('\\');
}
sb.append(c);
}
}
}