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