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