View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.servlet;
21  
22  import org.apache.myfaces.tobago.application.ProjectStage;
23  import org.apache.myfaces.tobago.config.TobagoConfig;
24  import org.apache.myfaces.tobago.context.Theme;
25  import org.apache.myfaces.tobago.internal.util.IoUtils;
26  import org.apache.myfaces.tobago.internal.util.MimeTypeUtils;
27  import org.apache.myfaces.tobago.internal.util.ResponseUtils;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  import javax.servlet.ServletConfig;
32  import javax.servlet.ServletException;
33  import javax.servlet.http.HttpServlet;
34  import javax.servlet.http.HttpServletRequest;
35  import javax.servlet.http.HttpServletResponse;
36  import java.io.IOException;
37  import java.io.InputStream;
38  import java.io.OutputStream;
39  import java.util.HashSet;
40  import java.util.List;
41  import java.util.Set;
42  
43  /**
44   * <p><pre>
45   * &lt;servlet&gt;
46   *   &lt;servlet-name&gt;ResourceServlet&lt;/servlet-name&gt;
47   *   &lt;servlet-class&gt;org.apache.myfaces.tobago.servlet.ResourceServlet&lt;/servlet-class&gt;
48   *   &lt;init-param&gt;
49   *     &lt;description&gt;The value for the expires header in seconds.
50   *       The default for ProjectStage.Production is 86400 sec (24 h) otherwise no expires header.&lt;/description&gt;
51   *     &lt;param-name&gt;expires&lt;/param-name&gt;
52   *     &lt;param-value&gt;14400&lt;/param-value&gt;
53   *   &lt;/init-param&gt;
54   *   &lt;init-param&gt;
55   *     &lt;description&gt;The value for the copy buffer size.
56   *            Default is 4096.&lt;/description&gt;
57   *     &lt;param-name&gt;bufferSize&lt;/param-name&gt;
58   *     &lt;param-value&gt;4096&lt;/param-value&gt;
59   *   &lt;/init-param&gt;
60   * &lt;/servlet&gt;
61   * &lt;servlet-mapping&gt;
62   *   &lt;servlet-name&gt;ResourceServlet&lt;/servlet-name&gt;
63   *   &lt;url-pattern&gt;/org/apache/myfaces/tobago/renderkit/*&lt;/url-pattern&gt;
64   * &lt;/servlet-mapping&gt;
65   * </pre><p>
66   *
67   * @since 1.0.7
68   */
69  public class ResourceServlet extends HttpServlet {
70  
71    private static final long serialVersionUID = -4491419290205206466L;
72  
73    private static final Logger LOG = LoggerFactory.getLogger(ResourceServlet.class);
74  
75    private Long expires;
76    private int bufferSize;
77    private Set<String> resourceDirs = new HashSet<String>();
78    private boolean nosniffHeader;
79  
80    @Override
81    public void init(final ServletConfig servletConfig) throws ServletException {
82      super.init(servletConfig);
83      final TobagoConfig tobagoConfig = TobagoConfig.getInstance(servletConfig.getServletContext());
84      if (tobagoConfig != null && tobagoConfig.getProjectStage() == ProjectStage.Production) {
85         expires = 24 * 60 * 60 * 1000L;
86      }
87      final Theme defaultTheme = tobagoConfig.getDefaultTheme();
88      addResourceDir(defaultTheme.getFallbackList());
89      addResourceDir(tobagoConfig.getSupportedThemes());
90  
91      final String expiresString = servletConfig.getInitParameter("expires");
92      if (expiresString != null) {
93        try {
94          expires = new Long(expiresString) * 1000;
95        } catch (final NumberFormatException e) {
96          LOG.error("Caught: " + e.getMessage(), e);
97        }
98      }
99      final String bufferSizeString = servletConfig.getInitParameter("bufferSize");
100     bufferSize = 1024 * 4;
101     if (bufferSizeString != null) {
102       try {
103         bufferSize = Integer.parseInt(bufferSizeString);
104       } catch (final NumberFormatException e) {
105         LOG.error("Caught: " + e.getMessage(), e);
106       }
107     }
108     nosniffHeader = tobagoConfig.isSetNosniffHeader();
109   }
110 
111   private void addResourceDir(final List<Theme> themes) {
112     for (final Theme theme : themes) {
113         addResourceDir(theme);
114     }
115   }
116 
117   private void addResourceDir(final Theme theme) {
118     final String dir = theme.getResourcePath();
119     if (dir.startsWith("/")) {
120       resourceDirs.add(dir.substring(1));
121     } else {
122       resourceDirs.add(dir);
123     }
124   }
125 
126     @Override
127   protected void doGet(
128       final HttpServletRequest request, final HttpServletResponse response)
129       throws ServletException, IOException {
130 
131     final String requestURI = request.getRequestURI();
132     String resource = requestURI.substring(request.getContextPath().length() + 1);
133     for (final String resourceDir : resourceDirs) {
134       if (resource.startsWith(resourceDir)) {
135         final int dirLength = resourceDir.length();
136         if (dirLength < resource.length() && Character.isDigit(resource.charAt(dirLength + 1))) {
137           // cut off the version number
138           resource = resourceDir + resource.substring(resource.indexOf('/', dirLength + 1));
139         }
140         break;
141       }
142     }
143     if (expires != null) {
144       response.setDateHeader("Last-Modified", 0);
145       response.setHeader("Cache-Control", "Public, max-age=" + expires);
146       response.setDateHeader("Expires", System.currentTimeMillis() + expires);
147     }
148     final String contentType = MimeTypeUtils.getMimeTypeForFile(requestURI);
149     if (contentType != null) {
150       response.setContentType(contentType);
151       if (nosniffHeader) {
152         ResponseUtils.ensureNosniffHeader(response);
153       }
154     } else {
155       final String message = "Unsupported mime type of resource='" + resource + "'";
156       LOG.warn(message + " (because of security reasons)");
157       response.sendError(HttpServletResponse.SC_FORBIDDEN, message);
158       return;
159     }
160 
161     InputStream inputStream = null;
162     try {
163       final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
164 
165       // meta inf (like in servlet 3.0)
166       inputStream = classLoader.getResourceAsStream("META-INF/resources/" + resource);
167 
168       // "normal" classpath
169       if (inputStream == null) {
170         inputStream = classLoader.getResourceAsStream(resource);
171       }
172 
173       if (inputStream != null) {
174         copy(inputStream, response.getOutputStream());
175       } else {
176         final String message = "Resource '" + resource + "' not found!";
177         LOG.warn(message);
178         response.sendError(HttpServletResponse.SC_NOT_FOUND, message);
179       }
180     } finally {
181       IoUtils.closeQuietly(inputStream);
182     }
183   }
184 
185   @Override
186   protected long getLastModified(final HttpServletRequest request) {
187     if (expires != null) {
188       return 0;
189     } else {
190       return super.getLastModified(request);
191     }
192   }
193 
194   private void copy(final InputStream inputStream, final OutputStream outputStream) throws IOException {
195     final byte[] buffer = new byte[bufferSize];
196     int count;
197     while (-1 != (count = inputStream.read(buffer))) {
198       outputStream.write(buffer, 0, count);
199     }
200   }
201 }