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