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.log.Log;
26  import com.liferay.portal.kernel.log.LogFactoryUtil;
27  import com.liferay.portal.kernel.servlet.BrowserSniffer;
28  import com.liferay.portal.kernel.util.ContentTypes;
29  import com.liferay.portal.kernel.util.FileUtil;
30  import com.liferay.portal.kernel.util.GetterUtil;
31  import com.liferay.portal.kernel.util.ParamUtil;
32  import com.liferay.portal.kernel.util.StringPool;
33  import com.liferay.portal.kernel.util.StringUtil;
34  import com.liferay.portal.kernel.util.Validator;
35  import com.liferay.portal.servlet.filters.BasePortalFilter;
36  import com.liferay.portal.util.MinifierUtil;
37  import com.liferay.portal.util.PropsUtil;
38  import com.liferay.util.SystemProperties;
39  import com.liferay.util.servlet.ServletResponseUtil;
40  import com.liferay.util.servlet.filters.CacheResponse;
41  import com.liferay.util.servlet.filters.CacheResponseUtil;
42  
43  import java.io.File;
44  import java.io.IOException;
45  
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import javax.servlet.FilterChain;
50  import javax.servlet.FilterConfig;
51  import javax.servlet.ServletContext;
52  import javax.servlet.ServletException;
53  import javax.servlet.http.HttpServletRequest;
54  import javax.servlet.http.HttpServletResponse;
55  
56  /**
57   * <a href="MinifierFilter.java.html"><b><i>View Source</i></b></a>
58   *
59   * @author Brian Wing Shun Chan
60   *
61   */
62  public class MinifierFilter extends BasePortalFilter {
63  
64      public void init(FilterConfig filterConfig) {
65          super.init(filterConfig);
66  
67          _servletContext = filterConfig.getServletContext();
68          _servletContextName = GetterUtil.getString(
69              _servletContext.getServletContextName());
70  
71          if (Validator.isNull(_servletContextName)) {
72              _tempDir += "/portal";
73          }
74      }
75  
76      protected String aggregateCss(String dir, String content, int level)
77          throws IOException {
78  
79          StringBuilder sb = new StringBuilder(content.length());
80  
81          int pos = 0;
82  
83          while (true) {
84              int x = content.indexOf(_CSS_IMPORT_BEGIN, pos);
85              int y = content.indexOf(
86                  _CSS_IMPORT_END, x + _CSS_IMPORT_BEGIN.length());
87  
88              if ((x == -1) || (y == -1)) {
89                  sb.append(content.substring(pos, content.length()));
90  
91                  break;
92              }
93              else {
94                  sb.append(content.substring(pos, x));
95  
96                  String importFile = content.substring(
97                      x + _CSS_IMPORT_BEGIN.length(), y);
98  
99                  String importContent = FileUtil.read(
100                     dir + StringPool.SLASH + importFile);
101 
102                 String importFilePath = StringPool.BLANK;
103 
104                 if (importFile.lastIndexOf(StringPool.SLASH) != -1) {
105                     importFilePath = StringPool.SLASH + importFile.substring(
106                         0, importFile.lastIndexOf(StringPool.SLASH) + 1);
107                 }
108 
109                 importContent = aggregateCss(
110                     dir + importFilePath, importContent, level + 1);
111 
112                 // LEP-7540
113 
114                 String relativePath = StringPool.BLANK;
115 
116                 for (int i = 0; i < level; i++) {
117                     relativePath += "../";
118                 }
119 
120                 importContent = StringUtil.replace(
121                     importContent,
122                     new String[] {
123                         "url('" + relativePath,
124                         "url(\"" + relativePath,
125                         "url(" + relativePath
126                     },
127                     new String[] {
128                         "url('[$TEMP_RELATIVE_PATH$]",
129                         "url(\"[$TEMP_RELATIVE_PATH$]",
130                         "url([$TEMP_RELATIVE_PATH$]"
131                     });
132 
133                 importContent = StringUtil.replace(
134                     importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
135 
136                 sb.append(importContent);
137 
138                 pos = y + _CSS_IMPORT_END.length();
139             }
140         }
141 
142         return sb.toString();
143     }
144 
145     protected String getMinifiedBundleContent(
146             HttpServletRequest request, HttpServletResponse response)
147         throws IOException {
148 
149         String minifierType = ParamUtil.getString(request, "minifierType");
150         String minifierBundleId = ParamUtil.getString(
151             request, "minifierBundleId");
152         String minifierBundleDir = ParamUtil.getString(
153             request, "minifierBundleDir");
154 
155         if (Validator.isNull(minifierType) ||
156             Validator.isNull(minifierBundleId) ||
157             Validator.isNull(minifierBundleDir)) {
158 
159             return null;
160         }
161 
162         String bundleDirRealPath = _servletContext.getRealPath(
163             minifierBundleDir);
164 
165         if (bundleDirRealPath == null) {
166             return null;
167         }
168 
169         String cacheFileName = _tempDir + request.getRequestURI();
170 
171         String queryString = request.getQueryString();
172 
173         if (queryString != null) {
174             cacheFileName += _QUESTION_SEPARATOR + queryString;
175         }
176 
177         String[] fileNames = PropsUtil.getArray(minifierBundleId);
178 
179         File cacheFile = new File(cacheFileName);
180 
181         if (cacheFile.exists()) {
182             boolean staleCache = false;
183 
184             for (String fileName : fileNames) {
185                 File file = new File(
186                     bundleDirRealPath + StringPool.SLASH + fileName);
187 
188                 if (file.lastModified() > cacheFile.lastModified()) {
189                     staleCache = true;
190 
191                     break;
192                 }
193             }
194 
195             if (!staleCache) {
196                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
197 
198                 return FileUtil.read(cacheFile);
199             }
200         }
201 
202         if (_log.isInfoEnabled()) {
203             _log.info("Minifying JavaScript bundle " + minifierBundleId);
204         }
205 
206         StringBuilder sb = new StringBuilder();
207 
208         for (String fileName : fileNames) {
209             String content = FileUtil.read(
210                 bundleDirRealPath + StringPool.SLASH + fileName);
211 
212             sb.append(content);
213             sb.append(StringPool.NEW_LINE);
214         }
215 
216         String minifiedContent = minifyJavaScript(sb.toString());
217 
218         response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
219 
220         FileUtil.write(cacheFile, minifiedContent);
221 
222         return minifiedContent;
223     }
224 
225     protected String getMinifiedContent(
226             HttpServletRequest request, HttpServletResponse response,
227             FilterChain filterChain)
228         throws IOException, ServletException {
229 
230         String minifierType = ParamUtil.getString(request, "minifierType");
231         String minifierBundleId = ParamUtil.getString(
232             request, "minifierBundleId");
233         String minifierBundleDir = ParamUtil.getString(
234             request, "minifierBundleDir");
235 
236         if (Validator.isNull(minifierType) ||
237             Validator.isNotNull(minifierBundleId) ||
238             Validator.isNotNull(minifierBundleDir)) {
239 
240             return null;
241         }
242 
243         String requestURI = request.getRequestURI();
244 
245         String realPath = StringUtil.replace(
246             _servletContext.getRealPath(requestURI), StringPool.BACK_SLASH,
247             StringPool.SLASH);
248 
249         if (realPath == null) {
250             return null;
251         }
252 
253         File file = new File(realPath);
254 
255         if (!file.exists()) {
256 
257             // Tomcat incorrectly returns the a real path to a resource that
258             // exists in another web application. For example, it returns
259             // ".../webapps/abc-theme/abc-theme/css/main.css" instead of
260             // ".../webapps/abc-theme/css/main.css".
261 
262             if (Validator.isNotNull(_servletContextName)) {
263                 realPath = StringUtil.replaceFirst(
264                     realPath, StringPool.SLASH + _servletContextName,
265                     StringPool.BLANK);
266 
267                 file = new File(realPath);
268             }
269         }
270 
271         if (!file.exists()) {
272             return null;
273         }
274 
275         String minifiedContent = null;
276 
277         String cacheCommonFileName = _tempDir + requestURI;
278 
279         String queryString = request.getQueryString();
280 
281         if (queryString != null) {
282             cacheCommonFileName += _QUESTION_SEPARATOR + queryString;
283         }
284 
285         File cacheContentTypeFile = new File(
286             cacheCommonFileName + "_E_CONTENT_TYPE");
287         File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
288 
289         if ((cacheDataFile.exists()) &&
290             (cacheDataFile.lastModified() >= file.lastModified())) {
291 
292             minifiedContent = FileUtil.read(cacheDataFile);
293 
294             if (cacheContentTypeFile.exists()) {
295                 String contentType = FileUtil.read(cacheContentTypeFile);
296 
297                 response.setContentType(contentType);
298             }
299         }
300         else {
301             if (realPath.endsWith(_CSS_EXTENSION)) {
302                 if (_log.isInfoEnabled()) {
303                     _log.info("Minifying CSS " + file);
304                 }
305 
306                 minifiedContent = minifyCss(request, file);
307 
308                 response.setContentType(ContentTypes.TEXT_CSS);
309 
310                 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
311             }
312             else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
313                 if (_log.isInfoEnabled()) {
314                     _log.info("Minifying JavaScript " + file);
315                 }
316 
317                 minifiedContent = minifyJavaScript(file);
318 
319                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
320 
321                 FileUtil.write(
322                     cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
323             }
324             else if (realPath.endsWith(_JSP_EXTENSION)) {
325                 if (_log.isInfoEnabled()) {
326                     _log.info("Minifying JSP " + file);
327                 }
328 
329                 CacheResponse cacheResponse = new CacheResponse(
330                     response, StringPool.UTF8);
331 
332                 processFilter(
333                     MinifierFilter.class, request, cacheResponse, filterChain);
334 
335                 CacheResponseUtil.addHeaders(
336                     response, cacheResponse.getHeaders());
337 
338                 response.setContentType(cacheResponse.getContentType());
339 
340                 minifiedContent = new String(
341                     cacheResponse.getData(), StringPool.UTF8);
342 
343                 if (minifierType.equals("css")) {
344                     minifiedContent = minifyCss(request, minifiedContent);
345                 }
346                 else if (minifierType.equals("js")) {
347                     minifiedContent = minifyJavaScript(minifiedContent);
348                 }
349 
350                 FileUtil.write(
351                     cacheContentTypeFile, cacheResponse.getContentType());
352             }
353             else {
354                 return null;
355             }
356 
357             FileUtil.write(cacheDataFile, minifiedContent);
358         }
359 
360         return minifiedContent;
361     }
362 
363     protected String minifyCss(HttpServletRequest request, File file)
364         throws IOException {
365 
366         String content = FileUtil.read(file);
367 
368         content = aggregateCss(file.getParent(), content, 0);
369 
370         return minifyCss(request, content);
371     }
372 
373     protected String minifyCss(HttpServletRequest request, String content)
374         throws IOException {
375 
376         String browserId = ParamUtil.getString(request, "browserId");
377 
378         if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
379             Matcher matcher = _pattern.matcher(content);
380 
381             content = matcher.replaceAll(StringPool.BLANK);
382         }
383 
384         return MinifierUtil.minifyCss(content);
385     }
386 
387     protected String minifyJavaScript(File file) throws IOException {
388         String content = FileUtil.read(file);
389 
390         return minifyJavaScript(content);
391     }
392 
393     protected String minifyJavaScript(String content) throws IOException {
394         return MinifierUtil.minifyJavaScript(content);
395     }
396 
397     protected void processFilter(
398             HttpServletRequest request, HttpServletResponse response,
399             FilterChain filterChain)
400         throws IOException, ServletException {
401 
402         String minifiedContent = getMinifiedContent(
403             request, response, filterChain);
404 
405         if (Validator.isNull(minifiedContent)) {
406             minifiedContent = getMinifiedBundleContent(request, response);
407         }
408 
409         if (Validator.isNull(minifiedContent)) {
410             processFilter(MinifierFilter.class, request, response, filterChain);
411         }
412         else {
413             ServletResponseUtil.write(response, minifiedContent);
414         }
415     }
416 
417     private static final String _CSS_IMPORT_BEGIN = "@import url(";
418 
419     private static final String _CSS_IMPORT_END = ");";
420 
421     private static final String _CSS_EXTENSION = ".css";
422 
423     private static final String _JAVASCRIPT_EXTENSION = ".js";
424 
425     private static final String _JSP_EXTENSION = ".jsp";
426 
427     private static final String _QUESTION_SEPARATOR = "_Q_";
428 
429     private static final String _TEMP_DIR =
430         SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
431 
432     private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
433 
434     private static Pattern _pattern = Pattern.compile(
435         "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
436 
437     private ServletContext _servletContext;
438     private String _servletContextName;
439     private String _tempDir = _TEMP_DIR;
440 
441 }