/** * Jooby https://jooby.io * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ package io.jooby; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.io.File; import java.nio.charset.Charset; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.function.BinaryOperator; import static java.nio.charset.StandardCharsets.UTF_8; /** * Implementation of media/content type. * * @since 2.0.0 */ public final class MediaType implements Comparable { /** * Computes and returns the most specific media type of both. */ public static final BinaryOperator MOST_SPECIFIC = (a, b) -> { int aScore = a.getScore(); int bScore = b.getScore(); return aScore >= bScore ? a : b; }; /** APPLICATION_JSON. */ public static final String JSON = "application/json"; /** APPLICATION_XML. */ public static final String XML = "application/xml"; /** TEXT_PLAIN. */ public static final String TEXT = "text/plain"; /** TEXT_HTML. */ public static final String HTML = "text/html"; /** APPLICATION_JAVASCRIPT. */ public static final String JS = "application/javascript"; /** TEXT_CSS. */ public static final String CSS = "text/css"; /** APPLICATION_OCTET_STREAM. */ public static final String OCTET_STREAM = "application/octet-stream"; /** FORM_URLENCODED. */ public static final String FORM_URLENCODED = "application/x-www-form-urlencoded"; /** MULTIPART_FORM_DATA. */ public static final String MULTIPART_FORMDATA = "multipart/form-data"; /** YAML. */ public static final String YAML = "text/yaml"; /** ALL. */ public static final String ALL = "*/*"; /** APPLICATION_JSON. */ public static final MediaType json = new MediaType(JSON, UTF_8); /** YAML. */ public static final MediaType yaml = new MediaType(YAML, UTF_8); /** APPLICATION_XML. */ public static final MediaType xml = new MediaType(XML, UTF_8); /** TEXT_PLAIN. */ public static final MediaType text = new MediaType(TEXT, UTF_8); /** TEXT_HTML. */ public static final MediaType html = new MediaType(HTML, UTF_8); /** APPLICATION_JSON. */ public static final MediaType js = new MediaType(JS, UTF_8); /** TEXT_CSS. */ public static final MediaType css = new MediaType(CSS, UTF_8); /** APPLICATION_OCTET_STREAM. */ public static final MediaType octetStream = new MediaType(OCTET_STREAM, null); /** FORM_URLENCODED. */ public static final MediaType form = new MediaType(FORM_URLENCODED, UTF_8); /** MULTIPART_FORM_DATA. */ public static final MediaType multipart = new MediaType(MULTIPART_FORMDATA, UTF_8); /** ALL. */ public static final MediaType all = new MediaType(ALL, null); private final String raw; private final Charset charset; private final int subtypeStart; private final int subtypeEnd; private final String value; private MediaType(@Nonnull String value, Charset charset) { this.raw = value; this.subtypeStart = value.indexOf('/'); if (subtypeStart < 0) { throw new IllegalArgumentException("Invalid media type: " + value); } int subtypeEnd = value.indexOf(';'); if (subtypeEnd < 0) { this.value = raw; this.subtypeEnd = value.length(); } else { this.value = raw.substring(0, subtypeEnd); this.subtypeEnd = subtypeEnd; } this.charset = charset; } @Override public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof MediaType) { MediaType that = (MediaType) obj; return getType().equals(that.getType()) && getSubtype().equals(that.getSubtype()); } return false; } @Override public int hashCode() { return value.hashCode(); } /** * Get a parameter that matches the given name or null. * * @param name Parameter name. * @return Parameter value or null. */ public @Nullable String getParameter(@Nonnull String name) { int paramStart = subtypeEnd + 1; for (int i = subtypeEnd; i < raw.length(); i++) { char ch = raw.charAt(i); if (ch == '=') { String pname = raw.substring(paramStart, i).trim(); int paramValueEnd = raw.indexOf(';', i); if (paramValueEnd < 0) { paramValueEnd = raw.length(); } if (pname.equals(name)) { return raw.substring(i + 1, paramValueEnd).trim(); } paramStart = paramValueEnd + 1; i = paramStart; } } return null; } /** * Media type value without parameters. * * @return Media type value. */ public @Nonnull String getValue() { return value; } /** * Render a content type header and add the charset parameter (when present). * * @param charset Charset. * @return Content type header. */ public @Nonnull String toContentTypeHeader(@Nullable Charset charset) { if (charset == null) { Charset paramCharset = getCharset(); if (paramCharset == null) { return value; } charset = paramCharset; } return value + ";charset=" + charset.name(); } /** * Value of q parameter. * * @return Value of q parameter. */ @Nonnull public float getQuality() { String q = getParameter("q"); return q == null ? 1f : Float.parseFloat(q); } @Override public int compareTo(MediaType other) { if (this == other) { return 0; } int diff = other.getScore() - getScore(); if (diff == 0) { diff = Float.compare(other.getQuality(), getQuality()); if (diff == 0) { diff = other.getParameterCount() - getParameterCount(); } } return diff; } /** * Indicates whenever this is a textual mediatype. * @return True for textual mediatype. */ public boolean isTextual() { if (getType().equals("text")) { return true; } String subtype = getSubtype(); return subtype.endsWith("json") || subtype.endsWith("javascript") || subtype.endsWith("xml") || subtype.endsWith("yaml"); } /** * Indicates whenever this is a json mediatype. * @return True for json mediatype. */ public boolean isJson() { String subtype = getSubtype(); return subtype.equals("json") || subtype.endsWith("+json"); } /** * Charset or null. * * @return Charset or null. */ public @Nullable Charset getCharset() { Charset charset = charset(this.charset); if (charset == null && isTextual()) { return UTF_8; } return charset; } private Charset charset(Charset charset) { String charsetName = getParameter("charset"); return charsetName == null ? charset : Charset.forName(charsetName); } /** * Type segment of mediatype (leading type). * * @return Type segment of mediatype (leading type). */ public @Nonnull String getType() { return raw.substring(0, subtypeStart).trim(); } /** * Subtype segment of mediatype (trailing type). * * @return Subtype segment of mediatype (trailing type). */ public @Nonnull String getSubtype() { return raw.substring(subtypeStart + 1, subtypeEnd).trim(); } /** * True if this mediatype is compatible with the given content type. * * @param mediaType Media type to test. * @return True if this mediatype is compatible with the given content type. */ public boolean matches(@Nonnull String mediaType) { return matches(value, mediaType); } /** * True if this mediatype is compatible with the given content type. * * @param type Media type to test. * @return True if this mediatype is compatible with the given content type. */ public boolean matches(@Nonnull MediaType type) { return matches(value, type.value); } private int getScore() { int precendence = 0; if (!getType().equals("*")) { precendence += 1; } if (!getSubtype().equals("*")) { precendence += 1; } return precendence; } private int getParameterCount() { int p = 0; for (int i = subtypeEnd; i < raw.length(); i++) { char ch = raw.charAt(i); if (ch == '=') { p += 1; } } return p; } /** * Parse a string value into a media-type. * * @param value String media-type. * @return Media type. */ public static @Nonnull MediaType valueOf(@Nonnull String value) { if (value == null || value.length() == 0 || value.equals("*")) { return all; } if (HTML.equalsIgnoreCase(value) || "html".equals(value)) { return html; } if (TEXT.equalsIgnoreCase(value) || "text".equals(value)) { return text; } if (JSON.equalsIgnoreCase(value) || "json".equals(value)) { return json; } if (JS.equalsIgnoreCase(value) || "js".equals(value) || "javascript".equals(value)) { return js; } if (CSS.equalsIgnoreCase(value) || "css".equals(value)) { return css; } if (FORM_URLENCODED.equalsIgnoreCase(value) || "form".equals(value)) { return form; } if (MULTIPART_FORMDATA.equalsIgnoreCase(value) || "multipart".equals(value)) { return multipart; } if (OCTET_STREAM.equalsIgnoreCase(value) || "octetStream".equals(value)) { return octetStream; } if (XML.equalsIgnoreCase(value) || "xml".equals(value)) { return xml; } if (YAML.equalsIgnoreCase(value) || "yaml".equals(value)) { return yaml; } return new MediaType(value, null); } /** * Parse one or more mediatype values. Mediatype must be separated by comma ,. * * @param value Mediatype comma separated value. * @return One or more mediatypes. */ public static @Nonnull List parse(@Nullable String value) { if (value == null || value.length() == 0) { return Collections.emptyList(); } List result = new ArrayList<>(3); int typeStart = 0; int len = value.length(); for (int i = 0; i < len; i++) { char ch = value.charAt(i); if (ch == ',') { result.add(valueOf(value.substring(typeStart, i).trim())); typeStart = i + 1; } } if (typeStart == 0) { result.add(valueOf(value)); } else if (typeStart < len) { result.add(valueOf(value.substring(typeStart, len).trim())); } return result; } static boolean matches(@Nonnull String expected, @Nonnull String contentType) { int start = 0; int len1 = expected.length(); int end = contentType.indexOf(','); while (end != -1) { if (matchOne(expected, len1, contentType.substring(start, end).trim())) { return true; } start = end + 1; end = contentType.indexOf(',', start); } int clen = contentType.length(); if (start < clen) { return matchOne(expected, len1, contentType.substring(start, clen).trim()); } return false; } /** * Mediatype by file extension. * * @param file File. * @return Mediatype. */ public static @Nonnull MediaType byFile(@Nonnull File file) { return byFile(file.getName()); } /** * Mediatype by file extension. * * @param file File. * @return Mediatype. */ public static @Nonnull MediaType byFile(@Nonnull Path file) { return byFile(file.getFileName().toString()); } /** * Mediatype by file extension. * * @param filename File. * @return Mediatype. */ public static @Nonnull MediaType byFile(@Nonnull String filename) { int index = filename.lastIndexOf('.'); return index > 0 ? byFileExtension(filename.substring(index + 1)) : octetStream; } /** * Mediatype by file extension. * * @param ext File extension. * @return Mediatype. */ public static @Nonnull MediaType byFileExtension(@Nonnull String ext) { switch (ext) { case "spl": return new MediaType("application/x-futuresplash", null); case "java": return text; case "class": return new MediaType("application/java-vm", null); case "cpt": return new MediaType("application/mac-compactpro", null); case "etx": return new MediaType("text/x-setext", null); case "tar": return new MediaType("application/x-tar", null); case "js": return js; case "ogg": return new MediaType("application/ogg", null); case "xyz": return new MediaType("chemical/x-xyz", null); case "msh": return new MediaType("model/mesh", null); case "ustar": return new MediaType("application/x-ustar", null); case "msi": return octetStream; case "xht": return new MediaType("application/xhtml+xml", UTF_8); case "bmp": return new MediaType("image/bmp", null); case "silo": return new MediaType("model/mesh", null); case "sv4crc": return new MediaType("application/x-sv4crc", null); case "man": return new MediaType("application/x-troff-man", null); case "map": return text; case "cpio": return new MediaType("application/x-cpio", null); case "snd": return new MediaType("audio/basic", null); case "iges": return new MediaType("model/iges", null); case "smi": return new MediaType("application/smil", null); case "bcpio": return new MediaType("application/x-bcpio", null); case "pgm": return new MediaType("image/x-portable-graymap", null); case "pgn": return new MediaType("application/x-chess-pgn", null); case "vcd": return new MediaType("application/x-cdlink", null); case "aif": return new MediaType("audio/x-aiff", null); case "ods": return new MediaType("application/vnd.oasis.opendocument.spreadsheet", null); case "odt": return new MediaType("application/vnd.oasis.opendocument.text", null); case "odp": return new MediaType("application/vnd.oasis.opendocument.presentation", null); case "jpeg": return new MediaType("image/jpeg", null); case "xwd": return new MediaType("image/x-xwindowdump", null); case "odc": return new MediaType("application/vnd.oasis.opendocument.chart", null); case "ots": return new MediaType("application/vnd.oasis.opendocument.spreadsheet-template", null); case "ott": return new MediaType("application/vnd.oasis.opendocument.text-template", null); case "odf": return new MediaType("application/vnd.oasis.opendocument.formula", null); case "otp": return new MediaType("application/vnd.oasis.opendocument.presentation-template", null); case "oda": return new MediaType("application/oda", null); case "odb": return new MediaType("application/vnd.oasis.opendocument.database", null); case "less": return css; case "doc": return new MediaType("application/msword", null); case "odm": return new MediaType("application/vnd.oasis.opendocument.text-master", null); case "odg": return new MediaType("application/vnd.oasis.opendocument.graphics", null); case "woff": return new MediaType("application/x-font-woff", null); case "odi": return new MediaType("application/vnd.oasis.opendocument.image", null); case "otc": return new MediaType("application/vnd.oasis.opendocument.chart-template", null); case "otf": return new MediaType("font/opentype", null); case "zip": return new MediaType("application/zip", null); case "skt": return new MediaType("application/x-koan", null); case "eps": return new MediaType("application/postscript", null); case "mpe": return new MediaType("video/mpeg", null); case "otg": return new MediaType("application/vnd.oasis.opendocument.graphics-template", null); case "oth": return new MediaType("application/vnd.oasis.opendocument.text-web", null); case "oti": return new MediaType("application/vnd.oasis.opendocument.image-template", null); case "mpg": return new MediaType("video/mpeg", null); case "ps": return new MediaType("application/postscript", null); case "xul": return new MediaType("application/vnd.mozilla.xul+xml", UTF_8); case "xslt": return new MediaType("application/xslt+xml", UTF_8); case "dms": return octetStream; case "mol": return new MediaType("chemical/x-mdl-molfile", null); case "eot": return new MediaType("application/vnd.ms-fontobject", null); case "skd": return new MediaType("application/x-koan", null); case "wmlsc": return new MediaType("application/vnd.wap.wmlscriptc", null); case "roff": return new MediaType("application/x-troff", null); case "skp": return new MediaType("application/x-koan", null); case "mpga": return new MediaType("audio/mpeg", null); case "mov": return new MediaType("video/quicktime", null); case "igs": return new MediaType("model/iges", null); case "skm": return new MediaType("application/x-koan", null); case "sv4cpio": return new MediaType("application/x-sv4cpio", null); case "wbmp": return new MediaType("image/vnd.wap.wbmp", null); case "bin": return new MediaType("application/octet-stream", null); case "z": return new MediaType("application/compress", null); case "html": return html; case "gtar": return new MediaType("application/x-gtar", null); case "pdb": return new MediaType("chemical/x-pdb", null); case "t": return new MediaType("application/x-troff", null); case "mp2": return new MediaType("audio/mpeg", null); case "mp3": return new MediaType("audio/mpeg", null); case "ms": return new MediaType("application/x-troff-ms", null); case "wrl": return new MediaType("model/vrml", null); case "mp4": return new MediaType("video/mp4", null); case "vxml": return new MediaType("application/voicexml+xml", UTF_8); case "mathml": return new MediaType("application/mathml+xml", UTF_8); case "hdf": return new MediaType("application/x-hdf", null); case "wav": return new MediaType("audio/x-wav", null); case "pdf": return new MediaType("application/pdf", null); case "nc": return new MediaType("application/x-netcdf", null); case "sit": return new MediaType("application/x-stuffit", null); case "htm": return html; case "jnlp": return new MediaType("application/x-java-jnlp-file", null); case "dll": return new MediaType("application/x-msdownload", null); case "xsl": return xml; case "ief": return new MediaType("image/ief", null); case "rgb": return new MediaType("image/x-rgb", null); case "htc": return new MediaType("text/x-component", null); case "avi": return new MediaType("video/x-msvideo", null); case "me": return new MediaType("application/x-troff-me", null); case "tiff": return new MediaType("image/tiff", null); case "pbm": return new MediaType("image/x-portable-bitmap", null); case "xsd": return xml; case "mesh": return new MediaType("model/mesh", null); case "xbm": return new MediaType("image/x-xbitmap", null); case "midi": return new MediaType("audio/midi", null); case "texi": return new MediaType("application/x-texinfo", null); case "conf": return new MediaType("application/hocon", UTF_8); case "lzh": return new MediaType("application/octet-stream", null); case "tr": return new MediaType("application/x-troff", null); case "ts": return js; case "hqx": return new MediaType("application/mac-binhex40", null); case "tif": return new MediaType("image/tiff", null); case "ice": return new MediaType("x-conference/x-cooltalk", null); case "dir": return new MediaType("application/x-director", null); case "sgm": return new MediaType("text/sgml", null); case "woff2": return new MediaType("application/font-woff2", null); case "sh": return new MediaType("application/x-sh", null); case "ico": return new MediaType("image/x-icon", null); case "asx": return new MediaType("video/x.ms.asx", null); case "swf": return new MediaType("application/x-shockwave-flash", null); case "texinfo": return new MediaType("application/x-texinfo", null); case "ai": return new MediaType("application/postscript", null); case "txt": return text; case "asc": return text; case "ppm": return new MediaType("image/x-portable-pixmap", null); case "rtx": return new MediaType("text/richtext", UTF_8); case "movie": return new MediaType("video/x-sgi-movie", null); case "ra": return new MediaType("audio/x-pn-realaudio", null); case "vrml": return new MediaType("model/vrml", null); case "au": return new MediaType("audio/basic", null); case "gzip": return new MediaType("application/gzip", null); case "pps": return new MediaType("application/vnd.ms-powerpoint", null); case "rdf": return new MediaType("application/rdf+xml", UTF_8); case "ppt": return new MediaType("application/vnd.ms-powerpoint", null); case "asf": return new MediaType("video/x.ms.asf", null); case "xpm": return new MediaType("image/x-xpixmap", null); case "dxr": return new MediaType("application/x-director", null); case "ser": return new MediaType("application/java-serialized-object", null); case "rm": return new MediaType("audio/x-pn-realaudio", null); case "tgz": return new MediaType("application/x-gtar", null); case "rv": return new MediaType("video/vnd.rn-realvideo", null); case "shar": return new MediaType("application/x-shar", null); case "rtf": return new MediaType("application/rtf", null); case "svg": return new MediaType("image/svg+xml", null); case "lha": return new MediaType("application/octet-stream", null); case "mif": return new MediaType("application/vnd.mif", null); case "mpeg": return new MediaType("video/mpeg", null); case "wml": return new MediaType("text/vnd.wap.wml", null); case "jsp": return html; case "mid": return new MediaType("audio/midi", null); case "qt": return new MediaType("video/quicktime", null); case "yaml": case "yml": return yaml; case "pnm": return new MediaType("image/x-portable-anymap", null); case "tar.gz": return new MediaType("application/x-gtar", null); case "gz": return new MediaType("application/gzip", null); case "ram": return new MediaType("audio/x-pn-realaudio", null); case "jar": return new MediaType("application/java-archive", null); case "apk": return new MediaType("application/vnd.android.package-archive", null); case "tex": return new MediaType("application/x-tex", null); case "png": return new MediaType("image/png", null); case "ras": return new MediaType("image/x-cmu-raster", null); case "cdf": return new MediaType("application/x-netcdf", null); case "jad": return new MediaType("text/vnd.sun.j2me.app-descriptor", null); case "dvi": return new MediaType("application/x-dvi", null); case "xml": return xml; case "exe": return octetStream; case "xls": return new MediaType("application/vnd.ms-excel", null); case "scss": return css; case "csv": return new MediaType("text/comma-separated-values", UTF_8); case "css": return css; case "xhtml": return new MediaType("application/xhtml+xml", UTF_8); case "rpm": return new MediaType("application/x-rpm", null); case "wtls-ca-certificate": return new MediaType("application/vnd.wap.wtls-ca-certificate", null); case "wmls": return new MediaType("text/vnd.wap.wmlscript", null); case "csh": return new MediaType("application/x-csh", null); case "aifc": return new MediaType("audio/x-aiff", null); case "ez": return new MediaType("application/andrew-inset", null); case "jpe": return new MediaType("image/jpeg", null); case "jpg": return new MediaType("image/jpeg", null); case "coffee": return js; case "kar": return new MediaType("audio/midi", null); case "tcl": return new MediaType("application/x-tcl", null); case "wmlc": return new MediaType("application/vnd.wap.wmlc", null); case "ttf": return new MediaType("font/truetype", null); case "src": return new MediaType("application/x-wais-source", null); case "crt": return new MediaType("application/x-x509-ca-cert", null); case "qml": return new MediaType("text/x-qml", null); case "tsv": return new MediaType("text/tab-separated-values", null); case "smil": return new MediaType("application/smil", null); case "dcr": return new MediaType("application/x-director", null); case "dtd": return new MediaType("application/xml-dtd", null); case "sgml": return new MediaType("text/sgml", null); case "latex": return new MediaType("application/x-latex", null); case "aiff": return new MediaType("audio/x-aiff", null); case "json": return json; case "cab": return new MediaType("application/x-cabinet", null); case "gif": return new MediaType("image/gif", null); default: return octetStream; } } private static boolean matchOne(String expected, int len1, String contentType) { if (contentType.startsWith("*/*") || contentType.equals("*")) { return true; } int i = 0; int len2 = contentType.length(); int len = Math.min(len1, len2); while (i < len) { char ch1 = expected.charAt(i); char ch2 = contentType.charAt(i); if (ch1 != ch2) { if (i > 0) { char prev = expected.charAt(i - 1); if (prev == '/') { if (ch1 == '*') { if (i == len1 - 1) { return true; } // tail/suffix matches for (int j = len1 - 1, k = len2 - 1; j > i; j--, k--) { if (expected.charAt(j) != contentType.charAt(k)) { return false; } } return true; } else { return false; } } else { return false; } } else { return false; } } i += 1; } return i == len && len1 == len2; } @Override public String toString() { return raw; } }