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