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