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.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
292
293 public MimeTypes()
294 {
295 }
296
297
298 public synchronized Map<String,String> getMimeMap()
299 {
300 return _mimeMap;
301 }
302
303
304
305
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
319
320
321
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
358
359
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 }