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