View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.http;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InputStreamReader;
24  import java.net.URL;
25  import java.nio.ByteBuffer;
26  import java.nio.charset.Charset;
27  import java.nio.charset.StandardCharsets;
28  import java.util.HashMap;
29  import java.util.HashSet;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Map.Entry;
33  import java.util.Properties;
34  import java.util.Set;
35  
36  import org.eclipse.jetty.util.ArrayTrie;
37  import org.eclipse.jetty.util.BufferUtil;
38  import org.eclipse.jetty.util.Loader;
39  import org.eclipse.jetty.util.StringUtil;
40  import org.eclipse.jetty.util.Trie;
41  import org.eclipse.jetty.util.log.Log;
42  import org.eclipse.jetty.util.log.Logger;
43  
44  
45  /* ------------------------------------------------------------ */
46  /**
47   *
48   */
49  public class MimeTypes
50  {
51      public enum Type
52      {
53          FORM_ENCODED("application/x-www-form-urlencoded"),
54          MESSAGE_HTTP("message/http"),
55          MULTIPART_BYTERANGES("multipart/byteranges"),
56  
57          TEXT_HTML("text/html"),
58          TEXT_PLAIN("text/plain"),
59          TEXT_XML("text/xml"),
60          TEXT_JSON("text/json",StandardCharsets.UTF_8),
61          APPLICATION_JSON("application/json",StandardCharsets.UTF_8),
62  
63          TEXT_HTML_8859_1("text/html;charset=iso-8859-1",TEXT_HTML),
64          TEXT_HTML_UTF_8("text/html;charset=utf-8",TEXT_HTML),
65  
66          TEXT_PLAIN_8859_1("text/plain;charset=iso-8859-1",TEXT_PLAIN),
67          TEXT_PLAIN_UTF_8("text/plain;charset=utf-8",TEXT_PLAIN),
68  
69          TEXT_XML_8859_1("text/xml;charset=iso-8859-1",TEXT_XML),
70          TEXT_XML_UTF_8("text/xml;charset=utf-8",TEXT_XML),
71  
72          TEXT_JSON_8859_1("text/json;charset=iso-8859-1",TEXT_JSON),
73          TEXT_JSON_UTF_8("text/json;charset=utf-8",TEXT_JSON),
74  
75          APPLICATION_JSON_8859_1("text/json;charset=iso-8859-1",APPLICATION_JSON),
76          APPLICATION_JSON_UTF_8("text/json;charset=utf-8",APPLICATION_JSON);
77  
78  
79          /* ------------------------------------------------------------ */
80          private final String _string;
81          private final Type _base;
82          private final ByteBuffer _buffer;
83          private final Charset _charset;
84          private final String _charsetString;
85          private final boolean _assumedCharset;
86          private final HttpField _field;
87  
88          /* ------------------------------------------------------------ */
89          Type(String s)
90          {
91              _string=s;
92              _buffer=BufferUtil.toBuffer(s);
93              _base=this;
94              _charset=null;
95              _charsetString=null;
96              _assumedCharset=false;
97              _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
98          }
99  
100         /* ------------------------------------------------------------ */
101         Type(String s,Type base)
102         {
103             _string=s;
104             _buffer=BufferUtil.toBuffer(s);
105             _base=base;
106             int i=s.indexOf(";charset=");
107             _charset=Charset.forName(s.substring(i+9));
108             _charsetString=_charset.toString().toLowerCase(Locale.ENGLISH);
109             _assumedCharset=false;
110             _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
111         }
112 
113         /* ------------------------------------------------------------ */
114         Type(String s,Charset cs)
115         {
116             _string=s;
117             _base=this;
118             _buffer=BufferUtil.toBuffer(s);
119             _charset=cs;
120             _charsetString=_charset==null?null:_charset.toString().toLowerCase(Locale.ENGLISH);
121             _assumedCharset=true;
122             _field=new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,_string);
123         }
124 
125         /* ------------------------------------------------------------ */
126         public ByteBuffer asBuffer()
127         {
128             return _buffer.asReadOnlyBuffer();
129         }
130 
131         /* ------------------------------------------------------------ */
132         public Charset getCharset()
133         {
134             return _charset;
135         }
136 
137         /* ------------------------------------------------------------ */
138         public String getCharsetString()
139         {
140             return _charsetString;
141         }
142 
143         /* ------------------------------------------------------------ */
144         public boolean is(String s)
145         {
146             return _string.equalsIgnoreCase(s);
147         }
148 
149         /* ------------------------------------------------------------ */
150         public String asString()
151         {
152             return _string;
153         }
154 
155         /* ------------------------------------------------------------ */
156         @Override
157         public String toString()
158         {
159             return _string;
160         }
161 
162         /* ------------------------------------------------------------ */
163         public boolean isCharsetAssumed()
164         {
165             return _assumedCharset;
166         }
167 
168         /* ------------------------------------------------------------ */
169         public HttpField getContentTypeField()
170         {
171             return _field;
172         }
173 
174         /* ------------------------------------------------------------ */
175         public Type getBaseType()
176         {
177             return _base;
178         }
179     }
180 
181     /* ------------------------------------------------------------ */
182     private static final Logger LOG = Log.getLogger(MimeTypes.class);
183     public  final static Trie<MimeTypes.Type> CACHE= new ArrayTrie<>(512);
184     private final static Trie<ByteBuffer> TYPES= new ArrayTrie<ByteBuffer>(512);
185     private final static Map<String,String> __dftMimeMap = new HashMap<String,String>();
186     private final static Map<String,String> __encodings = new HashMap<String,String>();
187 
188     static
189     {
190         for (MimeTypes.Type type : MimeTypes.Type.values())
191         {
192             CACHE.put(type.toString(),type);
193             TYPES.put(type.toString(),type.asBuffer());
194 
195             int charset=type.toString().indexOf(";charset=");
196             if (charset>0)
197             {
198                 String alt=type.toString().replace(";charset=","; charset=");
199                 CACHE.put(alt,type);
200                 TYPES.put(alt,type.asBuffer());
201             }
202         }
203 
204         try
205         {
206             String resourceName = "org/eclipse/jetty/http/mime.properties";
207             URL mimeTypesUrl = Loader.getResource(__dftMimeMap.getClass(), resourceName);
208             if (mimeTypesUrl == null)
209             {
210                 LOG.warn("Missing mime-type resource: {}", resourceName);
211             }
212             else
213             {
214                 try (InputStream in = mimeTypesUrl.openStream();
215                      InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8))
216                 {
217                     Properties mime = new Properties();
218                     mime.load(reader);
219                     mime.stringPropertyNames().stream()
220                     .filter(x->x!=null)
221                     .forEach(x->
222                     __dftMimeMap.put(StringUtil.asciiToLowerCase(x), normalizeMimeType(mime.getProperty(x))));
223                     
224                     if (__dftMimeMap.size()<mime.size())
225                     {
226                         LOG.warn("Encountered duplicate or null mime-type extension in resource: {}", mimeTypesUrl);
227                     }
228                 }
229                 if (__dftMimeMap.size()==0)
230                 {
231                     LOG.warn("Empty mime types declaration at {}", mimeTypesUrl);
232                 }
233             }
234         }
235         catch(IOException e)
236         {
237             LOG.warn(e.toString());
238             LOG.debug(e);
239         }
240 
241         try
242         {
243             String resourceName = "org/eclipse/jetty/http/encoding.properties";
244             URL mimeTypesUrl = Loader.getResource(__dftMimeMap.getClass(), resourceName);
245             if (mimeTypesUrl == null)
246             {
247                 LOG.warn("Missing mime-type resource: {}", resourceName);
248             }
249             else
250             {
251                 try (InputStream in = mimeTypesUrl.openStream();
252                      InputStreamReader reader = new InputStreamReader(in, StandardCharsets.UTF_8))
253                 {
254                     Properties encoding = new Properties();
255                     encoding.load(reader);
256                     
257                     encoding.stringPropertyNames().stream()
258                     .filter(t->t!=null)
259                     .forEach(t->__encodings.put(t, encoding.getProperty(t)));
260 
261                     if (__encodings.size()<encoding.size())
262                     {
263                         LOG.warn("Encountered null or duplicate encoding type in resource: {}", mimeTypesUrl);
264                     }
265                 }
266 
267                 if (__encodings.size()==0)
268                 {
269                     LOG.warn("Empty mime types declaration at {}", mimeTypesUrl);
270                 }
271             }
272         }
273         catch(IOException e)
274         {
275             LOG.warn(e.toString());
276             LOG.debug(e);
277         }
278     }
279 
280 
281     /* ------------------------------------------------------------ */
282     private final Map<String,String> _mimeMap=new HashMap<String,String>();
283 
284     /* ------------------------------------------------------------ */
285     /** Constructor.
286      */
287     public MimeTypes()
288     {
289     }
290 
291     /* ------------------------------------------------------------ */
292     public synchronized Map<String,String> getMimeMap()
293     {
294         return _mimeMap;
295     }
296 
297     /* ------------------------------------------------------------ */
298     /**
299      * @param mimeMap A Map of file extension to mime-type.
300      */
301     public void setMimeMap(Map<String,String> mimeMap)
302     {
303         _mimeMap.clear();
304         if (mimeMap!=null)
305         {
306             for (Entry<String, String> ext : mimeMap.entrySet())
307                 _mimeMap.put(StringUtil.asciiToLowerCase(ext.getKey()),normalizeMimeType(ext.getValue()));
308         }
309     }
310 
311     /* ------------------------------------------------------------ */
312     /** Get the MIME type by filename extension.
313      * @param filename A file name
314      * @return MIME type matching the longest dot extension of the
315      * file name.
316      */
317     public String getMimeByExtension(String filename)
318     {
319         String type=null;
320 
321         if (filename!=null)
322         {
323             int i=-1;
324             while(type==null)
325             {
326                 i=filename.indexOf(".",i+1);
327 
328                 if (i<0 || i>=filename.length())
329                     break;
330 
331                 String ext=StringUtil.asciiToLowerCase(filename.substring(i+1));
332                 if (_mimeMap!=null)
333                     type=_mimeMap.get(ext);
334                 if (type==null)
335                     type=__dftMimeMap.get(ext);
336             }
337         }
338 
339         if (type==null)
340         {
341             if (_mimeMap!=null)
342                 type=_mimeMap.get("*");
343             if (type==null)
344                 type=__dftMimeMap.get("*");
345         }
346 
347         return type;
348     }
349 
350     /* ------------------------------------------------------------ */
351     /** Set a mime mapping
352      * @param extension the extension
353      * @param type the mime type
354      */
355     public void addMimeMapping(String extension,String type)
356     {
357         _mimeMap.put(StringUtil.asciiToLowerCase(extension),normalizeMimeType(type));
358     }
359 
360     /* ------------------------------------------------------------ */
361     public static Set<String> getKnownMimeTypes()
362     {
363         return new HashSet<>(__dftMimeMap.values());
364     }
365 
366     /* ------------------------------------------------------------ */
367     private static String normalizeMimeType(String type)
368     {
369         MimeTypes.Type t =CACHE.get(type);
370         if (t!=null)
371             return t.asString();
372 
373         return StringUtil.asciiToLowerCase(type);
374     }
375 
376     /* ------------------------------------------------------------ */
377     public static String getCharsetFromContentType(String value)
378     {
379         if (value==null)
380             return null;
381         int end=value.length();
382         int state=0;
383         int start=0;
384         boolean quote=false;
385         int i=0;
386         for (;i<end;i++)
387         {
388             char b = value.charAt(i);
389 
390             if (quote && state!=10)
391             {
392                 if ('"'==b)
393                     quote=false;
394                 continue;
395             }
396 
397             switch(state)
398             {
399                 case 0:
400                     if ('"'==b)
401                     {
402                         quote=true;
403                         break;
404                     }
405                     if (';'==b)
406                         state=1;
407                     break;
408 
409                 case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break;
410                 case 2: if ('h'==b) state=3; else state=0;break;
411                 case 3: if ('a'==b) state=4; else state=0;break;
412                 case 4: if ('r'==b) state=5; else state=0;break;
413                 case 5: if ('s'==b) state=6; else state=0;break;
414                 case 6: if ('e'==b) state=7; else state=0;break;
415                 case 7: if ('t'==b) state=8; else state=0;break;
416 
417                 case 8: if ('='==b) state=9; else if (' '!=b) state=0; break;
418 
419                 case 9:
420                     if (' '==b)
421                         break;
422                     if ('"'==b)
423                     {
424                         quote=true;
425                         start=i+1;
426                         state=10;
427                         break;
428                     }
429                     start=i;
430                     state=10;
431                     break;
432 
433                 case 10:
434                     if (!quote && (';'==b || ' '==b )||
435                             (quote && '"'==b ))
436                         return StringUtil.normalizeCharset(value,start,i-start);
437             }
438         }
439 
440         if (state==10)
441             return StringUtil.normalizeCharset(value,start,i-start);
442 
443         return null;
444     }
445 
446     public static String inferCharsetFromContentType(String value)
447     {
448         return __encodings.get(value);
449     }
450 
451     public static String getContentTypeWithoutCharset(String value)
452     {
453         int end=value.length();
454         int state=0;
455         int start=0;
456         boolean quote=false;
457         int i=0;
458         StringBuilder builder=null;
459         for (;i<end;i++)
460         {
461             char b = value.charAt(i);
462 
463             if ('"'==b)
464             {
465                 if (quote)
466                 {
467                     quote=false;
468                 }
469                 else
470                 {
471                     quote=true;
472                 }
473 
474                 switch(state)
475                 {
476                     case 11:
477                         builder.append(b);break;
478                     case 10:
479                         break;
480                     case 9:
481                         builder=new StringBuilder();
482                         builder.append(value,0,start+1);
483                         state=10;
484                         break;
485                     default:
486                         start=i;
487                         state=0;
488                 }
489                 continue;
490             }
491 
492             if (quote)
493             {
494                 if (builder!=null && state!=10)
495                     builder.append(b);
496                 continue;
497             }
498 
499             switch(state)
500             {
501                 case 0:
502                     if (';'==b)
503                         state=1;
504                     else if (' '!=b)
505                         start=i;
506                     break;
507 
508                 case 1: if ('c'==b) state=2; else if (' '!=b) state=0; break;
509                 case 2: if ('h'==b) state=3; else state=0;break;
510                 case 3: if ('a'==b) state=4; else state=0;break;
511                 case 4: if ('r'==b) state=5; else state=0;break;
512                 case 5: if ('s'==b) state=6; else state=0;break;
513                 case 6: if ('e'==b) state=7; else state=0;break;
514                 case 7: if ('t'==b) state=8; else state=0;break;
515                 case 8: if ('='==b) state=9; else if (' '!=b) state=0; break;
516 
517                 case 9:
518                     if (' '==b)
519                         break;
520                     builder=new StringBuilder();
521                     builder.append(value,0,start+1);
522                     state=10;
523                     break;
524 
525                 case 10:
526                     if (';'==b)
527                     {
528                         builder.append(b);
529                         state=11;
530                     }
531                     break;
532                 case 11:
533                     if (' '!=b)
534                         builder.append(b);
535             }
536         }
537         if (builder==null)
538             return value;
539         return builder.toString();
540 
541     }
542 }