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.util.ArrayUtil;
23  import com.liferay.portal.kernel.util.ContentTypes;
24  import com.liferay.portal.kernel.util.FileUtil;
25  import com.liferay.portal.kernel.util.GetterUtil;
26  import com.liferay.portal.kernel.util.ParamUtil;
27  import com.liferay.portal.kernel.util.PropsKeys;
28  import com.liferay.portal.kernel.util.StringBundler;
29  import com.liferay.portal.kernel.util.StringPool;
30  import com.liferay.portal.kernel.util.StringUtil;
31  import com.liferay.portal.kernel.util.Validator;
32  import com.liferay.portal.servlet.filters.BasePortalFilter;
33  import com.liferay.portal.servlet.filters.etag.ETagUtil;
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.CacheResponse;
40  import com.liferay.util.servlet.filters.CacheResponseUtil;
41  
42  import java.io.File;
43  import java.io.IOException;
44  
45  import java.util.regex.Matcher;
46  import java.util.regex.Pattern;
47  
48  import javax.servlet.FilterChain;
49  import javax.servlet.FilterConfig;
50  import javax.servlet.ServletContext;
51  import javax.servlet.http.HttpServletRequest;
52  import javax.servlet.http.HttpServletResponse;
53  
54  /**
55   * <a href="MinifierFilter.java.html"><b><i>View Source</i></b></a>
56   *
57   * @author Brian Wing Shun Chan
58   */
59  public class MinifierFilter extends BasePortalFilter {
60  
61      public void init(FilterConfig filterConfig) {
62          super.init(filterConfig);
63  
64          _servletContext = filterConfig.getServletContext();
65          _servletContextName = GetterUtil.getString(
66              _servletContext.getServletContextName());
67  
68          if (Validator.isNull(_servletContextName)) {
69              _tempDir += "/portal";
70          }
71      }
72  
73      protected String aggregateCss(String dir, String content)
74          throws IOException {
75  
76          StringBuilder sb = new StringBuilder(content.length());
77  
78          int pos = 0;
79  
80          while (true) {
81              int x = content.indexOf(_CSS_IMPORT_BEGIN, pos);
82              int y = content.indexOf(
83                  _CSS_IMPORT_END, x + _CSS_IMPORT_BEGIN.length());
84  
85              if ((x == -1) || (y == -1)) {
86                  sb.append(content.substring(pos, content.length()));
87  
88                  break;
89              }
90              else {
91                  sb.append(content.substring(pos, x));
92  
93                  String importFile = content.substring(
94                      x + _CSS_IMPORT_BEGIN.length(), y);
95  
96                  String importContent = FileUtil.read(
97                      dir + StringPool.SLASH + importFile);
98  
99                  String importFilePath = StringPool.BLANK;
100 
101                 if (importFile.lastIndexOf(StringPool.SLASH) != -1) {
102                     importFilePath = StringPool.SLASH + importFile.substring(
103                         0, importFile.lastIndexOf(StringPool.SLASH) + 1);
104                 }
105 
106                 importContent = aggregateCss(
107                     dir + importFilePath, importContent);
108 
109                 int importDepth = StringUtil.count(
110                     importFile, StringPool.SLASH);
111 
112                 // LEP-7540
113 
114                 String relativePath = StringPool.BLANK;
115 
116                 for (int i = 0; i < importDepth; 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 
153         if (Validator.isNull(minifierType) ||
154             Validator.isNull(minifierBundleId) ||
155             !ArrayUtil.contains(
156                 PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
157 
158             return null;
159         }
160 
161         String minifierBundleDir = PropsUtil.get(
162             PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
163 
164         String bundleDirRealPath = ServletContextUtil.getRealPath(
165             _servletContext, minifierBundleDir);
166 
167         if (bundleDirRealPath == null) {
168             return null;
169         }
170 
171         StringBundler sb = new StringBundler(4);
172 
173         sb.append(_tempDir);
174         sb.append(request.getRequestURI());
175 
176         String queryString = request.getQueryString();
177 
178         if (queryString != null) {
179             sb.append(_QUESTION_SEPARATOR);
180             sb.append(sterilizeQueryString(queryString));
181         }
182 
183         String cacheFileName = sb.toString();
184 
185         String[] fileNames = PropsUtil.getArray(minifierBundleId);
186 
187         File cacheFile = new File(cacheFileName);
188 
189         if (cacheFile.exists()) {
190             boolean staleCache = false;
191 
192             for (String fileName : fileNames) {
193                 File file = new File(
194                     bundleDirRealPath + StringPool.SLASH + fileName);
195 
196                 if (file.lastModified() > cacheFile.lastModified()) {
197                     staleCache = true;
198 
199                     break;
200                 }
201             }
202 
203             if (!staleCache) {
204                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
205 
206                 return FileUtil.read(cacheFile);
207             }
208         }
209 
210         if (_log.isInfoEnabled()) {
211             _log.info("Minifying JavaScript bundle " + minifierBundleId);
212         }
213 
214         String minifiedContent = null;
215 
216         if (fileNames.length == 0) {
217             minifiedContent = StringPool.BLANK;
218         }
219         else {
220             sb = new StringBundler(fileNames.length * 2);
221 
222             for (String fileName : fileNames) {
223                 String content = FileUtil.read(
224                     bundleDirRealPath + StringPool.SLASH + fileName);
225 
226                 sb.append(content);
227                 sb.append(StringPool.NEW_LINE);
228             }
229 
230             minifiedContent = minifyJavaScript(sb.toString());
231         }
232 
233         response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
234 
235         FileUtil.write(cacheFile, minifiedContent);
236 
237         return minifiedContent;
238     }
239 
240     protected String getMinifiedContent(
241             HttpServletRequest request, HttpServletResponse response,
242             FilterChain filterChain)
243         throws Exception {
244 
245         String minifierType = ParamUtil.getString(request, "minifierType");
246         String minifierBundleId = ParamUtil.getString(
247             request, "minifierBundleId");
248         String minifierBundleDir = ParamUtil.getString(
249             request, "minifierBundleDir");
250 
251         if (Validator.isNull(minifierType) ||
252             Validator.isNotNull(minifierBundleId) ||
253             Validator.isNotNull(minifierBundleDir)) {
254 
255             return null;
256         }
257 
258         String requestURI = request.getRequestURI();
259 
260         String requestPath = requestURI;
261 
262         String contextPath = request.getContextPath();
263 
264         if (!contextPath.equals(StringPool.SLASH)) {
265             requestPath = requestPath.substring(contextPath.length());
266         }
267 
268         String realPath = ServletContextUtil.getRealPath(
269             _servletContext, requestPath);
270 
271         if (realPath == null) {
272             return null;
273         }
274 
275         realPath = StringUtil.replace(
276             realPath, StringPool.BACK_SLASH, StringPool.SLASH);
277 
278         File file = new File(realPath);
279 
280         if (!file.exists()) {
281             return null;
282         }
283 
284         String minifiedContent = null;
285 
286         StringBundler sb = new StringBundler(4);
287 
288         sb.append(_tempDir);
289         sb.append(requestURI);
290 
291         String queryString = request.getQueryString();
292 
293         if (queryString != null) {
294             sb.append(_QUESTION_SEPARATOR);
295             sb.append(sterilizeQueryString(queryString));
296         }
297 
298         String cacheCommonFileName = sb.toString();
299 
300         File cacheContentTypeFile = new File(
301             cacheCommonFileName + "_E_CONTENT_TYPE");
302         File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
303 
304         if ((cacheDataFile.exists()) &&
305             (cacheDataFile.lastModified() >= file.lastModified())) {
306 
307             minifiedContent = FileUtil.read(cacheDataFile);
308 
309             if (cacheContentTypeFile.exists()) {
310                 String contentType = FileUtil.read(cacheContentTypeFile);
311 
312                 response.setContentType(contentType);
313             }
314         }
315         else {
316             if (realPath.endsWith(_CSS_EXTENSION)) {
317                 if (_log.isInfoEnabled()) {
318                     _log.info("Minifying CSS " + file);
319                 }
320 
321                 minifiedContent = minifyCss(request, file);
322 
323                 response.setContentType(ContentTypes.TEXT_CSS);
324 
325                 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
326             }
327             else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
328                 if (_log.isInfoEnabled()) {
329                     _log.info("Minifying JavaScript " + file);
330                 }
331 
332                 minifiedContent = minifyJavaScript(file);
333 
334                 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
335 
336                 FileUtil.write(
337                     cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
338             }
339             else if (realPath.endsWith(_JSP_EXTENSION)) {
340                 if (_log.isInfoEnabled()) {
341                     _log.info("Minifying JSP " + file);
342                 }
343 
344                 CacheResponse cacheResponse = new CacheResponse(
345                     response, StringPool.UTF8);
346 
347                 processFilter(
348                     MinifierFilter.class, request, cacheResponse, filterChain);
349 
350                 CacheResponseUtil.addHeaders(
351                     response, cacheResponse.getHeaders());
352 
353                 response.setContentType(cacheResponse.getContentType());
354 
355                 minifiedContent = new String(
356                     cacheResponse.unsafeGetData(), 0,
357                     cacheResponse.getContentLength(), 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     protected String sterilizeQueryString(String queryString) {
434         return StringUtil.replace(
435             queryString,
436             new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
437             new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
438     }
439 
440     private static final String _CSS_IMPORT_BEGIN = "@import url(";
441 
442     private static final String _CSS_IMPORT_END = ");";
443 
444     private static final String _CSS_EXTENSION = ".css";
445 
446     private static final String _JAVASCRIPT_EXTENSION = ".js";
447 
448     private static final String _JSP_EXTENSION = ".jsp";
449 
450     private static final String _QUESTION_SEPARATOR = "_Q_";
451 
452     private static final String _TEMP_DIR =
453         SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
454 
455     private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
456 
457     private static Pattern _pattern = Pattern.compile(
458         "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
459 
460     private ServletContext _servletContext;
461     private String _servletContextName;
462     private String _tempDir = _TEMP_DIR;
463 
464 }