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