/* * fred - MediaType.java - Copyright © 2011 David Roden * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program 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 for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package freenet.support; import java.net.MalformedURLException; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import freenet.client.DefaultMIMETypes; /** * A media type denotes the content type of a document. A media consists of a * top-level type, a subtype, and an optional list of key-value pairs. An * example would be “audio/ogg” or “text/html; charset=utf-8.” * <p> * {@link MediaType}s are immutable. The setter methods (e.g. * {@link #setType(String)} and {@link #setSubtype(String)}) return new * {@link MediaType} objects with the requested part changed and all other parts * copied. * <p> * Media types are defined in <a href="http://www.ietf.org/rfc/rfc2046.txt">RFC * 2046</a>. * * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a> */ public class MediaType { /** The top-level type. */ private final String type; /** The subtype. */ private final String subtype; /** The parameters. */ private final LinkedHashMap<String, String> parameters = new LinkedHashMap<String, String>(); /** * Creates a new media type by parsing the given string. * * @param mediaType * The media type to parse * @throws NullPointerException * if {@code mediaType} is {@code null} * @throws MalformedURLException * if {@code mediaType} is incorrectly formatted, i.e. does not * contain a slash, or a parameter does not contain an equals * sign */ public MediaType(String mediaType) throws NullPointerException, MalformedURLException { if (mediaType == null) { throw new NullPointerException("contentType must not be null"); } if(!DefaultMIMETypes.isPlausibleMIMEType(mediaType)) throw new MalformedURLException("Doesn't look like a MIME type"); int slash = mediaType.indexOf('/'); if (slash == -1) { throw new MalformedURLException("mediaType does not contain ‘/’!"); } type = mediaType.substring(0, slash); int semicolon = mediaType.indexOf(';'); if (semicolon == -1) { subtype = mediaType.substring(slash + 1); return; } subtype = mediaType.substring(slash + 1, semicolon).trim(); String[] parameters = mediaType.substring(semicolon + 1).split(";"); for (String parameter : parameters) { int equals = parameter.indexOf('='); if (equals == -1) { throw new MalformedURLException(String.format("Illegal parameter: “%s”", parameter)); } String name = parameter.substring(0, equals).trim().toLowerCase(); String value = parameter.substring(equals + 1).trim(); if(value.startsWith("\"") && value.endsWith("\"")) value = value.substring(1, value.length()-1).trim(); this.parameters.put(name, value); } } /** * Creates a new media type. * * @param type * The top-level type * @param subtype * The subtype * @param parameters * The parameters in key-value pairs, in the order {@code key1}, * {@code value1}, {@code key2}, {@code value2}, … * @throws IllegalArgumentException * if an invalid number of parameters is given (i.e. the number * of parameters is odd) */ public MediaType(String type, String subtype, String... parameters) throws IllegalArgumentException { if ((parameters.length & 1) != 0) { throw new IllegalArgumentException("Invalid number of parameters given!"); } this.type = type; this.subtype = subtype; for (int index = 0; index < parameters.length; index += 2) { this.parameters.put(parameters[index], parameters[index + 1]); } } /** * Creates a new media type. * * @param type * The top-level type * @param subtype * The subtype * @param parameters * The parameters of the media type */ public MediaType(String type, String subtype, Map<String, String> parameters) { this.type = type; this.subtype = subtype; this.parameters.putAll(parameters); } // // ACCESSORS // /** * Returns the top-level type of this media type. * * @return The top-level type */ public String getType() { return type; } /** * Creates a new media type that has the same subtype and parameters as this * media type and the given type as top-level type. * * @param type * The top-level type of the new media type * @return The new media type */ public MediaType setType(String type) { return new MediaType(type, subtype, parameters); } /** * Returns the subtype of this media type. * * @return The subtype */ public String getSubtype() { return subtype; } /** * Creates a new media type that has the same top-level type and parameters * as this media type and the given subtype as subtype. * * @param subtype * The subtype of the new media type * @return The new media type */ public MediaType setSubtype(String subtype) { return new MediaType(type, subtype, parameters); } /** * Returns the value of the parameter with the given name. * * @param name * The name of the parameter * @return The value of the parameter (or {@code null} if the media type * does not have a parameter with the given name) */ public String getParameter(String name) { return parameters.get(name.toLowerCase()); } /** * Creates a new media type that has the same top-level type, subtype, and * parameters as this media type but has the parameter with the given name * changed to the given value. * * @param name * The name of the parameter to change * @param value * The new value of the parameter. Null = delete parameter. * @return The new media type */ public MediaType setParameter(String name, String value) { MediaType newMediaType = new MediaType(type, subtype, parameters); if(value == null) newMediaType.parameters.remove(name.toLowerCase()); else newMediaType.parameters.put(name.toLowerCase(), value); return newMediaType; } /** * Creates a new media type that has the same top-level type, subtype, and * parameters as this media type but has the parameter with the given name * removed. * * @param name * The name of the parameter to remove * @return The new media type */ public MediaType removeParameter(String name) { if (!parameters.containsKey(name.toLowerCase())) { return this; } MediaType newMediaType = new MediaType(type, subtype, parameters); newMediaType.parameters.remove(name.toLowerCase()); return newMediaType; } // // OBJECT METHODS // /** * {@inheritDoc} */ @Override public String toString() { StringBuilder mediaType = new StringBuilder(); mediaType.append(type).append('/').append(subtype); for (Entry<String, String> parameter : parameters.entrySet()) { if (parameter.getValue() == null) { continue; } mediaType.append("; ").append(parameter.getKey()).append("=\"").append(parameter.getValue()).append("\""); } return mediaType.toString(); } public static String getCharsetRobust(String expectedMimeType) { try { if(expectedMimeType == null) return null; MediaType type = new MediaType(expectedMimeType); return type.getParameter("charset"); } catch (MalformedURLException e) { return null; } catch (Throwable t) { // Could be malicious, hence "Robust". return null; } } public static String getCharsetRobustOrUTF(String expectedMimeType) { String charset = getCharsetRobust(expectedMimeType); if(charset == null) return "UTF-8"; return charset; } public LinkedHashMap<String, String> getParameters() { LinkedHashMap<String, String> map = new LinkedHashMap<String, String>(); map.putAll(parameters); return map; } /** Get the base type without any parameters */ public String getPlainType() { return type + '/' + subtype; } }