001
014
015 package com.liferay.portal.servlet.filters.minifier;
016
017 import com.liferay.portal.kernel.cache.key.CacheKeyGenerator;
018 import com.liferay.portal.kernel.cache.key.CacheKeyGeneratorUtil;
019 import com.liferay.portal.kernel.configuration.Filter;
020 import com.liferay.portal.kernel.log.Log;
021 import com.liferay.portal.kernel.log.LogFactoryUtil;
022 import com.liferay.portal.kernel.servlet.BrowserSniffer;
023 import com.liferay.portal.kernel.servlet.HttpHeaders;
024 import com.liferay.portal.kernel.servlet.ServletContextUtil;
025 import com.liferay.portal.kernel.servlet.ServletResponseUtil;
026 import com.liferay.portal.kernel.servlet.StringServletResponse;
027 import com.liferay.portal.kernel.util.ArrayUtil;
028 import com.liferay.portal.kernel.util.CharPool;
029 import com.liferay.portal.kernel.util.ContentTypes;
030 import com.liferay.portal.kernel.util.FileUtil;
031 import com.liferay.portal.kernel.util.GetterUtil;
032 import com.liferay.portal.kernel.util.ParamUtil;
033 import com.liferay.portal.kernel.util.PropsKeys;
034 import com.liferay.portal.kernel.util.StringBundler;
035 import com.liferay.portal.kernel.util.StringPool;
036 import com.liferay.portal.kernel.util.StringUtil;
037 import com.liferay.portal.kernel.util.SystemProperties;
038 import com.liferay.portal.kernel.util.Validator;
039 import com.liferay.portal.servlet.filters.BasePortalFilter;
040 import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
041 import com.liferay.portal.util.JavaScriptBundleUtil;
042 import com.liferay.portal.util.LimitedFilesCache;
043 import com.liferay.portal.util.MinifierUtil;
044 import com.liferay.portal.util.PropsUtil;
045 import com.liferay.portal.util.PropsValues;
046 import com.liferay.util.servlet.filters.CacheResponseUtil;
047
048 import java.io.File;
049 import java.io.IOException;
050
051 import java.net.URL;
052 import java.net.URLConnection;
053
054 import java.util.regex.Matcher;
055 import java.util.regex.Pattern;
056
057 import javax.servlet.FilterChain;
058 import javax.servlet.FilterConfig;
059 import javax.servlet.ServletContext;
060 import javax.servlet.http.HttpServletRequest;
061 import javax.servlet.http.HttpServletResponse;
062
063
066 public class MinifierFilter extends BasePortalFilter {
067
068
071 public static String aggregateCss(String dir, String content)
072 throws IOException {
073
074 StringBundler sb = new StringBundler();
075
076 int pos = 0;
077
078 while (true) {
079 int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos);
080 int commentY = content.indexOf(
081 _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length());
082
083 int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
084 int importY = content.indexOf(
085 _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
086
087 if ((importX == -1) || (importY == -1)) {
088 sb.append(content.substring(pos));
089
090 break;
091 }
092 else if ((commentX != -1) && (commentY != -1) &&
093 (commentX < importX) && (commentY > importX)) {
094
095 commentY += _CSS_COMMENT_END.length();
096
097 sb.append(content.substring(pos, commentY));
098
099 pos = commentY;
100 }
101 else {
102 sb.append(content.substring(pos, importX));
103
104 String mediaQuery = StringPool.BLANK;
105
106 int mediaQueryImportX = content.indexOf(
107 CharPool.CLOSE_PARENTHESIS,
108 importX + _CSS_IMPORT_BEGIN.length());
109 int mediaQueryImportY = content.indexOf(
110 CharPool.SEMICOLON, importX + _CSS_IMPORT_BEGIN.length());
111
112 String importFileName = null;
113
114 if (importY != mediaQueryImportX) {
115 mediaQuery = content.substring(
116 mediaQueryImportX + 1, mediaQueryImportY);
117
118 importFileName = content.substring(
119 importX + _CSS_IMPORT_BEGIN.length(),
120 mediaQueryImportX);
121 }
122 else {
123 importFileName = content.substring(
124 importX + _CSS_IMPORT_BEGIN.length(), importY);
125 }
126
127 String importFullFileName = dir.concat(StringPool.SLASH).concat(
128 importFileName);
129
130 String importContent = null;
131
132 if (Validator.isUrl(importFileName)) {
133 URL resourceURL = new URL(importFileName);
134
135 URLConnection urlConnection = resourceURL.openConnection();
136
137 importContent = StringUtil.read(
138 urlConnection.getInputStream());
139 }
140 else {
141 importContent = FileUtil.read(importFullFileName);
142 }
143
144 if (importContent == null) {
145 if (_log.isWarnEnabled()) {
146 _log.warn(
147 "File " + importFullFileName + " does not exist");
148 }
149
150 importContent = StringPool.BLANK;
151 }
152
153 String importDir = StringPool.BLANK;
154
155 int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
156
157 if (slashPos != -1) {
158 importDir = StringPool.SLASH.concat(
159 importFileName.substring(0, slashPos + 1));
160 }
161
162 importContent = aggregateCss(dir + importDir, importContent);
163
164 int importDepth = StringUtil.count(
165 importFileName, StringPool.SLASH);
166
167
168
169 String relativePath = StringPool.BLANK;
170
171 for (int i = 0; i < importDepth; i++) {
172 relativePath += "../";
173 }
174
175 importContent = StringUtil.replace(
176 importContent,
177 new String[] {
178 "url('" + relativePath, "url(\"" + relativePath,
179 "url(" + relativePath
180 },
181 new String[] {
182 "url('[$TEMP_RELATIVE_PATH$]",
183 "url(\"[$TEMP_RELATIVE_PATH$]",
184 "url([$TEMP_RELATIVE_PATH$]"
185 });
186
187 importContent = StringUtil.replace(
188 importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
189
190 if (Validator.isNotNull(mediaQuery)) {
191 sb.append(_CSS_MEDIA_QUERY);
192 sb.append(CharPool.SPACE);
193 sb.append(mediaQuery);
194 sb.append(CharPool.OPEN_CURLY_BRACE);
195 sb.append(importContent);
196 sb.append(CharPool.CLOSE_CURLY_BRACE);
197
198 pos = mediaQueryImportY + 1;
199 }
200 else {
201 sb.append(importContent);
202
203 pos = importY + _CSS_IMPORT_END.length();
204 }
205 }
206 }
207
208 return sb.toString();
209 }
210
211 @Override
212 public void init(FilterConfig filterConfig) {
213 super.init(filterConfig);
214
215 _servletContext = filterConfig.getServletContext();
216 _servletContextName = GetterUtil.getString(
217 _servletContext.getServletContextName());
218
219 if (Validator.isNull(_servletContextName)) {
220 _tempDir += "/portal";
221 }
222
223 if (PropsValues.MINIFIER_FILES_LIMIT > 0) {
224 _limitedFilesCache = new LimitedFilesCache<String>(
225 PropsValues.MINIFIER_FILES_LIMIT);
226
227 if (_log.isDebugEnabled()) {
228 _log.debug(
229 "Minifier files limit " + PropsValues.MINIFIER_FILES_LIMIT);
230 }
231 }
232 }
233
234 protected String getCacheFileName(HttpServletRequest request) {
235 CacheKeyGenerator cacheKeyGenerator =
236 CacheKeyGeneratorUtil.getCacheKeyGenerator(
237 MinifierFilter.class.getName());
238
239 cacheKeyGenerator.append(request.getRequestURI());
240
241 String queryString = request.getQueryString();
242
243 if (queryString != null) {
244 cacheKeyGenerator.append(sterilizeQueryString(queryString));
245 }
246
247 String cacheKey = String.valueOf(cacheKeyGenerator.finish());
248
249 return _tempDir.concat(StringPool.SLASH).concat(cacheKey);
250 }
251
252 protected Object getMinifiedBundleContent(
253 HttpServletRequest request, HttpServletResponse response)
254 throws IOException {
255
256 String minifierType = ParamUtil.getString(request, "minifierType");
257 String minifierBundleId = ParamUtil.getString(
258 request, "minifierBundleId");
259
260 if (Validator.isNull(minifierType) ||
261 Validator.isNull(minifierBundleId) ||
262 !ArrayUtil.contains(
263 PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
264
265 return null;
266 }
267
268 String minifierBundleDir = PropsUtil.get(
269 PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
270
271 String bundleDirRealPath = ServletContextUtil.getRealPath(
272 _servletContext, minifierBundleDir);
273
274 if (bundleDirRealPath == null) {
275 return null;
276 }
277
278 String cacheFileName = getCacheFileName(request);
279
280 String[] fileNames = JavaScriptBundleUtil.getFileNames(
281 minifierBundleId);
282
283 File cacheFile = new File(cacheFileName);
284
285 if (_limitedFilesCache != null) {
286 _limitedFilesCache.put(cacheFileName);
287 }
288
289 if (cacheFile.exists()) {
290 boolean staleCache = false;
291
292 for (String fileName : fileNames) {
293 File file = new File(
294 bundleDirRealPath + StringPool.SLASH + fileName);
295
296 if (file.lastModified() > cacheFile.lastModified()) {
297 staleCache = true;
298
299 break;
300 }
301 }
302
303 if (!staleCache) {
304 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
305
306 return cacheFile;
307 }
308 }
309
310 if (_log.isInfoEnabled()) {
311 _log.info("Minifying JavaScript bundle " + minifierBundleId);
312 }
313
314 String minifiedContent = null;
315
316 if (fileNames.length == 0) {
317 minifiedContent = StringPool.BLANK;
318 }
319 else {
320 StringBundler sb = new StringBundler(fileNames.length * 2);
321
322 for (String fileName : fileNames) {
323 String content = FileUtil.read(
324 bundleDirRealPath + StringPool.SLASH + fileName);
325
326 sb.append(content);
327 sb.append(StringPool.NEW_LINE);
328 }
329
330 minifiedContent = minifyJavaScript(sb.toString());
331 }
332
333 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
334
335 FileUtil.write(cacheFile, minifiedContent);
336
337 return minifiedContent;
338 }
339
340 protected Object getMinifiedContent(
341 HttpServletRequest request, HttpServletResponse response,
342 FilterChain filterChain)
343 throws Exception {
344
345 String minifierType = ParamUtil.getString(request, "minifierType");
346 String minifierBundleId = ParamUtil.getString(
347 request, "minifierBundleId");
348 String minifierBundleDir = ParamUtil.getString(
349 request, "minifierBundleDir");
350
351 if (Validator.isNull(minifierType) ||
352 Validator.isNotNull(minifierBundleId) ||
353 Validator.isNotNull(minifierBundleDir)) {
354
355 return null;
356 }
357
358 String requestURI = request.getRequestURI();
359
360 String requestPath = requestURI;
361
362 String contextPath = request.getContextPath();
363
364 if (!contextPath.equals(StringPool.SLASH)) {
365 requestPath = requestPath.substring(contextPath.length());
366 }
367
368 String realPath = ServletContextUtil.getRealPath(
369 _servletContext, requestPath);
370
371 if (realPath == null) {
372 return null;
373 }
374
375 realPath = StringUtil.replace(
376 realPath, CharPool.BACK_SLASH, CharPool.SLASH);
377
378 File file = new File(realPath);
379
380 if (!file.exists()) {
381 return null;
382 }
383
384 String cacheCommonFileName = getCacheFileName(request);
385
386 File cacheContentTypeFile = new File(
387 cacheCommonFileName + "_E_CONTENT_TYPE");
388 File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
389
390 if (cacheDataFile.exists() &&
391 (cacheDataFile.lastModified() >= file.lastModified())) {
392
393 if (cacheContentTypeFile.exists()) {
394 String contentType = FileUtil.read(cacheContentTypeFile);
395
396 response.setContentType(contentType);
397 }
398
399 return cacheDataFile;
400 }
401
402 String minifiedContent = null;
403
404 if (realPath.endsWith(_CSS_EXTENSION)) {
405 if (_log.isInfoEnabled()) {
406 _log.info("Minifying CSS " + file);
407 }
408
409 minifiedContent = minifyCss(request, response, file);
410
411 response.setContentType(ContentTypes.TEXT_CSS);
412
413 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
414 }
415 else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
416 if (_log.isInfoEnabled()) {
417 _log.info("Minifying JavaScript " + file);
418 }
419
420 minifiedContent = minifyJavaScript(file);
421
422 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
423
424 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
425 }
426 else if (realPath.endsWith(_JSP_EXTENSION)) {
427 if (_log.isInfoEnabled()) {
428 _log.info("Minifying JSP " + file);
429 }
430
431 StringServletResponse stringResponse = new StringServletResponse(
432 response);
433
434 processFilter(
435 MinifierFilter.class, request, stringResponse, filterChain);
436
437 CacheResponseUtil.setHeaders(response, stringResponse.getHeaders());
438
439 response.setContentType(stringResponse.getContentType());
440
441 minifiedContent = stringResponse.getString();
442
443 if (minifierType.equals("css")) {
444 minifiedContent = minifyCss(
445 request, response, realPath, minifiedContent);
446 }
447 else if (minifierType.equals("js")) {
448 minifiedContent = minifyJavaScript(minifiedContent);
449 }
450
451 FileUtil.write(
452 cacheContentTypeFile, stringResponse.getContentType());
453 }
454 else {
455 return null;
456 }
457
458 FileUtil.write(cacheDataFile, minifiedContent);
459
460 return minifiedContent;
461 }
462
463 protected String minifyCss(
464 HttpServletRequest request, HttpServletResponse response, File file)
465 throws IOException {
466
467 String content = FileUtil.read(file);
468
469 content = aggregateCss(file.getParent(), content);
470
471 return minifyCss(request, response, file.getAbsolutePath(), content);
472 }
473
474 protected String minifyCss(
475 HttpServletRequest request, HttpServletResponse response,
476 String cssRealPath, String content) {
477
478 try {
479 content = DynamicCSSUtil.parseSass(request, cssRealPath, content);
480 }
481 catch (Exception e) {
482 _log.error("Unable to parse SASS on CSS " + cssRealPath, e);
483
484 if (_log.isDebugEnabled()) {
485 _log.debug(content);
486 }
487
488 response.setHeader(
489 HttpHeaders.CACHE_CONTROL,
490 HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
491 }
492
493 String browserId = ParamUtil.getString(request, "browserId");
494
495 if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
496 Matcher matcher = _pattern.matcher(content);
497
498 content = matcher.replaceAll(StringPool.BLANK);
499 }
500
501 return MinifierUtil.minifyCss(content);
502 }
503
504 protected String minifyJavaScript(File file) throws IOException {
505 String content = FileUtil.read(file);
506
507 return minifyJavaScript(content);
508 }
509
510 protected String minifyJavaScript(String content) {
511 return MinifierUtil.minifyJavaScript(content);
512 }
513
514 @Override
515 protected void processFilter(
516 HttpServletRequest request, HttpServletResponse response,
517 FilterChain filterChain)
518 throws Exception {
519
520 Object minifiedContent = getMinifiedContent(
521 request, response, filterChain);
522
523 if (minifiedContent == null) {
524 minifiedContent = getMinifiedBundleContent(request, response);
525 }
526
527 if (minifiedContent == null) {
528 processFilter(MinifierFilter.class, request, response, filterChain);
529 }
530 else {
531 if (minifiedContent instanceof File) {
532 ServletResponseUtil.write(response, (File)minifiedContent);
533 }
534 else if (minifiedContent instanceof String) {
535 ServletResponseUtil.write(response, (String)minifiedContent);
536 }
537 }
538 }
539
540 protected String sterilizeQueryString(String queryString) {
541 return StringUtil.replace(
542 queryString, new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
543 new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
544 }
545
546 private static final String _CSS_COMMENT_BEGIN = "";
549
550 private static final String _CSS_EXTENSION = ".css";
551
552 private static final String _CSS_IMPORT_BEGIN = "@import url(";
553
554 private static final String _CSS_IMPORT_END = ");";
555
556 private static final String _CSS_MEDIA_QUERY = "@media";
557
558 private static final String _JAVASCRIPT_EXTENSION = ".js";
559
560 private static final String _JSP_EXTENSION = ".jsp";
561
562 private static final String _TEMP_DIR =
563 SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
564
565 private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
566
567 private static Pattern _pattern = Pattern.compile(
568 "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
569
570 private LimitedFilesCache<String> _limitedFilesCache;
571 private ServletContext _servletContext;
572 private String _servletContextName;
573 private String _tempDir = _TEMP_DIR;
574
575 }