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