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