// // ======================================================================== // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // and Apache License v2.0 which accompanies this distribution. // // The Eclipse Public License is available at // http://www.eclipse.org/legal/epl-v10.html // // The Apache License v2.0 is available at // http://www.opensource.org/licenses/apache2.0.php // // You may elect to redistribute this code under either of these licenses. // ======================================================================== // package org.eclipse.jetty.http; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import org.eclipse.jetty.util.ArrayTrie; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.Trie; import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Logger; /* ------------------------------------------------------------ */ /** MIME Type enum and utilities * */ public class MimeTypes { /* ------------------------------------------------------------ */ private static final Logger LOG = Log.getLogger(MimeTypes.class); private static final Trie<ByteBuffer> TYPES= new ArrayTrie<ByteBuffer>(512); private static final Map<String,String> __dftMimeMap = new HashMap<String,String>(); private static final Map<String,String> __inferredEncodings = new HashMap<String,String>(); private static final Map<String,String> __assumedEncodings = new HashMap<String,String>(); public enum Type { FORM_ENCODED("application/x-www-form-urlencoded"), MESSAGE_HTTP("message/http"), MULTIPART_BYTERANGES("multipart/byteranges"), TEXT_HTML("text/html"), TEXT_PLAIN("text/plain"), TEXT_XML("text/xml"), TEXT_JSON("text/json",StandardCharsets.UTF_8), APPLICATION_JSON("application/json",StandardCharsets.UTF_8), TEXT_HTML_8859_1("text/html;charset=iso-8859-1",TEXT_HTML), TEXT_HTML_UTF_8("text/html;charset=utf-8",TEXT_HTML), TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1",TEXT_PLAIN), TEXT_PLAIN_UTF_8("text/plain;charset=utf-8",TEXT_PLAIN), TEXT_XML_8859_1("text/xml;charset=iso-8859-1",TEXT_XML), TEXT_XML_UTF_8("text/xml;charset=utf-8",TEXT_XML), TEXT_JSON_8859_1("text/json;charset=iso-8859-1",TEXT_JSON), TEXT_JSON_UTF_8("text/json;charset=utf-8",TEXT_JSON), APPLICATION_JSON_8859_1("application/json;charset=iso-8859-1",APPLICATION_JSON), APPLICATION_JSON_UTF_8("application/json;charset=utf-8",APPLICATION_JSON); /* ------------------------------------------------------------ */ private final String _string; private final Type _base; private final ByteBuffer _buffer; private final Charset _charset; private final String _charsetString; private final boolean _assumedCharset; private final HttpField _field; /* ------------------------------------------------------------ */ Type(String s) { _string=s; _buffer=BufferUtil.toBuffer(s); _base=this; _charset=null; _charsetString=null; _assumedCharset=false; _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string); } /* ------------------------------------------------------------ */ Type(String s,Type base) { _string=s; _buffer=BufferUtil.toBuffer(s); _base=base; int i=s.indexOf(";charset="); _charset=Charset.forName(s.substring(i+9)); _charsetString=_charset.toString().toLowerCase(Locale.ENGLISH); _assumedCharset=false; _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string); } /* ------------------------------------------------------------ */ Type(String s,Charset cs) { _string=s; _base=this; _buffer=BufferUtil.toBuffer(s); _charset=cs; _charsetString=_charset==null?null:_charset.toString().toLowerCase(Locale.ENGLISH); _assumedCharset=true; _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string); } /* ------------------------------------------------------------ */ public ByteBuffer asBuffer() { return _buffer.asReadOnlyBuffer(); } /* ------------------------------------------------------------ */ public Charset getCharset() { return _charset; } /* ------------------------------------------------------------ */ public String getCharsetString() { return _charsetString; } /* ------------------------------------------------------------ */ public boolean is(String s) { return _string.equalsIgnoreCase(s); } /* ------------------------------------------------------------ */ public String asString() { return _string; } /* ------------------------------------------------------------ */ @Override public String toString() { return _string; } /* ------------------------------------------------------------ */ public boolean isCharsetAssumed() { return _assumedCharset; } /* ------------------------------------------------------------ */ public HttpField getContentTypeField() { return _field; } /* ------------------------------------------------------------ */ public Type getBaseType() { return _base; } } /* ------------------------------------------------------------ */ public static final Trie<MimeTypes.Type> CACHE= new ArrayTrie<>(512); static { for (MimeTypes.Type type : MimeTypes.Type.values()) { CACHE.put(type.toString(),type); TYPES.put(type.toString(),type.asBuffer()); int charset=type.toString().indexOf(";charset="); if (charset>0) { String alt=type.toString().replace(";charset=","; charset="); CACHE.put(alt,type); TYPES.put(alt,type.asBuffer()); } if (type.isCharsetAssumed()) __assumedEncodings.put(type.asString(),type.getCharsetString()); } String resourceName = "org/eclipse/jetty/http/mime.properties"; try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName)) { if (stream == null) { LOG.warn("Missing mime-type resource: {}", resourceName); } else { try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { Properties props = new Properties(); props.load(reader); props.stringPropertyNames().stream() .filter(x->x!=null) .forEach(x-> __dftMimeMap.put(StringUtil.asciiToLowerCase(x), normalizeMimeType(props.getProperty(x)))); if (__dftMimeMap.size()==0) { LOG.warn("Empty mime types at {}", resourceName); } else if (__dftMimeMap.size()<props.keySet().size()) { LOG.warn("Duplicate or null mime-type extension in resource: {}", resourceName); } } catch (IOException e) { LOG.warn(e.toString()); LOG.debug(e); } } } catch (IOException e) { LOG.warn(e.toString()); LOG.debug(e); } resourceName = "org/eclipse/jetty/http/encoding.properties"; try (InputStream stream = MimeTypes.class.getClassLoader().getResourceAsStream(resourceName)) { if (stream == null) LOG.warn("Missing encoding resource: {}", resourceName); else { try (InputStreamReader reader = new InputStreamReader(stream, StandardCharsets.UTF_8)) { Properties props = new Properties(); props.load(reader); props.stringPropertyNames().stream() .filter(t->t!=null) .forEach(t-> { String charset = props.getProperty(t); if (charset.startsWith("-")) __assumedEncodings.put(t, charset.substring(1)); else __inferredEncodings.put(t, props.getProperty(t)); }); if (__inferredEncodings.size()==0) { LOG.warn("Empty encodings at {}", resourceName); } else if ((__inferredEncodings.size()+__assumedEncodings.size())<props.keySet().size()) { LOG.warn("Null or duplicate encodings in resource: {}", resourceName); } } catch (IOException e) { LOG.warn(e.toString()); LOG.debug(e); } } } catch (IOException e) { LOG.warn(e.toString()); LOG.debug(e); } } /* ------------------------------------------------------------ */ private final Map<String,String> _mimeMap=new HashMap<String,String>(); /* ------------------------------------------------------------ */ /** Constructor. */ public MimeTypes() { } /* ------------------------------------------------------------ */ public synchronized Map<String,String> getMimeMap() { return _mimeMap; } /* ------------------------------------------------------------ */ /** * @param mimeMap A Map of file extension to mime-type. */ public void setMimeMap(Map<String,String> mimeMap) { _mimeMap.clear(); if (mimeMap!=null) { for (Entry<String, String> ext : mimeMap.entrySet()) _mimeMap.put(StringUtil.asciiToLowerCase(ext.getKey()),normalizeMimeType(ext.getValue())); } } /* ------------------------------------------------------------ */ /** Get the MIME type by filename extension. * Lookup only the static default mime map. * @param filename A file name * @return MIME type matching the longest dot extension of the * file name. */ public static String getDefaultMimeByExtension(String filename) { String type=null; if (filename!=null) { int i=-1; while(type==null) { i=filename.indexOf(".",i+1); if (i<0 || i>=filename.length()) break; String ext=StringUtil.asciiToLowerCase(filename.substring(i+1)); if (type==null) type=__dftMimeMap.get(ext); } } if (type==null) { if (type==null) type=__dftMimeMap.get("*"); } return type; } /* ------------------------------------------------------------ */ /** Get the MIME type by filename extension. * Lookup the content and static default mime maps. * @param filename A file name * @return MIME type matching the longest dot extension of the * file name. */ public String getMimeByExtension(String filename) { String type=null; if (filename!=null) { int i=-1; while(type==null) { i=filename.indexOf(".",i+1); if (i<0 || i>=filename.length()) break; String ext=StringUtil.asciiToLowerCase(filename.substring(i+1)); if (_mimeMap!=null) type=_mimeMap.get(ext); if (type==null) type=__dftMimeMap.get(ext); } } if (type==null) { if (_mimeMap!=null) type=_mimeMap.get("*"); if (type==null) type=__dftMimeMap.get("*"); } return type; } /* ------------------------------------------------------------ */ /** Set a mime mapping * @param extension the extension * @param type the mime type */ public void addMimeMapping(String extension,String type) { _mimeMap.put(StringUtil.asciiToLowerCase(extension),normalizeMimeType(type)); } /* ------------------------------------------------------------ */ public static Set<String> getKnownMimeTypes() { return new HashSet<>(__dftMimeMap.values()); } /* ------------------------------------------------------------ */ private static String normalizeMimeType(String type) { MimeTypes.Type t =CACHE.get(type); if (t!=null) return t.asString(); return StringUtil.asciiToLowerCase(type); } /* ------------------------------------------------------------ */ public static String getCharsetFromContentType(String value) { if (value==null) return null; int end=value.length(); int state=0; int start=0; boolean quote=false; int i=0; for (;i<end;i++) { char b = value.charAt(i); if (quote && state!=10) { if ('"'==b) quote=false; continue; } if(';'==b && state<=8) { state = 1; continue; } switch(state) { case 0: if ('"'==b) { quote=true; break; } break; case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break; case 2: if ('h'==b) state=3; else state=0;break; case 3: if ('a'==b) state=4; else state=0;break; case 4: if ('r'==b) state=5; else state=0;break; case 5: if ('s'==b) state=6; else state=0;break; case 6: if ('e'==b) state=7; else state=0;break; case 7: if ('t'==b) state=8; else state=0;break; case 8: if ('='==b) state=9; else if (' '!=b) state=0; break; case 9: if (' '==b) break; if ('"'==b) { quote=true; start=i+1; state=10; break; } start=i; state=10; break; case 10: if (!quote && (';'==b || ' '==b )|| (quote && '"'==b )) return StringUtil.normalizeCharset(value,start,i-start); } } if (state==10) return StringUtil.normalizeCharset(value,start,i-start); return null; } /** * Access a mutable map of mime type to the charset inferred from that content type. * An inferred encoding is used by when encoding/decoding a stream and is * explicitly set in any metadata (eg Content-Type). * @return Map of mime type to charset */ public static Map<String,String> getInferredEncodings() { return __inferredEncodings; } /** * Access a mutable map of mime type to the charset assumed for that content type. * An assumed encoding is used by when encoding/decoding a stream, but is not * explicitly set in any metadata (eg Content-Type). * @return Map of mime type to charset */ public static Map<String,String> getAssumedEncodings() { return __inferredEncodings; } @Deprecated public static String inferCharsetFromContentType(String contentType) { return getCharsetAssumedFromContentType(contentType); } public static String getCharsetInferredFromContentType(String contentType) { return __inferredEncodings.get(contentType); } public static String getCharsetAssumedFromContentType(String contentType) { return __assumedEncodings.get(contentType); } public static String getContentTypeWithoutCharset(String value) { int end=value.length(); int state=0; int start=0; boolean quote=false; int i=0; StringBuilder builder=null; for (;i<end;i++) { char b = value.charAt(i); if ('"'==b) { if (quote) { quote=false; } else { quote=true; } switch(state) { case 11: builder.append(b);break; case 10: break; case 9: builder=new StringBuilder(); builder.append(value,0,start+1); state=10; break; default: start=i; state=0; } continue; } if (quote) { if (builder!=null && state!=10) builder.append(b); continue; } switch(state) { case 0: if (';'==b) state=1; else if (' '!=b) start=i; break; case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break; case 2: if ('h'==b) state=3; else state=0;break; case 3: if ('a'==b) state=4; else state=0;break; case 4: if ('r'==b) state=5; else state=0;break; case 5: if ('s'==b) state=6; else state=0;break; case 6: if ('e'==b) state=7; else state=0;break; case 7: if ('t'==b) state=8; else state=0;break; case 8: if ('='==b) state=9; else if (' '!=b) state=0; break; case 9: if (' '==b) break; builder=new StringBuilder(); builder.append(value,0,start+1); state=10; break; case 10: if (';'==b) { builder.append(b); state=11; } break; case 11: if (' '!=b) builder.append(b); } } if (builder==null) return value; return builder.toString(); } }