001    /**
002     * Copyright (c) 2000-2011 Liferay, Inc. All rights reserved.
003     *
004     * The contents of this file are subject to the terms of the Liferay Enterprise
005     * Subscription License ("License"). You may not use this file except in
006     * compliance with the License. You can obtain a copy of the License by
007     * contacting Liferay, Inc. See the License for the specific language governing
008     * permissions and limitations under the License, including but not limited to
009     * distribution rights of the Software.
010     *
011     *
012     *
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.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    /**
058     * @author Brian Wing Shun Chan
059     */
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                                    // LEP-7540
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 = "/*";
489    
490            private static final String _CSS_COMMENT_END = "*/";
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    }