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