1   /**
2    * Copyright (c) 2000-2009 Liferay, Inc. All rights reserved.
3    *
4    * Permission is hereby granted, free of charge, to any person obtaining a copy
5    * of this software and associated documentation files (the "Software"), to deal
6    * in the Software without restriction, including without limitation the rights
7    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8    * copies of the Software, and to permit persons to whom the Software is
9    * furnished to do so, subject to the following conditions:
10   *
11   * The above copyright notice and this permission notice shall be included in
12   * all copies or substantial portions of the Software.
13   *
14   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20   * SOFTWARE.
21   */
22  
23  package com.liferay.portal.servlet.filters.minifier;
24  
25  import com.liferay.portal.kernel.configuration.Filter;
26  import com.liferay.portal.kernel.log.Log;
27  import com.liferay.portal.kernel.log.LogFactoryUtil;
28  import com.liferay.portal.kernel.servlet.BrowserSniffer;
29  import com.liferay.portal.kernel.servlet.ServletContextUtil;
30  import com.liferay.portal.kernel.util.ArrayUtil;
31  import com.liferay.portal.kernel.util.ContentTypes;
32  import com.liferay.portal.kernel.util.FileUtil;
33  import com.liferay.portal.kernel.util.GetterUtil;
34  import com.liferay.portal.kernel.util.ParamUtil;
35  import com.liferay.portal.kernel.util.StringPool;
36  import com.liferay.portal.kernel.util.StringUtil;
37  import com.liferay.portal.kernel.util.Validator;
38  import com.liferay.portal.servlet.filters.BasePortalFilter;
39  import com.liferay.portal.servlet.filters.etag.ETagUtil;
40  import com.liferay.portal.util.MinifierUtil;
41  import com.liferay.portal.util.PropsKeys;
42  import com.liferay.portal.util.PropsUtil;
43  import com.liferay.portal.util.PropsValues;
44  import com.liferay.util.SystemProperties;
45  import com.liferay.util.servlet.ServletResponseUtil;
46  import com.liferay.util.servlet.filters.CacheResponse;
47  import com.liferay.util.servlet.filters.CacheResponseUtil;
48  
49  import java.io.File;
50  import java.io.IOException;
51  
52  import java.util.regex.Matcher;
53  import java.util.regex.Pattern;
54  
55  import javax.servlet.FilterChain;
56  import javax.servlet.FilterConfig;
57  import javax.servlet.ServletContext;
58  import javax.servlet.ServletException;
59  import javax.servlet.http.HttpServletRequest;
60  import javax.servlet.http.HttpServletResponse;
61  
62  /**
63   * <a href="MinifierFilter.java.html"><b><i>View Source</i></b></a>
64   *
65   * @author Brian Wing Shun Chan
66   *
67   */
68  public class MinifierFilter extends BasePortalFilter {
69  
70      public void init(FilterConfig filterConfig) throws ServletException {
71          super.init(filterConfig);
72  
73          _servletContext = filterConfig.getServletContext();
74          _servletContextName = GetterUtil.getString(
75              _servletContext.getServletContextName());
76  
77          if (Validator.isNull(_servletContextName)) {
78              _tempDir += "/portal";
79          }
80      }
81  
82      protected String aggregateCss(String dir, String content)
83          throws IOException {
84  
85          StringBuilder sb = new StringBuilder(content.length());
86  
87          int pos = 0;
88  
89          while (true) {
90              int x = content.indexOf(_CSS_IMPORT_BEGIN, pos);
91              int y = content.indexOf(
92                  _CSS_IMPORT_END, x + _CSS_IMPORT_BEGIN.length());
93  
94              if ((x == -1) || (y == -1)) {
95                  sb.append(content.substring(pos, content.length()));
96  
97                  break;
98              }
99              else {
100                 sb.append(content.substring(pos, x));
101 
102                 String importFile = content.substring(
103                     x + _CSS_IMPORT_BEGIN.length(), y);
104 
105                 String importContent = FileUtil.read(
106                     dir + StringPool.SLASH + importFile);
107 
108                 String importFilePath = StringPool.BLANK;
109 
110                 if (importFile.lastIndexOf(StringPool.SLASH) != -1) {
111                     importFilePath = StringPool.SLASH + importFile.substring(
112                         0, importFile.lastIndexOf(StringPool.SLASH) + 1);
113                 }
114 
115                 importContent = aggregateCss(
116                     dir + importFilePath, importContent);
117 
118                 int importDepth = StringUtil.count(
119                     importFile, StringPool.SLASH);
120 
121                 // LEP-7540
122 
123                 String relativePath = StringPool.BLANK;
124 
125                 for (int i = 0; i < importDepth; i++) {
126                     relativePath += "../";
127                 }
128 
129                 importContent = StringUtil.replace(
130                     importContent,
131                     new String[] {
132                         "url('" + relativePath,
133                         "url(\"" + relativePath,
134                         "url(" + relativePath
135                     },
136                     new String[] {
137                         "url('[$TEMP_RELATIVE_PATH$]",
138                         "url(\"[$TEMP_RELATIVE_PATH$]",
139                         "url([$TEMP_RELATIVE_PATH$]"
140                     });
141 
142                 importContent = StringUtil.replace(
143                     importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
144 
145                 sb.append(importContent);
146 
147                 pos = y + _CSS_IMPORT_END.length();
148             }
149         }
150 
151         return sb.toString();
152     }
153 
154     protected String getMinifiedBundleContent(
155             HttpServletRequest request, HttpServletResponse response)
156         throws IOException {
157 
158         String minifierType = ParamUtil.getString(request, "minifierType");
159         String minifierBundleId = ParamUtil.getString(
160             request, "minifierBundleId");
161 
162         if (Validator.isNull(minifierType) ||
163             Validator.isNull(minifierBundleId) ||
164             !ArrayUtil.contains(
165                 PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
166 
167             return null;
168         }
169 
170         String minifierBundleDir = PropsUtil.get(
171             PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
172 
173         String bundleDirRealPath = ServletContextUtil.getRealPath(
174             _servletContext, minifierBundleDir);
175 
176         if (bundleDirRealPath == null) {
177             return null;
178         }
179 
180         String cacheFileName = _tempDir + request.getRequestURI();
181 
182         String queryString = request.getQueryString();
183 
184         if (queryString != null) {
185             cacheFileName += _QUESTION_SEPARATOR + queryString;
186         }
187 
188         String[] fileNames = PropsUtil.getArray(minifierBundleId);
189 
190         File cacheFile = new File(cacheFileName);
191 
192         if (cacheFile.exists()) {
193             boolean staleCache = false;
194 
195             for (String fileName : fileNames) {
196                 File file = new File(
197                     bundleDirRealPath + StringPool.SLASH + fileName);
198 
199                 if (file.lastModified() > cacheFile.lastModified()) {
200                     staleCache = true;
201 
202                     break;
203                 }
204             }
205 
206             if (!staleCache) {
207                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
208 
209                 return FileUtil.read(cacheFile);
210             }
211         }
212 
213         if (_log.isInfoEnabled()) {
214             _log.info("Minifying JavaScript bundle " + minifierBundleId);
215         }
216 
217         StringBuilder sb = new StringBuilder();
218 
219         for (String fileName : fileNames) {
220             String content = FileUtil.read(
221                 bundleDirRealPath + StringPool.SLASH + fileName);
222 
223             sb.append(content);
224             sb.append(StringPool.NEW_LINE);
225         }
226 
227         String minifiedContent = minifyJavaScript(sb.toString());
228 
229         response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
230 
231         FileUtil.write(cacheFile, minifiedContent);
232 
233         return minifiedContent;
234     }
235 
236     protected String getMinifiedContent(
237             HttpServletRequest request, HttpServletResponse response,
238             FilterChain filterChain)
239         throws IOException, ServletException {
240 
241         String minifierType = ParamUtil.getString(request, "minifierType");
242         String minifierBundleId = ParamUtil.getString(
243             request, "minifierBundleId");
244         String minifierBundleDir = ParamUtil.getString(
245             request, "minifierBundleDir");
246 
247         if (Validator.isNull(minifierType) ||
248             Validator.isNotNull(minifierBundleId) ||
249             Validator.isNotNull(minifierBundleDir)) {
250 
251             return null;
252         }
253 
254         String requestURI = request.getRequestURI();
255 
256         String realPath = ServletContextUtil.getRealPath(
257             _servletContext, requestURI);
258 
259         if (realPath == null) {
260             return null;
261         }
262 
263         realPath = StringUtil.replace(
264             realPath, StringPool.BACK_SLASH, StringPool.SLASH);
265 
266         File file = new File(realPath);
267 
268         if (!file.exists()) {
269 
270             // Tomcat incorrectly returns the a real path to a resource that
271             // exists in another web application. For example, it returns
272             // ".../webapps/abc-theme/abc-theme/css/main.css" instead of
273             // ".../webapps/abc-theme/css/main.css".
274 
275             if (Validator.isNotNull(_servletContextName)) {
276                 realPath = StringUtil.replaceFirst(
277                     realPath, StringPool.SLASH + _servletContextName,
278                     StringPool.BLANK);
279 
280                 file = new File(realPath);
281             }
282         }
283 
284         if (!file.exists()) {
285             return null;
286         }
287 
288         String minifiedContent = null;
289 
290         String cacheCommonFileName = _tempDir + requestURI;
291 
292         String queryString = request.getQueryString();
293 
294         if (queryString != null) {
295             cacheCommonFileName += _QUESTION_SEPARATOR + queryString;
296         }
297 
298         File cacheContentTypeFile = new File(
299             cacheCommonFileName + "_E_CONTENT_TYPE");
300         File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
301 
302         if ((cacheDataFile.exists()) &&
303             (cacheDataFile.lastModified() >= file.lastModified())) {
304 
305             minifiedContent = FileUtil.read(cacheDataFile);
306 
307             if (cacheContentTypeFile.exists()) {
308                 String contentType = FileUtil.read(cacheContentTypeFile);
309 
310                 response.setContentType(contentType);
311             }
312         }
313         else {
314             if (realPath.endsWith(_CSS_EXTENSION)) {
315                 if (_log.isInfoEnabled()) {
316                     _log.info("Minifying CSS " + file);
317                 }
318 
319                 minifiedContent = minifyCss(request, file);
320 
321                 response.setContentType(ContentTypes.TEXT_CSS);
322 
323                 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
324             }
325             else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
326                 if (_log.isInfoEnabled()) {
327                     _log.info("Minifying JavaScript " + file);
328                 }
329 
330                 minifiedContent = minifyJavaScript(file);
331 
332                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
333 
334                 FileUtil.write(
335                     cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
336             }
337             else if (realPath.endsWith(_JSP_EXTENSION)) {
338                 if (_log.isInfoEnabled()) {
339                     _log.info("Minifying JSP " + file);
340                 }
341 
342                 CacheResponse cacheResponse = new CacheResponse(
343                     response, StringPool.UTF8);
344 
345                 processFilter(
346                     MinifierFilter.class, request, cacheResponse, filterChain);
347 
348                 CacheResponseUtil.addHeaders(
349                     response, cacheResponse.getHeaders());
350 
351                 response.setContentType(cacheResponse.getContentType());
352 
353                 minifiedContent = new String(
354                     cacheResponse.getData(), StringPool.UTF8);
355 
356                 if (minifierType.equals("css")) {
357                     minifiedContent = minifyCss(request, minifiedContent);
358                 }
359                 else if (minifierType.equals("js")) {
360                     minifiedContent = minifyJavaScript(minifiedContent);
361                 }
362 
363                 FileUtil.write(
364                     cacheContentTypeFile, cacheResponse.getContentType());
365             }
366             else {
367                 return null;
368             }
369 
370             FileUtil.write(cacheDataFile, minifiedContent);
371         }
372 
373         return minifiedContent;
374     }
375 
376     protected String minifyCss(HttpServletRequest request, File file)
377         throws IOException {
378 
379         String content = FileUtil.read(file);
380 
381         content = aggregateCss(file.getParent(), content);
382 
383         return minifyCss(request, content);
384     }
385 
386     protected String minifyCss(HttpServletRequest request, String content) {
387         String browserId = ParamUtil.getString(request, "browserId");
388 
389         if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
390             Matcher matcher = _pattern.matcher(content);
391 
392             content = matcher.replaceAll(StringPool.BLANK);
393         }
394 
395         return MinifierUtil.minifyCss(content);
396     }
397 
398     protected String minifyJavaScript(File file) throws IOException {
399         String content = FileUtil.read(file);
400 
401         return minifyJavaScript(content);
402     }
403 
404     protected String minifyJavaScript(String content) {
405         return MinifierUtil.minifyJavaScript(content);
406     }
407 
408     protected void processFilter(
409             HttpServletRequest request, HttpServletResponse response,
410             FilterChain filterChain)
411         throws IOException, ServletException {
412 
413         String minifiedContent = getMinifiedContent(
414             request, response, filterChain);
415 
416         if (Validator.isNull(minifiedContent)) {
417             minifiedContent = getMinifiedBundleContent(request, response);
418         }
419 
420         if (Validator.isNull(minifiedContent)) {
421             processFilter(MinifierFilter.class, request, response, filterChain);
422         }
423         else {
424             if (!ETagUtil.processETag(request, response, minifiedContent)) {
425                 ServletResponseUtil.write(response, minifiedContent);
426             }
427         }
428     }
429 
430     private static final String _CSS_IMPORT_BEGIN = "@import url(";
431 
432     private static final String _CSS_IMPORT_END = ");";
433 
434     private static final String _CSS_EXTENSION = ".css";
435 
436     private static final String _JAVASCRIPT_EXTENSION = ".js";
437 
438     private static final String _JSP_EXTENSION = ".jsp";
439 
440     private static final String _QUESTION_SEPARATOR = "_Q_";
441 
442     private static final String _TEMP_DIR =
443         SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
444 
445     private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
446 
447     private static Pattern _pattern = Pattern.compile(
448         "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
449 
450     private ServletContext _servletContext;
451     private String _servletContextName;
452     private String _tempDir = _TEMP_DIR;
453 
454 }