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