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