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