1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
286
287 public MimeTypes()
288 {
289 }
290
291
292 public synchronized Map<String,String> getMimeMap()
293 {
294 return _mimeMap;
295 }
296
297
298
299
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
313
314
315
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
352
353
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 }