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