1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.myfaces.trinidad.webapp;
20
21 import java.io.BufferedReader;
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25 import java.io.InputStreamReader;
26 import java.io.InterruptedIOException;
27 import java.io.OutputStream;
28 import java.io.Reader;
29
30 import java.lang.reflect.Constructor;
31 import java.lang.reflect.InvocationTargetException;
32
33 import java.net.SocketException;
34 import java.net.URL;
35 import java.net.URLConnection;
36
37 import java.util.Arrays;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.concurrent.ConcurrentMap;
42
43 import javax.faces.FacesException;
44 import javax.faces.FactoryFinder;
45 import javax.faces.application.ProjectStage;
46 import javax.faces.context.ExternalContext;
47 import javax.faces.context.FacesContext;
48 import javax.faces.context.FacesContextFactory;
49 import javax.faces.event.PhaseListener;
50 import javax.faces.lifecycle.Lifecycle;
51
52 import javax.naming.Context;
53 import javax.naming.InitialContext;
54 import javax.naming.NamingException;
55
56 import javax.servlet.ServletConfig;
57 import javax.servlet.ServletContext;
58 import javax.servlet.ServletException;
59 import javax.servlet.ServletRequest;
60 import javax.servlet.ServletResponse;
61 import javax.servlet.http.HttpServlet;
62 import javax.servlet.http.HttpServletRequest;
63 import javax.servlet.http.HttpServletResponse;
64
65 import org.apache.myfaces.trinidad.config.Configurator;
66 import org.apache.myfaces.trinidad.logging.TrinidadLogger;
67 import org.apache.myfaces.trinidad.resource.CachingResourceLoader;
68 import org.apache.myfaces.trinidad.resource.DirectoryResourceLoader;
69 import org.apache.myfaces.trinidad.resource.ResourceLoader;
70 import org.apache.myfaces.trinidad.resource.ServletContextResourceLoader;
71 import org.apache.myfaces.trinidad.util.URLUtils;
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91 public class ResourceServlet extends HttpServlet
92 {
93
94
95
96 @SuppressWarnings("compatibility:8282627001212629976")
97 private static final long serialVersionUID = 4547362994406585148L;
98
99
100
101
102 @Override
103 public void destroy()
104 {
105 _loaders = null;
106 _loaderErrors = null;
107 _facesContextFactory = null;
108 _lifecycle = null;
109
110 super.destroy();
111 }
112
113
114
115
116 @Override
117 public void init(
118 ServletConfig config
119 ) throws ServletException
120 {
121 super.init(config);
122
123
124 try
125 {
126 _facesContextFactory = (FacesContextFactory)
127 FactoryFinder.getFactory
128 (FactoryFinder.FACES_CONTEXT_FACTORY);
129 }
130 catch (FacesException e)
131 {
132 Throwable rootCause = e.getCause();
133 if (rootCause == null)
134 {
135 throw e;
136 }
137 else
138 {
139 throw new ServletException(e.getMessage(), rootCause);
140 }
141 }
142
143
144 _lifecycle = new _ResourceLifecycle();
145 _initDebug(config);
146 _loaders = new ConcurrentHashMap<String, ResourceLoader>();
147 _loaderErrors = new ConcurrentHashMap<String, Class<?>>();
148 }
149
150 @Override
151 public void service(
152 ServletRequest request,
153 ServletResponse response
154 ) throws ServletException, IOException
155 {
156 boolean hasFacesContext = false;
157 FacesContext context = FacesContext.getCurrentInstance();
158
159
160
161
162
163 if (context != null)
164 {
165
166
167 if (_isContextValid(context))
168 {
169 hasFacesContext = true;
170 }
171 else
172 {
173
174 context.release();
175 }
176 }
177
178 if (!hasFacesContext)
179 {
180 Configurator.disableConfiguratorServices(request);
181
182
183
184
185 context = _facesContextFactory.getFacesContext(getServletContext(), request, response, _lifecycle);
186 }
187
188 try
189 {
190 super.service(request, response);
191 }
192 catch (ServletException e)
193 {
194 _logServiceException(e, request);
195 throw e;
196 }
197 catch (IOException e)
198 {
199 if (!_canIgnore(e))
200 _logServiceException(e, request);
201 throw e;
202 }
203 catch (RuntimeException e)
204 {
205 _logServiceException(e, request);
206 throw e;
207 }
208 catch (Error e)
209 {
210 _logServiceException(e, request);
211 throw e;
212 }
213 finally
214 {
215
216 if (!hasFacesContext)
217 context.release();
218 }
219 }
220
221
222
223
224 @Override
225 protected void doGet(
226 HttpServletRequest request,
227 HttpServletResponse response
228 ) throws ServletException, IOException
229 {
230 ResourceLoader loader = _getResourceLoader(request);
231 String resourcePath = getResourcePath(request);
232 URL url = loader.getResource(resourcePath);
233
234
235 if (url == null)
236 {
237 _logURLNotFound(request, loader, resourcePath);
238 response.sendError(HttpServletResponse.SC_NOT_FOUND);
239 return;
240 }
241
242
243 URLConnection connection = url.openConnection();
244 connection.setDoInput(true);
245 connection.setDoOutput(false);
246
247
248
249
250
251
252
253 connection.connect();
254
255 _setHeaders(connection, response, loader);
256
257 InputStream in = connection.getInputStream();
258 OutputStream out = response.getOutputStream();
259 byte[] buffer = new byte[_BUFFER_SIZE];
260
261 try
262 {
263 _pipeBytes(in, out, buffer);
264 }
265 finally
266 {
267 try
268 {
269 in.close();
270 }
271 finally
272 {
273 out.close();
274 }
275 }
276 }
277
278
279
280
281 @Override
282 protected long getLastModified(
283 HttpServletRequest request)
284 {
285 try
286 {
287 ResourceLoader loader = _getResourceLoader(request);
288 String resourcePath = getResourcePath(request);
289 URL url = loader.getResource(resourcePath);
290
291 if (url == null)
292 return super.getLastModified(request);
293
294 return URLUtils.getLastModified(url);
295 }
296 catch (IOException e)
297 {
298
299
300 return super.getLastModified(request);
301 }
302 }
303
304
305
306
307
308
309
310
311 protected String getResourcePath(
312 HttpServletRequest request)
313 {
314 return request.getServletPath() + request.getPathInfo();
315 }
316
317
318
319
320 private boolean _isContextValid(FacesContext context)
321 {
322 ExternalContext ec = context.getExternalContext();
323
324
325 return ((ec != null) && (ec.getRequest() != null));
326 }
327
328
329
330
331 private ResourceLoader _getResourceLoader(
332 String servletPath)
333 {
334 ResourceLoader loader = null;
335
336 try
337 {
338 String key = "META-INF/servlets/resources" +
339 servletPath +
340 ".resources";
341 ClassLoader cl = Thread.currentThread().getContextClassLoader();
342 URL url = cl.getResource(key);
343
344 if (url != null)
345 {
346 Reader r = new InputStreamReader(url.openStream());
347 BufferedReader br = new BufferedReader(r);
348 try
349 {
350 String className = br.readLine();
351 if (className != null)
352 {
353 className = className.trim();
354 Class<?> clazz = cl.loadClass(className);
355 try
356 {
357
358 Constructor<?> decorator = clazz.getConstructor(_DECORATOR_SIGNATURE);
359 ServletContext context = getServletContext();
360 File tempdir = (File)
361 context.getAttribute("javax.servlet.context.tempdir");
362 ResourceLoader delegate = new DirectoryResourceLoader(tempdir);
363 loader = (ResourceLoader) decorator.newInstance(new Object[]{delegate});
364 }
365 catch (InvocationTargetException e)
366 {
367
368 _logLoaderException(e, servletPath);
369 loader = (ResourceLoader) clazz.newInstance();
370 }
371 catch (NoSuchMethodException e)
372 {
373
374 loader = (ResourceLoader) clazz.newInstance();
375 }
376 }
377 }
378 finally
379 {
380 br.close();
381 }
382 }
383 else
384 {
385
386 if (_LOG.isWarning())
387 {
388 _LOG.warning("Unable to find ResourceLoader for ResourceServlet" +
389 " at servlet path:{0}" +
390 "\nCause: Could not find resource:{1}",
391 new Object[] {servletPath, key});
392 }
393
394 loader = new ServletContextResourceLoader(getServletContext())
395 {
396 @Override
397 public URL getResource(
398 String path) throws IOException
399 {
400 return super.getResource(path);
401 }
402 };
403 }
404
405
406 if (!_debug && loader.isCachable())
407 {
408 loader = new CachingResourceLoader(loader);
409 }
410 }
411 catch (IllegalAccessException e)
412 {
413 loader = logExceptionAndReturnFailureLoader(e, servletPath);
414 }
415 catch (InstantiationException e)
416 {
417 loader = logExceptionAndReturnFailureLoader(e, servletPath);
418 }
419 catch (ClassNotFoundException e)
420 {
421 loader = logExceptionAndReturnFailureLoader(e, servletPath);
422 }
423 catch (IOException e)
424 {
425 loader = logExceptionAndReturnFailureLoader(e, servletPath);
426 }
427
428 return loader;
429 }
430
431
432
433
434
435
436
437
438 private ResourceLoader _getResourceLoader(
439 HttpServletRequest request)
440 {
441 final String servletPath = request.getServletPath();
442 ResourceLoader loader = _loaders.get(servletPath);
443
444 if (loader == null)
445 {
446 loader = _getResourceLoader(servletPath);
447
448
449 _registerLoader(servletPath, loader);
450 }
451
452 return loader;
453 }
454
455
456
457
458
459
460 private void _registerLoader(String servletPath, ResourceLoader loader)
461 {
462 _loaders.put(servletPath, loader);
463 }
464
465
466
467
468 private void _logLoaderException(Exception e, String servletPath)
469 {
470 Class<?> previousExceptionClass = _loaderErrors.get(servletPath);
471
472
473 if (previousExceptionClass == null || previousExceptionClass != e.getClass())
474 {
475 _LOG.severe(e);
476
477 if (e.getCause() != null)
478 _LOG.severe("Caused by ", e.getCause());
479
480 _loaderErrors.put(servletPath, e.getClass());
481 }
482 }
483
484
485
486
487
488
489
490
491 private void _logURLNotFound(HttpServletRequest request,
492 ResourceLoader loader,
493 String resourcePath)
494 {
495
496 if (_LOG.isWarning())
497 {
498 FacesContext context = FacesContext.getCurrentInstance();
499 Object servletContext = _getServletContextFromFacesContext(context);
500
501 _LOG.warning("URL for resource not found.\n"+
502 " resourcePath: {0}\n"+
503 " loader class name: {1}\n"+
504 " request.pathTranslated: {2}\n"+
505 " request.requestURL: {3}\n"+
506 " FacesContext: {4}\n" +
507 " ServletContext: {5}\n",
508 new Object[] { resourcePath,
509 loader,
510 request.getPathTranslated(),
511 request.getRequestURL(),
512 context,
513 servletContext});
514 }
515 }
516
517
518
519
520
521
522
523 private void _logServiceException(Throwable e, ServletRequest request)
524 {
525
526 FacesContext context = FacesContext.getCurrentInstance();
527 Object servletContext = _getServletContextFromFacesContext(context);
528 HttpServletRequest sr = (HttpServletRequest) request;
529
530 _LOG.severe("An Exception occured in ResourceServlet.service().\n"+
531 " request.pathTranslated:" + sr.getPathTranslated() + "\n" +
532 " request.requestURI:" + sr.getRequestURI() + "\n" +
533 " FacesContext: " + context + "\n" +
534 " ServletContext: " + servletContext + "\n",
535 e);
536 }
537
538
539
540
541
542
543
544 private Object _getServletContextFromFacesContext(FacesContext context)
545 {
546 ExternalContext ec = null;
547 Object sc = null;
548
549 if (context != null)
550 {
551 ec = context.getExternalContext();
552
553 if (ec != null)
554 {
555 sc = ec.getContext();
556 }
557 }
558
559 return sc;
560 }
561
562
563
564
565
566 private static void _pipeBytes(
567 InputStream in,
568 OutputStream out,
569 byte[] buffer
570 ) throws IOException
571 {
572 int length;
573
574 while ((length = (in.read(buffer))) >= 0)
575 {
576 out.write(buffer, 0, length);
577 }
578 }
579
580
581
582
583 private void _initDebug(
584 ServletConfig config
585 )
586 {
587 String debug = config.getInitParameter(DEBUG_INIT_PARAM);
588 if (debug == null)
589 {
590
591
592 debug = config.getServletContext().getInitParameter(DEBUG_INIT_PARAM);
593 }
594
595
596
597 ProjectStage currentStage = _getFacesProjectStage(config.getServletContext());
598
599 if (debug != null)
600 {
601 _debug = "true".equalsIgnoreCase(debug);
602 }
603 else
604 {
605
606
607
608
609 _debug = !(ProjectStage.Production.equals(currentStage));
610 }
611
612 if (_debug)
613 {
614
615
616 if (ProjectStage.Production.equals(currentStage))
617 {
618 _LOG.warning("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM);
619 }
620 else
621 {
622 _LOG.info("RESOURCESERVLET_IN_DEBUG_MODE",DEBUG_INIT_PARAM);
623 }
624 }
625 }
626
627
628
629
630
631
632
633
634
635
636 private ProjectStage _getFacesProjectStage(ServletContext servletContext)
637 {
638 if (_projectStage == null)
639 {
640 String stageName = null;
641
642
643 try
644 {
645 Context ctx = new InitialContext();
646 Object temp = ctx.lookup(ProjectStage.PROJECT_STAGE_JNDI_NAME);
647 if (temp != null)
648 {
649 if (temp instanceof String)
650 {
651 stageName = (String) temp;
652 }
653 else
654 {
655 if (_LOG.isSevere())
656 {
657 _LOG.severe("Invalid JNDI lookup for key " + ProjectStage.PROJECT_STAGE_JNDI_NAME);
658 }
659 }
660 }
661 }
662 catch (NamingException e)
663 {
664
665 }
666
667
668
669
670
671 if (stageName == null)
672 {
673 stageName = servletContext.getInitParameter(ProjectStage.PROJECT_STAGE_PARAM_NAME);
674 }
675
676
677 if (stageName != null)
678 {
679
680
681
682
683 try
684 {
685 _projectStage = ProjectStage.valueOf(stageName);
686 return _projectStage;
687 }
688 catch (IllegalArgumentException e)
689 {
690 _LOG.severe("Couldn't discover the current project stage", e);
691 }
692 }
693 else
694 {
695 if (_LOG.isInfo())
696 {
697 _LOG.info("Couldn't discover the current project stage, using " + ProjectStage.Production);
698 }
699 }
700
701
702
703
704
705 _projectStage = ProjectStage.Production;
706 }
707
708 return _projectStage;
709 }
710
711
712
713
714
715 private void _setHeaders(
716 URLConnection connection,
717 HttpServletResponse response,
718 ResourceLoader loader)
719 {
720 String resourcePath;
721 URL url;
722 String contentType = ResourceLoader.getContentType(loader, connection);
723
724 if (contentType == null || "content/unknown".equals(contentType))
725 {
726 url = connection.getURL();
727 resourcePath = url.getPath();
728
729
730 if (resourcePath.endsWith(".css"))
731 contentType = "text/css";
732 else if (resourcePath.endsWith(".js"))
733 contentType = "application/x-javascript";
734 else if (resourcePath.endsWith(".cur") || resourcePath.endsWith(".ico"))
735 contentType = "image/vnd.microsoft.icon";
736 else
737 contentType = getServletContext().getMimeType(resourcePath);
738
739
740
741 if (contentType == null)
742 {
743 _LOG.warning("ResourceServlet._setHeaders(): " +
744 "Content type for {0} is NULL!\n" +
745 "Cause: Unknown file extension",
746 resourcePath);
747 }
748 }
749
750 if (contentType != null)
751 {
752 response.setContentType(contentType);
753 int contentLength = connection.getContentLength();
754
755 if (contentLength >= 0)
756 response.setContentLength(contentLength);
757 }
758
759 long lastModified;
760 try
761 {
762 lastModified = URLUtils.getLastModified(connection);
763 }
764 catch (IOException exception)
765 {
766 lastModified = 0;
767 }
768
769 if (lastModified == 0)
770 response.setDateHeader("Last-Modified", lastModified);
771
772
773
774
775
776
777 String cacheControl = connection.getHeaderField("cache-control");
778 Long expires = connection.getExpiration();
779
780 if (!_debug && null == cacheControl && 0 == expires)
781 {
782
783
784
785
786
787 cacheControl = "Public";
788 expires = System.currentTimeMillis() + ONE_YEAR_MILLIS;
789 }
790
791 if(null != cacheControl)
792 {
793 response.setHeader("Cache-Control", cacheControl);
794 }
795
796 if(0 != expires)
797 {
798 response.setDateHeader("Expires", expires);
799 }
800
801 Map<String, List<String>> headerMap = connection.getHeaderFields();
802 if(null != headerMap)
803 {
804
805 for(Map.Entry<String, List<String>> entry: connection.getHeaderFields().entrySet())
806 {
807
808
809 String key = entry.getKey();
810 if(Arrays.binarySearch(_INCLUDED_HEADERS, key.toLowerCase()) >= 0 && null != entry.getValue())
811 {
812
813 for(String value:entry.getValue())
814 {
815 response.addHeader(key, value);
816 }
817 }
818 }
819 }
820 }
821
822 private static boolean _canIgnore(Throwable t)
823 {
824 if (t instanceof InterruptedIOException)
825 {
826
827 return true;
828 }
829 else if (t instanceof SocketException)
830 {
831
832
833
834
835 return true;
836 }
837 else if (t instanceof IOException)
838 {
839 String message = t.getMessage();
840
841
842 if ((message != null) &&
843 ((message.indexOf("Broken pipe") >= 0) ||
844 (message.indexOf("abort") >= 0)))
845 return true;
846 }
847 return false;
848 }
849
850
851
852
853
854 private ResourceLoader logExceptionAndReturnFailureLoader(Exception e, String servletPath)
855 {
856 _logLoaderException(e, servletPath);
857 return new _ExponentialBackoffResourceLoader(servletPath);
858 }
859
860
861
862
863
864
865
866 private final class _ExponentialBackoffResourceLoader extends ResourceLoader
867 {
868
869
870
871 protected _ExponentialBackoffResourceLoader(String servletPath)
872 {
873 super(null);
874
875
876 _initialTimeStamp = System.currentTimeMillis();
877 _nextBackoffTime = _initialTimeStamp + _INITIAL_WAIT_TIME;
878 _servletPath = servletPath;
879 }
880
881
882 @Override
883 protected URL findResource(
884 String name
885 ) throws IOException
886 {
887 long currentTime = System.currentTimeMillis();
888
889
890 if (currentTime > _nextBackoffTime)
891 {
892 synchronized(this)
893 {
894
895
896
897 if (currentTime > _nextBackoffTime)
898 {
899 ResourceLoader newLoader = _getResourceLoader(_servletPath);
900
901 if (!(newLoader instanceof _ExponentialBackoffResourceLoader))
902 {
903
904 _registerLoader(_servletPath, newLoader);
905
906
907 _LOG.warning("Fixed resource loader for servlet path: {0}", _servletPath);
908
909 return newLoader.getResource(name);
910 }
911
912
913
914
915 _nextBackoffTime += Math.min((currentTime - _initialTimeStamp) * 2, _MAX_WAIT_TIME);
916 }
917 }
918 }
919
920
921 return null;
922 }
923
924 private volatile long _nextBackoffTime;
925 private final long _initialTimeStamp;
926 private final String _servletPath;
927 }
928
929 static private class _ResourceLifecycle extends Lifecycle
930 {
931 @Override
932 public void execute(FacesContext p0) throws FacesException
933 {
934 }
935
936 @Override
937 public PhaseListener[] getPhaseListeners()
938 {
939 return null;
940 }
941
942 @Override
943 public void removePhaseListener(PhaseListener p0)
944 {
945 }
946
947 @Override
948 public void render(FacesContext p0) throws FacesException
949 {
950 }
951
952 @Override
953 public void addPhaseListener(PhaseListener p0)
954 {
955 }
956 }
957
958
959
960
961
962 public static final String DEBUG_INIT_PARAM =
963 "org.apache.myfaces.trinidad.resource.DEBUG";
964
965
966
967
968 public static final long ONE_YEAR_MILLIS = 31363200000L;
969 private static final long _INITIAL_WAIT_TIME = 10L;
970 private static final long _MAX_WAIT_TIME = 60000L;
971
972 private static final Class[] _DECORATOR_SIGNATURE =
973 new Class[]{ResourceLoader.class};
974
975 private static final TrinidadLogger _LOG = TrinidadLogger.createTrinidadLogger(ResourceServlet.class);
976
977
978 private static final int _BUFFER_SIZE = 2048;
979
980 private volatile boolean _debug;
981 private volatile ConcurrentMap<String, ResourceLoader> _loaders;
982 private volatile ConcurrentMap<String, Class<?>> _loaderErrors;
983 private volatile FacesContextFactory _facesContextFactory;
984 private volatile Lifecycle _lifecycle;
985 private volatile ProjectStage _projectStage;
986
987
988
989
990
991 private static final String[] _INCLUDED_HEADERS;
992
993 static
994 {
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008 String[] tempArray = new String[]
1009 {
1010 "age",
1011 "content-language",
1012 "content-location",
1013 "content-md5",
1014 "content-disposition",
1015 "content-range",
1016 "date",
1017 "link",
1018 "p3p",
1019 "refresh",
1020 "retry-after",
1021 "strict-transport-security",
1022 "trailer",
1023
1024
1025 "warning"
1026 };
1027
1028
1029 Arrays.sort(tempArray);
1030
1031 _INCLUDED_HEADERS = tempArray;
1032 }
1033 }