001    /**
002     * Copyright (c) 2000-2012 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.aggregate;
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.BufferCacheServletResponse;
024    import com.liferay.portal.kernel.servlet.HttpHeaders;
025    import com.liferay.portal.kernel.servlet.ServletResponseUtil;
026    import com.liferay.portal.kernel.util.ArrayUtil;
027    import com.liferay.portal.kernel.util.CharPool;
028    import com.liferay.portal.kernel.util.ContentTypes;
029    import com.liferay.portal.kernel.util.FileUtil;
030    import com.liferay.portal.kernel.util.JavaConstants;
031    import com.liferay.portal.kernel.util.ParamUtil;
032    import com.liferay.portal.kernel.util.PropsKeys;
033    import com.liferay.portal.kernel.util.StringBundler;
034    import com.liferay.portal.kernel.util.StringPool;
035    import com.liferay.portal.kernel.util.StringUtil;
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.LimitedFilesCache;
041    import com.liferay.portal.util.MinifierUtil;
042    import com.liferay.portal.util.PropsUtil;
043    import com.liferay.portal.util.PropsValues;
044    
045    import java.io.File;
046    import java.io.IOException;
047    
048    import java.net.URL;
049    import java.net.URLConnection;
050    
051    import java.util.regex.Matcher;
052    import java.util.regex.Pattern;
053    
054    import javax.servlet.FilterChain;
055    import javax.servlet.FilterConfig;
056    import javax.servlet.ServletContext;
057    import javax.servlet.http.HttpServletRequest;
058    import javax.servlet.http.HttpServletResponse;
059    
060    /**
061     * @author Brian Wing Shun Chan
062     * @author Raymond Augé
063     */
064    public class AggregateFilter extends BasePortalFilter {
065    
066            /**
067             * @see DynamicCSSUtil#_propagateQueryString(String, String)
068             */
069            public static String aggregateCss(
070                            AggregateContext aggregateContext, String content)
071                    throws IOException {
072    
073                    StringBundler sb = new StringBundler();
074    
075                    int pos = 0;
076    
077                    while (true) {
078                            int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos);
079                            int commentY = content.indexOf(
080                                    _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length());
081    
082                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
083                            int importY = content.indexOf(
084                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
085    
086                            if ((importX == -1) || (importY == -1)) {
087                                    sb.append(content.substring(pos));
088    
089                                    break;
090                            }
091                            else if ((commentX != -1) && (commentY != -1) &&
092                                             (commentX < importX) && (commentY > importX)) {
093    
094                                    commentY += _CSS_COMMENT_END.length();
095    
096                                    sb.append(content.substring(pos, commentY));
097    
098                                    pos = commentY;
099                            }
100                            else {
101                                    sb.append(content.substring(pos, importX));
102    
103                                    String mediaQuery = StringPool.BLANK;
104    
105                                    int mediaQueryImportX = content.indexOf(
106                                            CharPool.CLOSE_PARENTHESIS,
107                                            importX + _CSS_IMPORT_BEGIN.length());
108                                    int mediaQueryImportY = content.indexOf(
109                                            CharPool.SEMICOLON, importX + _CSS_IMPORT_BEGIN.length());
110    
111                                    String importFileName = null;
112    
113                                    if (importY != mediaQueryImportX) {
114                                            mediaQuery = content.substring(
115                                                    mediaQueryImportX + 1, mediaQueryImportY);
116    
117                                            importFileName = content.substring(
118                                                    importX + _CSS_IMPORT_BEGIN.length(),
119                                                    mediaQueryImportX);
120                                    }
121                                    else {
122                                            importFileName = content.substring(
123                                                    importX + _CSS_IMPORT_BEGIN.length(), importY);
124                                    }
125    
126                                    String importContent = aggregateContext.getContent(
127                                            importFileName);
128    
129                                    if (importContent == null) {
130                                            if (_log.isWarnEnabled()) {
131                                                    _log.warn(
132                                                            "File " +
133                                                                    aggregateContext.getFullPath(importFileName) +
134                                                                            " does not exist");
135                                            }
136    
137                                            importContent = StringPool.BLANK;
138                                    }
139    
140                                    String importDir = StringPool.BLANK;
141    
142                                    int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
143    
144                                    if (slashPos != -1) {
145                                            importDir = StringPool.SLASH.concat(
146                                                    importFileName.substring(0, slashPos + 1));
147                                    }
148    
149                                    aggregateContext.pushPath(importDir);
150    
151                                    importContent = aggregateCss(aggregateContext, importContent);
152    
153                                    aggregateContext.popPath(importDir);
154    
155                                    int importDepth = StringUtil.count(
156                                            importFileName, StringPool.SLASH);
157    
158                                    // LEP-7540
159    
160                                    String relativePath = StringPool.BLANK;
161    
162                                    for (int i = 0; i < importDepth; i++) {
163                                            relativePath += "../";
164                                    }
165    
166                                    importContent = StringUtil.replace(
167                                            importContent,
168                                            new String[] {
169                                                    "url('" + relativePath, "url(\"" + relativePath,
170                                                    "url(" + relativePath
171                                            },
172                                            new String[] {
173                                                    "url('[$TEMP_RELATIVE_PATH$]",
174                                                    "url(\"[$TEMP_RELATIVE_PATH$]",
175                                                    "url([$TEMP_RELATIVE_PATH$]"
176                                            });
177    
178                                    importContent = StringUtil.replace(
179                                            importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
180    
181                                    if (Validator.isNotNull(mediaQuery)) {
182                                            sb.append(_CSS_MEDIA_QUERY);
183                                            sb.append(CharPool.SPACE);
184                                            sb.append(mediaQuery);
185                                            sb.append(CharPool.OPEN_CURLY_BRACE);
186                                            sb.append(importContent);
187                                            sb.append(CharPool.CLOSE_CURLY_BRACE);
188    
189                                            pos = mediaQueryImportY + 1;
190                                    }
191                                    else {
192                                            sb.append(importContent);
193    
194                                            pos = importY + _CSS_IMPORT_END.length();
195                                    }
196                            }
197                    }
198    
199                    return sb.toString();
200            }
201    
202            public static String aggregateJavaScript(
203                    AggregateContext aggregateContext, String[] fileNames) {
204    
205                    StringBundler sb = new StringBundler(fileNames.length * 2);
206    
207                    for (String fileName : fileNames) {
208                            String content = aggregateContext.getContent(fileName);
209    
210                            if (Validator.isNull(content)) {
211                                    continue;
212                            }
213    
214                            sb.append(content);
215                            sb.append(StringPool.NEW_LINE);
216                    }
217    
218                    return getJavaScriptContent(sb.toString());
219            }
220    
221            @Override
222            public void init(FilterConfig filterConfig) {
223                    super.init(filterConfig);
224    
225                    _servletContext = filterConfig.getServletContext();
226    
227                    File tempDir = (File)_servletContext.getAttribute(
228                            JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
229    
230                    _tempDir = new File(tempDir, _TEMP_DIR);
231    
232                    _tempDir.mkdirs();
233    
234                    if (PropsValues.MINIFIER_FILES_LIMIT > 0) {
235                            _limitedFilesCache = new LimitedFilesCache<String>(
236                                    PropsValues.MINIFIER_FILES_LIMIT);
237    
238                            if (_log.isDebugEnabled()) {
239                                    _log.debug(
240                                            "Aggregate files limit " +
241                                                    PropsValues.MINIFIER_FILES_LIMIT);
242                            }
243                    }
244            }
245    
246            protected static String getJavaScriptContent(String content) {
247                    return MinifierUtil.minifyJavaScript(content);
248            }
249    
250            protected Object getBundleContent(
251                            HttpServletRequest request, HttpServletResponse response)
252                    throws IOException {
253    
254                    String minifierType = ParamUtil.getString(request, "minifierType");
255                    String bundleId = ParamUtil.getString(
256                            request, "bundleId",
257                            ParamUtil.getString(request, "minifierBundleId"));
258    
259                    if (Validator.isNull(minifierType) ||
260                            Validator.isNull(bundleId) ||
261                            !ArrayUtil.contains(PropsValues.JAVASCRIPT_BUNDLE_IDS, bundleId)) {
262    
263                            return null;
264                    }
265    
266                    String bundleDir = PropsUtil.get(
267                            PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(bundleId));
268    
269                    URL bundleDirURL = _servletContext.getResource(bundleDir);
270    
271                    if (bundleDirURL == null) {
272                            return null;
273                    }
274    
275                    String cacheFileName = getCacheFileName(request);
276    
277                    String[] fileNames = JavaScriptBundleUtil.getFileNames(bundleId);
278    
279                    File cacheFile = new File(_tempDir, cacheFileName);
280    
281                    if (_limitedFilesCache != null) {
282                            _limitedFilesCache.put(cacheFileName);
283                    }
284    
285                    if (cacheFile.exists()) {
286                            boolean staleCache = false;
287    
288                            for (String fileName : fileNames) {
289                                    URL resourceURL = _servletContext.getResource(
290                                            bundleDir.concat(StringPool.SLASH).concat(fileName));
291    
292                                    if (resourceURL == null) {
293                                            continue;
294                                    }
295    
296                                    URLConnection urlConnection = resourceURL.openConnection();
297    
298                                    if (urlConnection.getLastModified() >
299                                                    cacheFile.lastModified()) {
300    
301                                            staleCache = true;
302    
303                                            break;
304                                    }
305                            }
306    
307                            if (!staleCache) {
308                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
309    
310                                    return cacheFile;
311                            }
312                    }
313    
314                    if (_log.isInfoEnabled()) {
315                            _log.info("Aggregating JavaScript bundle " + bundleId);
316                    }
317    
318                    String content = null;
319    
320                    if (fileNames.length == 0) {
321                            content = StringPool.BLANK;
322                    }
323                    else {
324                            AggregateContext aggregateContext = new ServletAggregateContext(
325                                    _servletContext, StringPool.SLASH);
326    
327                            aggregateContext.pushPath(bundleDir);
328    
329                            content = aggregateJavaScript(aggregateContext, fileNames);
330                    }
331    
332                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
333    
334                    FileUtil.write(cacheFile, content);
335    
336                    return content;
337            }
338    
339            protected String getCacheFileName(HttpServletRequest request) {
340                    CacheKeyGenerator cacheKeyGenerator =
341                            CacheKeyGeneratorUtil.getCacheKeyGenerator(
342                                    AggregateFilter.class.getName());
343    
344                    cacheKeyGenerator.append(request.getRequestURI());
345    
346                    String queryString = request.getQueryString();
347    
348                    if (queryString != null) {
349                            cacheKeyGenerator.append(sterilizeQueryString(queryString));
350                    }
351    
352                    return String.valueOf(cacheKeyGenerator.finish());
353            }
354    
355            protected Object getContent(
356                            HttpServletRequest request, HttpServletResponse response,
357                            FilterChain filterChain)
358                    throws Exception {
359    
360                    String minifierType = ParamUtil.getString(request, "minifierType");
361                    String minifierBundleId = ParamUtil.getString(
362                            request, "minifierBundleId");
363                    String minifierBundleDir = ParamUtil.getString(
364                            request, "minifierBundleDir");
365    
366                    if (Validator.isNull(minifierType) ||
367                            Validator.isNotNull(minifierBundleId) ||
368                            Validator.isNotNull(minifierBundleDir)) {
369    
370                            return null;
371                    }
372    
373                    String requestURI = request.getRequestURI();
374    
375                    String resourcePath = requestURI;
376    
377                    String contextPath = request.getContextPath();
378    
379                    if (!contextPath.equals(StringPool.SLASH)) {
380                            resourcePath = resourcePath.substring(contextPath.length());
381                    }
382    
383                    URL resourceURL = _servletContext.getResource(resourcePath);
384    
385                    if (resourceURL == null) {
386                            return null;
387                    }
388    
389                    URLConnection urlConnection = resourceURL.openConnection();
390    
391                    String cacheCommonFileName = getCacheFileName(request);
392    
393                    File cacheContentTypeFile = new File(
394                            _tempDir, cacheCommonFileName + "_E_CONTENT_TYPE");
395                    File cacheDataFile = new File(
396                            _tempDir, cacheCommonFileName + "_E_DATA");
397    
398                    if (cacheDataFile.exists() &&
399                            (cacheDataFile.lastModified() >= urlConnection.getLastModified())) {
400    
401                            if (cacheContentTypeFile.exists()) {
402                                    String contentType = FileUtil.read(cacheContentTypeFile);
403    
404                                    response.setContentType(contentType);
405                            }
406    
407                            return cacheDataFile;
408                    }
409    
410                    String content = null;
411    
412                    if (resourcePath.endsWith(_CSS_EXTENSION)) {
413                            if (_log.isInfoEnabled()) {
414                                    _log.info("Minifying CSS " + resourcePath);
415                            }
416    
417                            content = getCssContent(
418                                    request, response, resourceURL, resourcePath);
419    
420                            response.setContentType(ContentTypes.TEXT_CSS);
421    
422                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
423                    }
424                    else if (resourcePath.endsWith(_JAVASCRIPT_EXTENSION)) {
425                            if (_log.isInfoEnabled()) {
426                                    _log.info("Minifying JavaScript " + resourcePath);
427                            }
428    
429                            content = getJavaScriptContent(resourceURL);
430    
431                            response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
432    
433                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
434                    }
435                    else if (resourcePath.endsWith(_JSP_EXTENSION)) {
436                            if (_log.isInfoEnabled()) {
437                                    _log.info("Minifying JSP " + resourcePath);
438                            }
439    
440                            BufferCacheServletResponse bufferCacheServletResponse =
441                                    new BufferCacheServletResponse(response);
442    
443                            processFilter(
444                                    AggregateFilter.class, request, bufferCacheServletResponse,
445                                    filterChain);
446    
447                            bufferCacheServletResponse.finishResponse();
448    
449                            content = bufferCacheServletResponse.getString();
450    
451                            if (minifierType.equals("css")) {
452                                    content = getCssContent(
453                                            request, response, resourcePath, content);
454                            }
455                            else if (minifierType.equals("js")) {
456                                    content = getJavaScriptContent(content);
457                            }
458    
459                            FileUtil.write(
460                                    cacheContentTypeFile,
461                                    bufferCacheServletResponse.getContentType());
462                    }
463                    else {
464                            return null;
465                    }
466    
467                    FileUtil.write(cacheDataFile, content);
468    
469                    return content;
470            }
471    
472            protected String getCssContent(
473                    HttpServletRequest request, HttpServletResponse response,
474                    String resourcePath, String content) {
475    
476                    try {
477                            content = DynamicCSSUtil.parseSass(
478                                    _servletContext, request, resourcePath, content);
479                    }
480                    catch (Exception e) {
481                            _log.error("Unable to parse SASS on CSS " + resourcePath, e);
482    
483                            if (_log.isDebugEnabled()) {
484                                    _log.debug(content);
485                            }
486    
487                            response.setHeader(
488                                    HttpHeaders.CACHE_CONTROL,
489                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
490                    }
491    
492                    String browserId = ParamUtil.getString(request, "browserId");
493    
494                    if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
495                            Matcher matcher = _pattern.matcher(content);
496    
497                            content = matcher.replaceAll(StringPool.BLANK);
498                    }
499    
500                    return MinifierUtil.minifyCss(content);
501            }
502    
503            protected String getCssContent(
504                            HttpServletRequest request, HttpServletResponse response,
505                            URL resourceURL, String resourcePath)
506                    throws IOException {
507    
508                    URLConnection urlConnection = resourceURL.openConnection();
509    
510                    String content = StringUtil.read(urlConnection.getInputStream());
511    
512                    content = aggregateCss(
513                            new ServletAggregateContext(_servletContext, resourcePath),
514                            content);
515    
516                    return getCssContent(request, response, resourcePath, content);
517            }
518    
519            protected String getJavaScriptContent(URL resourceURL) throws IOException {
520                    URLConnection urlConnection = resourceURL.openConnection();
521    
522                    String content = StringUtil.read(urlConnection.getInputStream());
523    
524                    return getJavaScriptContent(content);
525            }
526    
527            @Override
528            protected void processFilter(
529                            HttpServletRequest request, HttpServletResponse response,
530                            FilterChain filterChain)
531                    throws Exception {
532    
533                    Object minifiedContent = getContent(request, response, filterChain);
534    
535                    if (minifiedContent == null) {
536                            minifiedContent = getBundleContent(request, response);
537                    }
538    
539                    if (minifiedContent == null) {
540                            processFilter(
541                                    AggregateFilter.class, request, response, filterChain);
542                    }
543                    else {
544                            if (minifiedContent instanceof File) {
545                                    ServletResponseUtil.write(response, (File)minifiedContent);
546                            }
547                            else if (minifiedContent instanceof String) {
548                                    ServletResponseUtil.write(response, (String)minifiedContent);
549                            }
550                    }
551            }
552    
553            protected String sterilizeQueryString(String queryString) {
554                    return StringUtil.replace(
555                            queryString, new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
556                            new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
557            }
558    
559            private static final String _CSS_COMMENT_BEGIN = "/*";
560    
561            private static final String _CSS_COMMENT_END = "*/";
562    
563            private static final String _CSS_EXTENSION = ".css";
564    
565            private static final String _CSS_IMPORT_BEGIN = "@import url(";
566    
567            private static final String _CSS_IMPORT_END = ");";
568    
569            private static final String _CSS_MEDIA_QUERY = "@media";
570    
571            private static final String _JAVASCRIPT_EXTENSION = ".js";
572    
573            private static final String _JSP_EXTENSION = ".jsp";
574    
575            private static final String _TEMP_DIR = "aggregate";
576    
577            private static Log _log = LogFactoryUtil.getLog(AggregateFilter.class);
578    
579            private static Pattern _pattern = Pattern.compile(
580                    "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
581    
582            private LimitedFilesCache<String> _limitedFilesCache;
583            private ServletContext _servletContext;
584            private File _tempDir;
585    
586    }