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