001
014
015 package com.liferay.portal.servlet.filters.aggregate;
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.BufferCacheServletResponse;
024 import com.liferay.portal.kernel.servlet.HttpHeaders;
025 import com.liferay.portal.kernel.servlet.ServletResponseUtil;
026 import com.liferay.portal.kernel.util.ArrayUtil;
027 import com.liferay.portal.kernel.util.CharPool;
028 import com.liferay.portal.kernel.util.ContentTypes;
029 import com.liferay.portal.kernel.util.FileUtil;
030 import com.liferay.portal.kernel.util.JavaConstants;
031 import com.liferay.portal.kernel.util.ParamUtil;
032 import com.liferay.portal.kernel.util.PropsKeys;
033 import com.liferay.portal.kernel.util.StringBundler;
034 import com.liferay.portal.kernel.util.StringPool;
035 import com.liferay.portal.kernel.util.StringUtil;
036 import com.liferay.portal.kernel.util.Validator;
037 import com.liferay.portal.servlet.filters.IgnoreModuleRequestFilter;
038 import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
039 import com.liferay.portal.util.JavaScriptBundleUtil;
040 import com.liferay.portal.util.LimitedFilesCache;
041 import com.liferay.portal.util.MinifierUtil;
042 import com.liferay.portal.util.PropsUtil;
043 import com.liferay.portal.util.PropsValues;
044
045 import java.io.File;
046 import java.io.IOException;
047
048 import java.net.URL;
049 import java.net.URLConnection;
050
051 import java.util.regex.Matcher;
052 import java.util.regex.Pattern;
053
054 import javax.servlet.FilterChain;
055 import javax.servlet.FilterConfig;
056 import javax.servlet.ServletContext;
057 import javax.servlet.http.HttpServletRequest;
058 import javax.servlet.http.HttpServletResponse;
059
060
065 public class AggregateFilter extends IgnoreModuleRequestFilter {
066
067
070 public static String aggregateCss(
071 AggregateContext aggregateContext, 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 importContent = aggregateContext.getContent(
128 importFileName);
129
130 if (importContent == null) {
131 if (_log.isWarnEnabled()) {
132 _log.warn(
133 "File " +
134 aggregateContext.getFullPath(importFileName) +
135 " does not exist");
136 }
137
138 importContent = StringPool.BLANK;
139 }
140
141 String importDirName = StringPool.BLANK;
142
143 int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
144
145 if (slashPos != -1) {
146 importDirName = importFileName.substring(0, slashPos + 1);
147 }
148
149 aggregateContext.pushPath(importDirName);
150
151 importContent = aggregateCss(aggregateContext, importContent);
152
153 if (Validator.isNotNull(importDirName)) {
154 aggregateContext.popPath();
155 }
156
157
158
159 String baseURL = _BASE_URL;
160
161 baseURL = baseURL.concat(
162 aggregateContext.getResourcePath(StringPool.BLANK));
163 baseURL = baseURL.concat(importDirName);
164
165 importContent = updateCssRelativeUrls(importContent, baseURL);
166
167 if (Validator.isNotNull(mediaQuery)) {
168 sb.append(_CSS_MEDIA_QUERY);
169 sb.append(CharPool.SPACE);
170 sb.append(mediaQuery);
171 sb.append(CharPool.OPEN_CURLY_BRACE);
172 sb.append(importContent);
173 sb.append(CharPool.CLOSE_CURLY_BRACE);
174
175 pos = mediaQueryImportY + 1;
176 }
177 else {
178 sb.append(importContent);
179
180 pos = importY + _CSS_IMPORT_END.length();
181 }
182 }
183 }
184
185 return sb.toString();
186 }
187
188 public static String aggregateJavaScript(
189 AggregateContext aggregateContext, String[] fileNames) {
190
191 StringBundler sb = new StringBundler(fileNames.length * 2);
192
193 for (String fileName : fileNames) {
194 String content = aggregateContext.getContent(fileName);
195
196 if (Validator.isNull(content)) {
197 continue;
198 }
199
200 sb.append(content);
201 sb.append(StringPool.NEW_LINE);
202 }
203
204 return getJavaScriptContent(sb.toString());
205 }
206
207 @Override
208 public void init(FilterConfig filterConfig) {
209 super.init(filterConfig);
210
211 _servletContext = filterConfig.getServletContext();
212
213 File tempDir = (File)_servletContext.getAttribute(
214 JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
215
216 _tempDir = new File(tempDir, _TEMP_DIR);
217
218 _tempDir.mkdirs();
219
220 if (PropsValues.MINIFIER_FILES_LIMIT > 0) {
221 _limitedFilesCache = new LimitedFilesCache<String>(
222 PropsValues.MINIFIER_FILES_LIMIT);
223
224 if (_log.isDebugEnabled()) {
225 _log.debug(
226 "Aggregate files limit " +
227 PropsValues.MINIFIER_FILES_LIMIT);
228 }
229 }
230 }
231
232 protected static String getJavaScriptContent(String content) {
233 return MinifierUtil.minifyJavaScript(content);
234 }
235
236 protected static String updateCssRelativeUrls(
237 String content, String baseURL) {
238
239 content = StringUtil.replace(
240 content, _CSS_PATH_TYPES, _CSS_PATH_PLACEHOLDERS);
241
242 content = StringUtil.replace(
243 content,
244 new String[] {
245 "[$RELATIVE_1$]", "[$RELATIVE_2$]", "[$RELATIVE_3$]"
246 },
247 new String[] {
248 "url('" + baseURL, "url(\"" + baseURL, "url(" + baseURL
249 });
250
251 content = StringUtil.replace(
252 content, _CSS_PATH_PLACEHOLDERS, _CSS_PATH_TYPES);
253
254 return content;
255 }
256
257 protected Object getBundleContent(
258 HttpServletRequest request, HttpServletResponse response)
259 throws IOException {
260
261 String minifierType = ParamUtil.getString(request, "minifierType");
262 String bundleId = ParamUtil.getString(
263 request, "bundleId",
264 ParamUtil.getString(request, "minifierBundleId"));
265
266 if (Validator.isNull(minifierType) ||
267 Validator.isNull(bundleId) ||
268 !ArrayUtil.contains(PropsValues.JAVASCRIPT_BUNDLE_IDS, bundleId)) {
269
270 return null;
271 }
272
273 String bundleDirName = PropsUtil.get(
274 PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(bundleId));
275
276 URL bundleDirURL = _servletContext.getResource(bundleDirName);
277
278 if (bundleDirURL == null) {
279 return null;
280 }
281
282 String cacheFileName = getCacheFileName(request);
283
284 String[] fileNames = JavaScriptBundleUtil.getFileNames(bundleId);
285
286 File cacheFile = new File(_tempDir, cacheFileName);
287
288 if (_limitedFilesCache != null) {
289 _limitedFilesCache.put(cacheFileName);
290 }
291
292 if (cacheFile.exists()) {
293 boolean staleCache = false;
294
295 for (String fileName : fileNames) {
296 URL resourceURL = _servletContext.getResource(
297 bundleDirName.concat(StringPool.SLASH).concat(fileName));
298
299 if (resourceURL == null) {
300 continue;
301 }
302
303 URLConnection urlConnection = resourceURL.openConnection();
304
305 if (urlConnection.getLastModified() >
306 cacheFile.lastModified()) {
307
308 staleCache = true;
309
310 break;
311 }
312 }
313
314 if (!staleCache) {
315 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
316
317 return cacheFile;
318 }
319 }
320
321 if (_log.isInfoEnabled()) {
322 _log.info("Aggregating JavaScript bundle " + bundleId);
323 }
324
325 String content = null;
326
327 if (fileNames.length == 0) {
328 content = StringPool.BLANK;
329 }
330 else {
331 AggregateContext aggregateContext = new ServletAggregateContext(
332 _servletContext, StringPool.SLASH);
333
334 aggregateContext.pushPath(bundleDirName);
335
336 content = aggregateJavaScript(aggregateContext, fileNames);
337 }
338
339 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
340
341 FileUtil.write(cacheFile, content);
342
343 return content;
344 }
345
346 protected String getCacheFileName(HttpServletRequest request) {
347 CacheKeyGenerator cacheKeyGenerator =
348 CacheKeyGeneratorUtil.getCacheKeyGenerator(
349 AggregateFilter.class.getName());
350
351 cacheKeyGenerator.append(request.getRequestURI());
352
353 String queryString = request.getQueryString();
354
355 if (queryString != null) {
356 cacheKeyGenerator.append(sterilizeQueryString(queryString));
357 }
358
359 return String.valueOf(cacheKeyGenerator.finish());
360 }
361
362 protected Object getContent(
363 HttpServletRequest request, HttpServletResponse response,
364 FilterChain filterChain)
365 throws Exception {
366
367 String minifierType = ParamUtil.getString(request, "minifierType");
368 String minifierBundleId = ParamUtil.getString(
369 request, "minifierBundleId");
370 String minifierBundleDirName = ParamUtil.getString(
371 request, "minifierBundleDir");
372
373 if (Validator.isNull(minifierType) ||
374 Validator.isNotNull(minifierBundleId) ||
375 Validator.isNotNull(minifierBundleDirName)) {
376
377 return null;
378 }
379
380 String requestURI = request.getRequestURI();
381
382 String resourcePath = requestURI;
383
384 String contextPath = request.getContextPath();
385
386 if (!contextPath.equals(StringPool.SLASH)) {
387 resourcePath = resourcePath.substring(contextPath.length());
388 }
389
390 URL resourceURL = _servletContext.getResource(resourcePath);
391
392 if (resourceURL == null) {
393 return null;
394 }
395
396 URLConnection urlConnection = resourceURL.openConnection();
397
398 String cacheCommonFileName = getCacheFileName(request);
399
400 File cacheContentTypeFile = new File(
401 _tempDir, cacheCommonFileName + "_E_CONTENT_TYPE");
402 File cacheDataFile = new File(
403 _tempDir, cacheCommonFileName + "_E_DATA");
404
405 if (cacheDataFile.exists() &&
406 (cacheDataFile.lastModified() >= urlConnection.getLastModified())) {
407
408 if (cacheContentTypeFile.exists()) {
409 String contentType = FileUtil.read(cacheContentTypeFile);
410
411 response.setContentType(contentType);
412 }
413
414 return cacheDataFile;
415 }
416
417 String content = null;
418
419 if (resourcePath.endsWith(_CSS_EXTENSION)) {
420 if (_log.isInfoEnabled()) {
421 _log.info("Minifying CSS " + resourcePath);
422 }
423
424 content = getCssContent(
425 request, response, resourceURL, resourcePath);
426
427 response.setContentType(ContentTypes.TEXT_CSS);
428
429 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
430 }
431 else if (resourcePath.endsWith(_JAVASCRIPT_EXTENSION)) {
432 if (_log.isInfoEnabled()) {
433 _log.info("Minifying JavaScript " + resourcePath);
434 }
435
436 content = getJavaScriptContent(resourceURL);
437
438 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
439
440 FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
441 }
442 else if (resourcePath.endsWith(_JSP_EXTENSION)) {
443 if (_log.isInfoEnabled()) {
444 _log.info("Minifying JSP " + resourcePath);
445 }
446
447 BufferCacheServletResponse bufferCacheServletResponse =
448 new BufferCacheServletResponse(response);
449
450 processFilter(
451 AggregateFilter.class, request, bufferCacheServletResponse,
452 filterChain);
453
454 bufferCacheServletResponse.finishResponse();
455
456 content = bufferCacheServletResponse.getString();
457
458 if (minifierType.equals("css")) {
459 content = getCssContent(
460 request, response, resourcePath, content);
461 }
462 else if (minifierType.equals("js")) {
463 content = getJavaScriptContent(content);
464 }
465
466 FileUtil.write(
467 cacheContentTypeFile,
468 bufferCacheServletResponse.getContentType());
469 }
470 else {
471 return null;
472 }
473
474 FileUtil.write(cacheDataFile, content);
475
476 return content;
477 }
478
479 protected String getCssContent(
480 HttpServletRequest request, HttpServletResponse response,
481 String resourcePath, String content) {
482
483 try {
484 content = DynamicCSSUtil.parseSass(
485 _servletContext, request, resourcePath, content);
486 }
487 catch (Exception e) {
488 _log.error("Unable to parse SASS on CSS " + resourcePath, e);
489
490 if (_log.isDebugEnabled()) {
491 _log.debug(content);
492 }
493
494 response.setHeader(
495 HttpHeaders.CACHE_CONTROL,
496 HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
497 }
498
499 String browserId = ParamUtil.getString(request, "browserId");
500
501 if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
502 Matcher matcher = _pattern.matcher(content);
503
504 content = matcher.replaceAll(StringPool.BLANK);
505 }
506
507 return MinifierUtil.minifyCss(content);
508 }
509
510 protected String getCssContent(
511 HttpServletRequest request, HttpServletResponse response,
512 URL resourceURL, String resourcePath)
513 throws IOException {
514
515 URLConnection urlConnection = resourceURL.openConnection();
516
517 String content = StringUtil.read(urlConnection.getInputStream());
518
519 content = aggregateCss(
520 new ServletAggregateContext(_servletContext, resourcePath),
521 content);
522
523 return getCssContent(request, response, resourcePath, content);
524 }
525
526 protected String getJavaScriptContent(URL resourceURL) throws IOException {
527 URLConnection urlConnection = resourceURL.openConnection();
528
529 String content = StringUtil.read(urlConnection.getInputStream());
530
531 return getJavaScriptContent(content);
532 }
533
534 @Override
535 protected void processFilter(
536 HttpServletRequest request, HttpServletResponse response,
537 FilterChain filterChain)
538 throws Exception {
539
540 Object minifiedContent = getContent(request, response, filterChain);
541
542 if (minifiedContent == null) {
543 minifiedContent = getBundleContent(request, response);
544 }
545
546 if (minifiedContent == null) {
547 processFilter(
548 AggregateFilter.class, request, response, filterChain);
549 }
550 else {
551 if (minifiedContent instanceof File) {
552 ServletResponseUtil.write(response, (File)minifiedContent);
553 }
554 else if (minifiedContent instanceof String) {
555 ServletResponseUtil.write(response, (String)minifiedContent);
556 }
557 }
558 }
559
560 protected String sterilizeQueryString(String queryString) {
561 return StringUtil.replace(
562 queryString, new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
563 new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
564 }
565
566 private static final String _CSS_COMMENT_BEGIN = "";
569
570 private static final String _CSS_EXTENSION = ".css";
571
572 private static final String _CSS_IMPORT_BEGIN = "@import url(";
573
574 private static final String _CSS_IMPORT_END = ");";
575
576 private static final String _CSS_MEDIA_QUERY = "@media";
577
578 private static final String[] _CSS_PATH_PLACEHOLDERS = new String[] {
579 "[$EMPTY_1$]", "[$EMPTY_2$]", "[$EMPTY_3$]", "[$TOKEN_1$]",
580 "[$TOKEN_2$]", "[$TOKEN_3$]", "[$ABSOLUTE_1$]", "[$ABSOLUTE_2$]",
581 "[$ABSOLUTE_3$]", "[$ABSOLUTE_4$]", "[$ABSOLUTE_5$]", "[$ABSOLUTE_6$]",
582 "[$ABSOLUTE_7$]", "[$ABSOLUTE_8$]", "[$ABSOLUTE_9$]", "[$RELATIVE_1$]",
583 "[$RELATIVE_2$]", "[$RELATIVE_3$]"
584 };
585
586 private static final String[] _CSS_PATH_TYPES = new String[] {
587 "url('')", "url(\"\")", "url()", "url('@theme_image_path@",
588 "url(\"@", "url(@", "url('http:
589 "url('https:
590 "url(/", "url('", "url(\"", "url("
591 };
592
593 private static final String _JAVASCRIPT_EXTENSION = ".js";
594
595 private static final String _JSP_EXTENSION = ".jsp";
596
597 private static final String _TEMP_DIR = "aggregate";
598
599 private static Log _log = LogFactoryUtil.getLog(AggregateFilter.class);
600
601 private static String _BASE_URL = "@base_url@";
602
603 private static Pattern _pattern = Pattern.compile(
604 "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
605
606 private LimitedFilesCache<String> _limitedFilesCache;
607 private ServletContext _servletContext;
608 private File _tempDir;
609
610 }