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