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 importFile = content.substring(
092                                            x + _CSS_IMPORT_BEGIN.length(), y);
093    
094                                    String importContent = FileUtil.read(
095                                            dir + StringPool.SLASH + importFile);
096    
097                                    String importFilePath = StringPool.BLANK;
098    
099                                    if (importFile.lastIndexOf(StringPool.SLASH) != -1) {
100                                            importFilePath = StringPool.SLASH + importFile.substring(
101                                                    0, importFile.lastIndexOf(StringPool.SLASH) + 1);
102                                    }
103    
104                                    importContent = aggregateCss(
105                                            dir + importFilePath, importContent);
106    
107                                    int importDepth = StringUtil.count(
108                                            importFile, StringPool.SLASH);
109    
110                                    // LEP-7540
111    
112                                    String relativePath = StringPool.BLANK;
113    
114                                    for (int i = 0; i < importDepth; i++) {
115                                            relativePath += "../";
116                                    }
117    
118                                    importContent = StringUtil.replace(
119                                            importContent,
120                                            new String[] {
121                                                    "url('" + relativePath,
122                                                    "url(\"" + relativePath,
123                                                    "url(" + relativePath
124                                            },
125                                            new String[] {
126                                                    "url('[$TEMP_RELATIVE_PATH$]",
127                                                    "url(\"[$TEMP_RELATIVE_PATH$]",
128                                                    "url([$TEMP_RELATIVE_PATH$]"
129                                            });
130    
131                                    importContent = StringUtil.replace(
132                                            importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
133    
134                                    sb.append(importContent);
135    
136                                    pos = y + _CSS_IMPORT_END.length();
137                            }
138                    }
139    
140                    return sb.toString();
141            }
142    
143            protected String getMinifiedBundleContent(
144                            HttpServletRequest request, HttpServletResponse response)
145                    throws IOException {
146    
147                    String minifierType = ParamUtil.getString(request, "minifierType");
148                    String minifierBundleId = ParamUtil.getString(
149                            request, "minifierBundleId");
150    
151                    if (Validator.isNull(minifierType) ||
152                            Validator.isNull(minifierBundleId) ||
153                            !ArrayUtil.contains(
154                                    PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
155    
156                            return null;
157                    }
158    
159                    String minifierBundleDir = PropsUtil.get(
160                            PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
161    
162                    String bundleDirRealPath = ServletContextUtil.getRealPath(
163                            _servletContext, minifierBundleDir);
164    
165                    if (bundleDirRealPath == null) {
166                            return null;
167                    }
168    
169                    StringBundler sb = new StringBundler(4);
170    
171                    sb.append(_tempDir);
172                    sb.append(request.getRequestURI());
173    
174                    String queryString = request.getQueryString();
175    
176                    if (queryString != null) {
177                            sb.append(_QUESTION_SEPARATOR);
178                            sb.append(sterilizeQueryString(queryString));
179                    }
180    
181                    String cacheFileName = sb.toString();
182    
183                    String[] fileNames = JavaScriptBundleUtil.getFileNames(
184                            minifierBundleId);
185    
186                    File cacheFile = new File(cacheFileName);
187    
188                    if (cacheFile.exists()) {
189                            boolean staleCache = false;
190    
191                            for (String fileName : fileNames) {
192                                    File file = new File(
193                                            bundleDirRealPath + StringPool.SLASH + fileName);
194    
195                                    if (file.lastModified() > cacheFile.lastModified()) {
196                                            staleCache = true;
197    
198                                            break;
199                                    }
200                            }
201    
202                            if (!staleCache) {
203                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
204    
205                                    return FileUtil.read(cacheFile);
206                            }
207                    }
208    
209                    if (_log.isInfoEnabled()) {
210                            _log.info("Minifying JavaScript bundle " + minifierBundleId);
211                    }
212    
213                    String minifiedContent = null;
214    
215                    if (fileNames.length == 0) {
216                            minifiedContent = StringPool.BLANK;
217                    }
218                    else {
219                            sb = new StringBundler(fileNames.length * 2);
220    
221                            for (String fileName : fileNames) {
222                                    String content = FileUtil.read(
223                                            bundleDirRealPath + StringPool.SLASH + fileName);
224    
225                                    sb.append(content);
226                                    sb.append(StringPool.NEW_LINE);
227                            }
228    
229                            minifiedContent = minifyJavaScript(sb.toString());
230                    }
231    
232                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
233    
234                    FileUtil.write(cacheFile, minifiedContent);
235    
236                    return minifiedContent;
237            }
238    
239            protected String getMinifiedContent(
240                            HttpServletRequest request, HttpServletResponse response,
241                            FilterChain filterChain)
242                    throws Exception {
243    
244                    String minifierType = ParamUtil.getString(request, "minifierType");
245                    String minifierBundleId = ParamUtil.getString(
246                            request, "minifierBundleId");
247                    String minifierBundleDir = ParamUtil.getString(
248                            request, "minifierBundleDir");
249    
250                    if (Validator.isNull(minifierType) ||
251                            Validator.isNotNull(minifierBundleId) ||
252                            Validator.isNotNull(minifierBundleDir)) {
253    
254                            return null;
255                    }
256    
257                    String requestURI = request.getRequestURI();
258    
259                    String requestPath = requestURI;
260    
261                    String contextPath = request.getContextPath();
262    
263                    if (!contextPath.equals(StringPool.SLASH)) {
264                            requestPath = requestPath.substring(contextPath.length());
265                    }
266    
267                    String realPath = ServletContextUtil.getRealPath(
268                            _servletContext, requestPath);
269    
270                    if (realPath == null) {
271                            return null;
272                    }
273    
274                    realPath = StringUtil.replace(
275                            realPath, StringPool.BACK_SLASH, StringPool.SLASH);
276    
277                    File file = new File(realPath);
278    
279                    if (!file.exists()) {
280                            return null;
281                    }
282    
283                    String minifiedContent = null;
284    
285                    StringBundler sb = new StringBundler(4);
286    
287                    sb.append(_tempDir);
288                    sb.append(requestURI);
289    
290                    String queryString = request.getQueryString();
291    
292                    if (queryString != null) {
293                            sb.append(_QUESTION_SEPARATOR);
294                            sb.append(sterilizeQueryString(queryString));
295                    }
296    
297                    String cacheCommonFileName = sb.toString();
298    
299                    File cacheContentTypeFile = new File(
300                            cacheCommonFileName + "_E_CONTENT_TYPE");
301                    File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
302    
303                    if ((cacheDataFile.exists()) &&
304                            (cacheDataFile.lastModified() >= file.lastModified())) {
305    
306                            minifiedContent = FileUtil.read(cacheDataFile);
307    
308                            if (cacheContentTypeFile.exists()) {
309                                    String contentType = FileUtil.read(cacheContentTypeFile);
310    
311                                    response.setContentType(contentType);
312                            }
313                    }
314                    else {
315                            if (realPath.endsWith(_CSS_EXTENSION)) {
316                                    if (_log.isInfoEnabled()) {
317                                            _log.info("Minifying CSS " + file);
318                                    }
319    
320                                    minifiedContent = minifyCss(request, file);
321    
322                                    response.setContentType(ContentTypes.TEXT_CSS);
323    
324                                    FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
325                            }
326                            else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
327                                    if (_log.isInfoEnabled()) {
328                                            _log.info("Minifying JavaScript " + file);
329                                    }
330    
331                                    minifiedContent = minifyJavaScript(file);
332    
333                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
334    
335                                    FileUtil.write(
336                                            cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
337                            }
338                            else if (realPath.endsWith(_JSP_EXTENSION)) {
339                                    if (_log.isInfoEnabled()) {
340                                            _log.info("Minifying JSP " + file);
341                                    }
342    
343                                    StringServletResponse stringResponse =
344                                            new StringServletResponse(response);
345    
346                                    processFilter(
347                                            MinifierFilter.class, request, stringResponse, filterChain);
348    
349                                    CacheResponseUtil.setHeaders(
350                                            response, stringResponse.getHeaders());
351    
352                                    response.setContentType(stringResponse.getContentType());
353    
354                                    minifiedContent = stringResponse.getString();
355    
356                                    if (minifierType.equals("css")) {
357                                            minifiedContent = minifyCss(request, minifiedContent);
358                                    }
359                                    else if (minifierType.equals("js")) {
360                                            minifiedContent = minifyJavaScript(minifiedContent);
361                                    }
362    
363                                    FileUtil.write(
364                                            cacheContentTypeFile, stringResponse.getContentType());
365                            }
366                            else {
367                                    return null;
368                            }
369    
370                            FileUtil.write(cacheDataFile, minifiedContent);
371                    }
372    
373                    return minifiedContent;
374            }
375    
376            protected String minifyCss(HttpServletRequest request, File file)
377                    throws IOException {
378    
379                    String content = FileUtil.read(file);
380    
381                    content = aggregateCss(file.getParent(), content);
382    
383                    return minifyCss(request, content);
384            }
385    
386            protected String minifyCss(HttpServletRequest request, String content) {
387                    String browserId = ParamUtil.getString(request, "browserId");
388    
389                    if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
390                            Matcher matcher = _pattern.matcher(content);
391    
392                            content = matcher.replaceAll(StringPool.BLANK);
393                    }
394    
395                    return MinifierUtil.minifyCss(content);
396            }
397    
398            protected String minifyJavaScript(File file) throws IOException {
399                    String content = FileUtil.read(file);
400    
401                    return minifyJavaScript(content);
402            }
403    
404            protected String minifyJavaScript(String content) {
405                    return MinifierUtil.minifyJavaScript(content);
406            }
407    
408            protected void processFilter(
409                            HttpServletRequest request, HttpServletResponse response,
410                            FilterChain filterChain)
411                    throws Exception {
412    
413                    String minifiedContent = getMinifiedContent(
414                            request, response, filterChain);
415    
416                    if (Validator.isNull(minifiedContent)) {
417                            minifiedContent = getMinifiedBundleContent(request, response);
418                    }
419    
420                    if (Validator.isNull(minifiedContent)) {
421                            processFilter(MinifierFilter.class, request, response, filterChain);
422                    }
423                    else {
424                            ServletResponseUtil.write(response, minifiedContent);
425                    }
426            }
427    
428            protected String sterilizeQueryString(String queryString) {
429                    return StringUtil.replace(
430                            queryString,
431                            new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
432                            new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
433            }
434    
435            private static final String _CSS_IMPORT_BEGIN = "@import url(";
436    
437            private static final String _CSS_IMPORT_END = ");";
438    
439            private static final String _CSS_EXTENSION = ".css";
440    
441            private static final String _JAVASCRIPT_EXTENSION = ".js";
442    
443            private static final String _JSP_EXTENSION = ".jsp";
444    
445            private static final String _QUESTION_SEPARATOR = "_Q_";
446    
447            private static final String _TEMP_DIR =
448                    SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
449    
450            private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
451    
452            private static Pattern _pattern = Pattern.compile(
453                    "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
454    
455            private ServletContext _servletContext;
456            private String _servletContextName;
457            private String _tempDir = _TEMP_DIR;
458    
459    }