blob: 89e43f415f686a9b672302a522a68f0658a354ec [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.olingo.commons.api.format;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
/**
* Internally used {@link ContentType} for OData library.
*
* For more details on format and content of a {@link ContentType} see
* <em>Media Type</em> format as defined in
* <a href="http://www.ietf.org/rfc/rfc7231.txt">RFC 7231</a>, chapter 3.1.1.1.
* <pre>
* media-type = type "/" subtype *( OWS ";" OWS parameter )
* type = token
* subtype = token
* OWS = *( SP / HTAB ) ; optional whitespace
* parameter = token "=" ( token / quoted-string )
* </pre>
*
* Once created a {@link ContentType} is <b>IMMUTABLE</b>.
*/
public final class ContentType {
private static final String APPLICATION = "application";
private static final String TEXT = "text";
private static final String MULTIPART = "multipart";
public static final String PARAMETER_CHARSET = "charset";
public static final String PARAMETER_IEEE754_COMPATIBLE = "IEEE754Compatible";
public static final String PARAMETER_ODATA_METADATA = "odata.metadata";
public static final String VALUE_ODATA_METADATA_NONE = "none";
public static final String VALUE_ODATA_METADATA_MINIMAL = "minimal";
public static final String VALUE_ODATA_METADATA_FULL = "full";
public static final ContentType APPLICATION_JSON = new ContentType(APPLICATION, "json", null);
public static final ContentType JSON = create(ContentType.APPLICATION_JSON,
PARAMETER_ODATA_METADATA, VALUE_ODATA_METADATA_MINIMAL);
public static final ContentType JSON_NO_METADATA = create(ContentType.APPLICATION_JSON,
PARAMETER_ODATA_METADATA, VALUE_ODATA_METADATA_NONE);
public static final ContentType JSON_FULL_METADATA = create(ContentType.APPLICATION_JSON,
PARAMETER_ODATA_METADATA, VALUE_ODATA_METADATA_FULL);
public static final ContentType APPLICATION_XML = new ContentType(APPLICATION, "xml", null);
public static final ContentType APPLICATION_ATOM_XML = new ContentType(APPLICATION, "atom+xml", null);
public static final ContentType APPLICATION_ATOM_XML_ENTRY = create(APPLICATION_ATOM_XML, "type", "entry");
public static final ContentType APPLICATION_ATOM_XML_ENTRY_UTF8 = create(APPLICATION_ATOM_XML_ENTRY,
PARAMETER_CHARSET, "utf-8");
public static final ContentType APPLICATION_ATOM_XML_FEED = create(APPLICATION_ATOM_XML, "type", "feed");
public static final ContentType APPLICATION_ATOM_XML_FEED_UTF8 = create(APPLICATION_ATOM_XML_FEED,
PARAMETER_CHARSET, "utf-8");
public static final ContentType APPLICATION_ATOM_SVC = new ContentType(APPLICATION, "atomsvc+xml", null);
public static final ContentType APPLICATION_OCTET_STREAM = new ContentType(APPLICATION, "octet-stream", null);
public static final ContentType APPLICATION_XHTML_XML = new ContentType(APPLICATION, "xhtml+xml", null);
public static final ContentType TEXT_HTML = new ContentType(TEXT, "html", null);
public static final ContentType TEXT_XML = new ContentType(TEXT, "xml", null);
public static final ContentType TEXT_PLAIN = new ContentType(TEXT, "plain", null);
public static final ContentType APPLICATION_SVG_XML = new ContentType(APPLICATION, "svg+xml", null);
public static final ContentType APPLICATION_FORM_URLENCODED =
new ContentType(APPLICATION, "x-www-form-urlencoded", null);
public static final ContentType APPLICATION_HTTP = new ContentType(APPLICATION, "http", null);
public static final ContentType MULTIPART_MIXED = new ContentType(MULTIPART, "mixed", null);
public static final ContentType MULTIPART_FORM_DATA = new ContentType(MULTIPART, "form-data", null);
private final String type;
private final String subtype;
private final Map<String, String> parameters;
/**
* Creates a content type from type, subtype, and parameters.
* @param type type
* @param subtype subtype
* @param parameters parameters as map from names to values
*/
private ContentType(final String type, final String subtype, final Map<String, String> parameters) {
this.type = validateType(type);
this.subtype = validateType(subtype);
if (parameters == null) {
this.parameters = Collections.emptyMap();
} else {
this.parameters = TypeUtil.createParameterMap();
this.parameters.putAll(parameters);
}
}
private String validateType(final String type) throws IllegalArgumentException {
if (type == null || type.isEmpty() || "*".equals(type)) {
throw new IllegalArgumentException("Illegal type '" + type + "'.");
}
if (type.indexOf(TypeUtil.WHITESPACE_CHAR) >= 0) {
throw new IllegalArgumentException("Illegal whitespace found for type '" + type + "'.");
}
return type;
}
/**
* Creates a content type from an existing content type and an additional parameter as key-value pair.
* @param contentType an existing content type
* @param parameterName the name of the additional parameter
* @param parameterValue the value of the additional parameter
* @return a new {@link ContentType} object
*/
public static ContentType create(final ContentType contentType,
final String parameterName, final String parameterValue) throws IllegalArgumentException {
TypeUtil.validateParameterNameAndValue(parameterName, parameterValue);
ContentType type = new ContentType(contentType.type, contentType.subtype, contentType.parameters);
type.parameters.put(parameterName.toLowerCase(Locale.ROOT), parameterValue);
return type;
}
/**
* Creates a {@link ContentType} based on given input string (<code>format</code>). Supported format is
* <code>Media Type</code> format as defined in RFC 7231, chapter 3.1.1.1.
*
* @param format a string in format as defined in RFC 7231, chapter 3.1.1.1
* @return a new {@link ContentType} object
* @throws IllegalArgumentException if input string is not parseable
*/
public static ContentType create(final String format) throws IllegalArgumentException {
if (format == null) {
throw new IllegalArgumentException("Parameter format MUST NOT be NULL.");
}
List<String> typeSubtype = new ArrayList<String>();
Map<String, String> parameters = new HashMap<String, String>();
parse(format, typeSubtype, parameters);
return new ContentType(typeSubtype.get(0), typeSubtype.get(1), parameters);
}
/**
* Parses the given input string (<code>format</code>) and returns created {@link ContentType} if input was valid or
* return <code>NULL</code> if input was not parseable.
*
* For the definition of the supported format see {@link #create(String)}.
*
* @param format a string in format as defined in RFC 7231, chapter 3.1.1.1
* @return a new <code>ContentType</code> object
*/
public static ContentType parse(final String format) {
try {
return ContentType.create(format);
} catch (IllegalArgumentException e) {
return null;
}
}
private static void parse(final String format, List<String> typeSubtype, Map<String, String> parameters)
throws IllegalArgumentException {
final String[] typesAndParameters = format.split(TypeUtil.PARAMETER_SEPARATOR, 2);
final String types = typesAndParameters[0];
final String params = (typesAndParameters.length > 1 ? typesAndParameters[1] : null);
if (types.contains(TypeUtil.TYPE_SUBTYPE_SEPARATOR)) {
final String[] tokens = types.split(TypeUtil.TYPE_SUBTYPE_SEPARATOR);
if (tokens.length == 2) {
if (tokens[0] == null || tokens[0].isEmpty()) {
throw new IllegalArgumentException("No type found in format '" + format + "'.");
} else if (tokens[1] == null || tokens[1].isEmpty()) {
throw new IllegalArgumentException("No subtype found in format '" + format + "'.");
} else {
typeSubtype.add(tokens[0]);
typeSubtype.add(tokens[1]);
}
} else {
throw new IllegalArgumentException(
"Too many '" + TypeUtil.TYPE_SUBTYPE_SEPARATOR + "' in format '" + format + "'.");
}
} else {
throw new IllegalArgumentException("No separator '" + TypeUtil.TYPE_SUBTYPE_SEPARATOR
+ "' was found in format '" + format + "'.");
}
TypeUtil.parseParameters(params, parameters);
}
/**
* Uses the first MIME type from the accept header to determine the content type.
*
* @param accept The accept header content, e.g. text/html,application/xhtml+xml,application/xml, may be null
* @return The content type according to the accept header's first MIME type. Defaults to application/json if the
* accept header does not contain valid information. Never null.
*/
public static ContentType fromAcceptHeader(String accept) {
if (accept == null || accept.trim().isEmpty()) {
return JSON;
}
String acceptType = accept.split(",")[0];
if (acceptType == null || acceptType.trim().isEmpty()) {
return JSON;
}
int semicolonIndex = acceptType.indexOf(';');
String cleanedAcceptType;
if (semicolonIndex == -1) {
cleanedAcceptType = acceptType.trim();
} else {
cleanedAcceptType = acceptType.trim().substring(0, semicolonIndex).trim();
if (cleanedAcceptType.trim().isEmpty()) {
return JSON;
}
}
try {
return create(cleanedAcceptType);
} catch (Exception exception) {
return accept.contains("xml") ? APPLICATION_ATOM_XML : JSON;
}
}
/** Gets the type of this content type. */
public String getType() {
return type;
}
/** Gets the subtype of this content type. */
public String getSubtype() {
return subtype;
}
/**
* Gets the parameters of this content type.
* @return parameters of this {@link ContentType} as unmodifiable map
*/
public Map<String, String> getParameters() {
return Collections.unmodifiableMap(parameters);
}
/**
* Returns the value of a given parameter.
* If the parameter does not exist the method returns null.
* @param name the name of the parameter to get (case-insensitive)
* @return the value of the parameter or <code>null</code> if the parameter is not present
*/
public String getParameter(final String name) {
return parameters.get(name.toLowerCase(Locale.ROOT));
}
@Override
public int hashCode() {
return 1;
}
/**
* {@link ContentType}s are equal if <code>type</code>, <code>subtype</code>, and all <code>parameters</code>
* have the same value.
*/
@Override
public boolean equals(final Object obj) {
// basic checks
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final ContentType other = (ContentType) obj;
// type/subtype checks
if (!isCompatible(other)) {
return false;
}
// parameter checks
if (parameters.size() == other.parameters.size()) {
final Iterator<Entry<String, String>> entries = parameters.entrySet().iterator();
final Iterator<Entry<String, String>> otherEntries = other.parameters.entrySet().iterator();
while (entries.hasNext()) {
final Entry<String, String> e = entries.next();
final Entry<String, String> oe = otherEntries.next();
if (!areEqual(e.getKey(), oe.getKey())
|| !areEqual(e.getValue(), oe.getValue())) {
return false;
}
}
return true;
} else {
return false;
}
}
/**
* <p>{@link ContentType}s are <b>compatible</b>
* if <code>type</code> and <code>subtype</code> have the same value.</p>
* <p>The set <code>parameters</code> are <b>always</b> ignored
* (for compare with parameters see {@link #equals(Object)}).</p>
* @return <code>true</code> if both instances are compatible (see definition above), otherwise <code>false</code>.
*/
public boolean isCompatible(final ContentType other) {
return type.equalsIgnoreCase(other.type) && subtype.equalsIgnoreCase(other.subtype);
}
/**
* Checks whether both strings are equal ignoring the case of the strings.
* @param first first string
* @param second second string
* @return <code>true</code> if both strings are equal (ignoring the case), otherwise <code>false</code>
*/
private static boolean areEqual(final String first, final String second) {
return first == null && second == null || (first != null && first.equalsIgnoreCase(second));
}
/**
* Gets {@link ContentType} as string as defined in
* <a href="http://www.ietf.org/rfc/rfc7231.txt">RFC 7231</a>, chapter 3.1.1.1: Media Type.
* @return string representation of {@link ContentType} object
*/
public String toContentTypeString() {
final StringBuilder sb = new StringBuilder();
sb.append(type).append(TypeUtil.TYPE_SUBTYPE_SEPARATOR).append(subtype);
for (Entry<String, String> entry : parameters.entrySet()) {
sb.append(TypeUtil.PARAMETER_SEPARATOR).append(entry.getKey())
.append(TypeUtil.PARAMETER_KEY_VALUE_SEPARATOR).append(entry.getValue());
}
return sb.toString();
}
@Override
public String toString() {
return toContentTypeString();
}
}