1   /**
2    * Copyright (c) 2000-2010 Liferay, Inc. All rights reserved.
3    *
4    * This library is free software; you can redistribute it and/or modify it under
5    * the terms of the GNU Lesser General Public License as published by the Free
6    * Software Foundation; either version 2.1 of the License, or (at your option)
7    * any later version.
8    *
9    * This library is distributed in the hope that it will be useful, but WITHOUT
10   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11   * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
12   * details.
13   */
14  
15  package com.liferay.portal.servlet.filters.minifier;
16  
17  import com.liferay.portal.kernel.configuration.Filter;
18  import com.liferay.portal.kernel.log.Log;
19  import com.liferay.portal.kernel.log.LogFactoryUtil;
20  import com.liferay.portal.kernel.servlet.BrowserSniffer;
21  import com.liferay.portal.kernel.servlet.ServletContextUtil;
22  import com.liferay.portal.kernel.servlet.StringServletResponse;
23  import com.liferay.portal.kernel.util.ArrayUtil;
24  import com.liferay.portal.kernel.util.ContentTypes;
25  import com.liferay.portal.kernel.util.FileUtil;
26  import com.liferay.portal.kernel.util.GetterUtil;
27  import com.liferay.portal.kernel.util.ParamUtil;
28  import com.liferay.portal.kernel.util.PropsKeys;
29  import com.liferay.portal.kernel.util.StringBundler;
30  import com.liferay.portal.kernel.util.StringPool;
31  import com.liferay.portal.kernel.util.StringUtil;
32  import com.liferay.portal.kernel.util.Validator;
33  import com.liferay.portal.servlet.filters.BasePortalFilter;
34  import com.liferay.portal.util.MinifierUtil;
35  import com.liferay.portal.util.PropsUtil;
36  import com.liferay.portal.util.PropsValues;
37  import com.liferay.util.SystemProperties;
38  import com.liferay.util.servlet.ServletResponseUtil;
39  import com.liferay.util.servlet.filters.CacheResponseUtil;
40  
41  import java.io.File;
42  import java.io.IOException;
43  
44  import java.util.regex.Matcher;
45  import java.util.regex.Pattern;
46  
47  import javax.servlet.FilterChain;
48  import javax.servlet.FilterConfig;
49  import javax.servlet.ServletContext;
50  import javax.servlet.http.HttpServletRequest;
51  import javax.servlet.http.HttpServletResponse;
52  
53  /**
54   * <a href="MinifierFilter.java.html"><b><i>View Source</i></b></a>
55   *
56   * @author Brian Wing Shun Chan
57   */
58  public class MinifierFilter extends BasePortalFilter {
59  
60      public void init(FilterConfig filterConfig) {
61          super.init(filterConfig);
62  
63          _servletContext = filterConfig.getServletContext();
64          _servletContextName = GetterUtil.getString(
65              _servletContext.getServletContextName());
66  
67          if (Validator.isNull(_servletContextName)) {
68              _tempDir += "/portal";
69          }
70      }
71  
72      protected String aggregateCss(String dir, String content)
73          throws IOException {
74  
75          StringBuilder sb = new StringBuilder(content.length());
76  
77          int pos = 0;
78  
79          while (true) {
80              int x = content.indexOf(_CSS_IMPORT_BEGIN, pos);
81              int y = content.indexOf(
82                  _CSS_IMPORT_END, x + _CSS_IMPORT_BEGIN.length());
83  
84              if ((x == -1) || (y == -1)) {
85                  sb.append(content.substring(pos, content.length()));
86  
87                  break;
88              }
89              else {
90                  sb.append(content.substring(pos, x));
91  
92                  String importFile = content.substring(
93                      x + _CSS_IMPORT_BEGIN.length(), y);
94  
95                  String importContent = FileUtil.read(
96                      dir + StringPool.SLASH + importFile);
97  
98                  String importFilePath = StringPool.BLANK;
99  
100                 if (importFile.lastIndexOf(StringPool.SLASH) != -1) {
101                     importFilePath = StringPool.SLASH + importFile.substring(
102                         0, importFile.lastIndexOf(StringPool.SLASH) + 1);
103                 }
104 
105                 importContent = aggregateCss(
106                     dir + importFilePath, importContent);
107 
108                 int importDepth = StringUtil.count(
109                     importFile, StringPool.SLASH);
110 
111                 // LEP-7540
112 
113                 String relativePath = StringPool.BLANK;
114 
115                 for (int i = 0; i < importDepth; i++) {
116                     relativePath += "../";
117                 }
118 
119                 importContent = StringUtil.replace(
120                     importContent,
121                     new String[] {
122                         "url('" + relativePath,
123                         "url(\"" + relativePath,
124                         "url(" + relativePath
125                     },
126                     new String[] {
127                         "url('[$TEMP_RELATIVE_PATH$]",
128                         "url(\"[$TEMP_RELATIVE_PATH$]",
129                         "url([$TEMP_RELATIVE_PATH$]"
130                     });
131 
132                 importContent = StringUtil.replace(
133                     importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
134 
135                 sb.append(importContent);
136 
137                 pos = y + _CSS_IMPORT_END.length();
138             }
139         }
140 
141         return sb.toString();
142     }
143 
144     protected String getMinifiedBundleContent(
145             HttpServletRequest request, HttpServletResponse response)
146         throws IOException {
147 
148         String minifierType = ParamUtil.getString(request, "minifierType");
149         String minifierBundleId = ParamUtil.getString(
150             request, "minifierBundleId");
151 
152         if (Validator.isNull(minifierType) ||
153             Validator.isNull(minifierBundleId) ||
154             !ArrayUtil.contains(
155                 PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
156 
157             return null;
158         }
159 
160         String minifierBundleDir = PropsUtil.get(
161             PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
162 
163         String bundleDirRealPath = ServletContextUtil.getRealPath(
164             _servletContext, minifierBundleDir);
165 
166         if (bundleDirRealPath == null) {
167             return null;
168         }
169 
170         StringBundler sb = new StringBundler(4);
171 
172         sb.append(_tempDir);
173         sb.append(request.getRequestURI());
174 
175         String queryString = request.getQueryString();
176 
177         if (queryString != null) {
178             sb.append(_QUESTION_SEPARATOR);
179             sb.append(sterilizeQueryString(queryString));
180         }
181 
182         String cacheFileName = sb.toString();
183 
184         String[] fileNames = PropsUtil.getArray(minifierBundleId);
185 
186         File cacheFile = new File(cacheFileName);
187 
188         if (cacheFile.exists()) {
189             boolean staleCache = false;
190 
191             for (String fileName : fileNames) {
192                 File file = new File(
193                     bundleDirRealPath + StringPool.SLASH + fileName);
194 
195                 if (file.lastModified() > cacheFile.lastModified()) {
196                     staleCache = true;
197 
198                     break;
199                 }
200             }
201 
202             if (!staleCache) {
203                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
204 
205                 return FileUtil.read(cacheFile);
206             }
207         }
208 
209         if (_log.isInfoEnabled()) {
210             _log.info("Minifying JavaScript bundle " + minifierBundleId);
211         }
212 
213         String minifiedContent = null;
214 
215         if (fileNames.length == 0) {
216             minifiedContent = StringPool.BLANK;
217         }
218         else {
219             sb = new StringBundler(fileNames.length * 2);
220 
221             for (String fileName : fileNames) {
222                 String content = FileUtil.read(
223                     bundleDirRealPath + StringPool.SLASH + fileName);
224 
225                 sb.append(content);
226                 sb.append(StringPool.NEW_LINE);
227             }
228 
229             minifiedContent = minifyJavaScript(sb.toString());
230         }
231 
232         response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
233 
234         FileUtil.write(cacheFile, minifiedContent);
235 
236         return minifiedContent;
237     }
238 
239     protected String getMinifiedContent(
240             HttpServletRequest request, HttpServletResponse response,
241             FilterChain filterChain)
242         throws Exception {
243 
244         String minifierType = ParamUtil.getString(request, "minifierType");
245         String minifierBundleId = ParamUtil.getString(
246             request, "minifierBundleId");
247         String minifierBundleDir = ParamUtil.getString(
248             request, "minifierBundleDir");
249 
250         if (Validator.isNull(minifierType) ||
251             Validator.isNotNull(minifierBundleId) ||
252             Validator.isNotNull(minifierBundleDir)) {
253 
254             return null;
255         }
256 
257         String requestURI = request.getRequestURI();
258 
259         String requestPath = requestURI;
260 
261         String contextPath = request.getContextPath();
262 
263         if (!contextPath.equals(StringPool.SLASH)) {
264             requestPath = requestPath.substring(contextPath.length());
265         }
266 
267         String realPath = ServletContextUtil.getRealPath(
268             _servletContext, requestPath);
269 
270         if (realPath == null) {
271             return null;
272         }
273 
274         realPath = StringUtil.replace(
275             realPath, StringPool.BACK_SLASH, StringPool.SLASH);
276 
277         File file = new File(realPath);
278 
279         if (!file.exists()) {
280             return null;
281         }
282 
283         String minifiedContent = null;
284 
285         StringBundler sb = new StringBundler(4);
286 
287         sb.append(_tempDir);
288         sb.append(requestURI);
289 
290         String queryString = request.getQueryString();
291 
292         if (queryString != null) {
293             sb.append(_QUESTION_SEPARATOR);
294             sb.append(sterilizeQueryString(queryString));
295         }
296 
297         String cacheCommonFileName = sb.toString();
298 
299         File cacheContentTypeFile = new File(
300             cacheCommonFileName + "_E_CONTENT_TYPE");
301         File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
302 
303         if ((cacheDataFile.exists()) &&
304             (cacheDataFile.lastModified() >= file.lastModified())) {
305 
306             minifiedContent = FileUtil.read(cacheDataFile);
307 
308             if (cacheContentTypeFile.exists()) {
309                 String contentType = FileUtil.read(cacheContentTypeFile);
310 
311                 response.setContentType(contentType);
312             }
313         }
314         else {
315             if (realPath.endsWith(_CSS_EXTENSION)) {
316                 if (_log.isInfoEnabled()) {
317                     _log.info("Minifying CSS " + file);
318                 }
319 
320                 minifiedContent = minifyCss(request, file);
321 
322                 response.setContentType(ContentTypes.TEXT_CSS);
323 
324                 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
325             }
326             else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
327                 if (_log.isInfoEnabled()) {
328                     _log.info("Minifying JavaScript " + file);
329                 }
330 
331                 minifiedContent = minifyJavaScript(file);
332 
333                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
334 
335                 FileUtil.write(
336                     cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
337             }
338             else if (realPath.endsWith(_JSP_EXTENSION)) {
339                 if (_log.isInfoEnabled()) {
340                     _log.info("Minifying JSP " + file);
341                 }
342 
343                 StringServletResponse stringResponse =
344                     new StringServletResponse(response);
345 
346                 processFilter(
347                     MinifierFilter.class, request, stringResponse, filterChain);
348 
349                 CacheResponseUtil.setHeaders(
350                     response, stringResponse.getHeaders());
351 
352                 response.setContentType(stringResponse.getContentType());
353 
354                 minifiedContent = stringResponse.getString();
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, stringResponse.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 Exception {
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             ServletResponseUtil.write(response, minifiedContent);
425         }
426     }
427 
428     protected String sterilizeQueryString(String queryString) {
429         return StringUtil.replace(
430             queryString,
431             new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
432             new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
433     }
434 
435     private static final String _CSS_IMPORT_BEGIN = "@import url(";
436 
437     private static final String _CSS_IMPORT_END = ");";
438 
439     private static final String _CSS_EXTENSION = ".css";
440 
441     private static final String _JAVASCRIPT_EXTENSION = ".js";
442 
443     private static final String _JSP_EXTENSION = ".jsp";
444 
445     private static final String _QUESTION_SEPARATOR = "_Q_";
446 
447     private static final String _TEMP_DIR =
448         SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
449 
450     private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
451 
452     private static Pattern _pattern = Pattern.compile(
453         "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
454 
455     private ServletContext _servletContext;
456     private String _servletContextName;
457     private String _tempDir = _TEMP_DIR;
458 
459 }