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.IgnoreModuleRequestFilter;
038 import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil;
039 import com.liferay.portal.util.AggregateUtil;
040 import com.liferay.portal.util.JavaScriptBundleUtil;
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
065 public class AggregateFilter extends IgnoreModuleRequestFilter {
066
067
070 public static String aggregateCss(
071 AggregateContext aggregateContext, 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 = aggregateContext.getContent(
128 importFileName);
129
130 if (importContent == null) {
131 if (_log.isWarnEnabled()) {
132 _log.warn(
133 "File " +
134 aggregateContext.getFullPath(importFileName) +
135 " does not exist");
136 }
137
138 importContent = StringPool.BLANK;
139 }
140
141 String importDirName = StringPool.BLANK;
142
143 int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
144
145 if (slashPos != -1) {
146 importDirName = importFileName.substring(0, slashPos + 1);
147 }
148
149 aggregateContext.pushPath(importDirName);
150
151 importContent = aggregateCss(aggregateContext, importContent);
152
153 if (Validator.isNotNull(importDirName)) {
154 aggregateContext.popPath();
155 }
156
157
158
159 String baseURL = _BASE_URL;
160
161 baseURL = baseURL.concat(
162 aggregateContext.getResourcePath(StringPool.BLANK));
163 baseURL = baseURL.concat(importDirName);
164
165 importContent = AggregateUtil.updateRelativeURLs(
166 importContent, baseURL);
167
168 if (Validator.isNotNull(mediaQuery)) {
169 sb.append(_CSS_MEDIA_QUERY);
170 sb.append(CharPool.SPACE);
171 sb.append(mediaQuery);
172 sb.append(CharPool.OPEN_CURLY_BRACE);
173 sb.append(importContent);
174 sb.append(CharPool.CLOSE_CURLY_BRACE);
175
176 pos = mediaQueryImportY + 1;
177 }
178 else {
179 sb.append(importContent);
180
181 pos = importY + _CSS_IMPORT_END.length();
182 }
183 }
184 }
185
186 return sb.toString();
187 }
188
189 public static String aggregateJavaScript(
190 AggregateContext aggregateContext, String[] fileNames) {
191
192 StringBundler sb = new StringBundler(fileNames.length * 2);
193
194 for (String fileName : fileNames) {
195 String content = aggregateContext.getContent(fileName);
196
197 if (Validator.isNull(content)) {
198 continue;
199 }
200
201 sb.append(content);
202 sb.append(StringPool.NEW_LINE);
203 }
204
205 return getJavaScriptContent(sb.toString());
206 }
207
208 @Override
209 public void init(FilterConfig filterConfig) {
210 super.init(filterConfig);
211
212 _servletContext = filterConfig.getServletContext();
213
214 File tempDir = (File)_servletContext.getAttribute(
215 JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
216
217 _tempDir = new File(tempDir, _TEMP_DIR);
218
219 _tempDir.mkdirs();
220 }
221
222 protected static String getJavaScriptContent(String content) {
223 return MinifierUtil.minifyJavaScript(content);
224 }
225
226 protected Object getBundleContent(
227 HttpServletRequest request, HttpServletResponse response)
228 throws IOException {
229
230 String minifierType = ParamUtil.getString(request, "minifierType");
231 String bundleId = ParamUtil.getString(
232 request, "bundleId",
233 ParamUtil.getString(request, "minifierBundleId"));
234
235 if (Validator.isNull(minifierType) ||
236 Validator.isNull(bundleId) ||
237 !ArrayUtil.contains(PropsValues.JAVASCRIPT_BUNDLE_IDS, bundleId)) {
238
239 return null;
240 }
241
242 String bundleDirName = PropsUtil.get(
243 PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(bundleId));
244
245 URL bundleDirURL = _servletContext.getResource(bundleDirName);
246
247 if (bundleDirURL == null) {
248 return null;
249 }
250
251 String cacheFileName = bundleId;
252
253 String[] fileNames = JavaScriptBundleUtil.getFileNames(bundleId);
254
255 File cacheFile = new File(_tempDir, cacheFileName);
256
257 if (cacheFile.exists()) {
258 boolean staleCache = false;
259
260 for (String fileName : fileNames) {
261 URL resourceURL = _servletContext.getResource(
262 bundleDirName.concat(StringPool.SLASH).concat(fileName));
263
264 if (resourceURL == null) {
265 continue;
266 }
267
268 URLConnection urlConnection = resourceURL.openConnection();
269
270 if (urlConnection.getLastModified() >
271 cacheFile.lastModified()) {
272
273 staleCache = true;
274
275 break;
276 }
277 }
278
279 if (!staleCache) {
280 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
281
282 return cacheFile;
283 }
284 }
285
286 if (_log.isInfoEnabled()) {
287 _log.info("Aggregating JavaScript bundle " + bundleId);
288 }
289
290 String content = null;
291
292 if (fileNames.length == 0) {
293 content = StringPool.BLANK;
294 }
295 else {
296 AggregateContext aggregateContext = new ServletAggregateContext(
297 _servletContext, StringPool.SLASH);
298
299 aggregateContext.pushPath(bundleDirName);
300
301 content = aggregateJavaScript(aggregateContext, fileNames);
302 }
303
304 response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
305
306 FileUtil.write(cacheFile, content);
307
308 return content;
309 }
310
311 protected String getCacheFileName(HttpServletRequest request) {
312 CacheKeyGenerator cacheKeyGenerator =
313 CacheKeyGeneratorUtil.getCacheKeyGenerator(
314 AggregateFilter.class.getName());
315
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();
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 }