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.PortalUtil;
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 queryString = request.getQueryString();
325    
326                    if (queryString != null) {
327                            cacheKeyGenerator.append(sterilizeQueryString(queryString));
328                    }
329    
330                    return String.valueOf(cacheKeyGenerator.finish());
331            }
332    
333            protected Object getContent(
334                            HttpServletRequest request, HttpServletResponse response,
335                            FilterChain filterChain)
336                    throws Exception {
337    
338                    String minifierType = ParamUtil.getString(request, "minifierType");
339                    String minifierBundleId = ParamUtil.getString(
340                            request, "minifierBundleId");
341                    String minifierBundleDirName = ParamUtil.getString(
342                            request, "minifierBundleDir");
343    
344                    if (Validator.isNull(minifierType) ||
345                            Validator.isNotNull(minifierBundleId) ||
346                            Validator.isNotNull(minifierBundleDirName)) {
347    
348                            return null;
349                    }
350    
351                    String requestURI = request.getRequestURI();
352    
353                    String resourcePath = requestURI;
354    
355                    String contextPath = request.getContextPath();
356    
357                    if (!contextPath.equals(StringPool.SLASH)) {
358                            resourcePath = resourcePath.substring(contextPath.length());
359                    }
360    
361                    if (resourcePath.endsWith(_CSS_EXTENSION) &&
362                            PortalUtil.isRightToLeft(request)) {
363    
364                            int pos = resourcePath.lastIndexOf(StringPool.PERIOD);
365    
366                            resourcePath =
367                                    resourcePath.substring(0, pos) + "_rtl" +
368                                            resourcePath.substring(pos);
369                    }
370    
371                    URL resourceURL = _servletContext.getResource(resourcePath);
372    
373                    if (resourceURL == null) {
374                            resourceURL = PortalWebResourcesUtil.getResource(resourcePath);
375    
376                            if (resourceURL == null) {
377                                    return null;
378                            }
379                    }
380    
381                    String cacheCommonFileName = getCacheFileName(request);
382    
383                    File cacheContentTypeFile = new File(
384                            _tempDir, cacheCommonFileName + "_E_CONTENT_TYPE");
385                    File cacheDataFile = new File(
386                            _tempDir, cacheCommonFileName + "_E_DATA");
387    
388                    if (cacheDataFile.exists() &&
389                            (cacheDataFile.lastModified() >=
390                                    URLUtil.getLastModifiedTime(resourceURL))) {
391    
392                            if (cacheContentTypeFile.exists()) {
393                                    String contentType = FileUtil.read(cacheContentTypeFile);
394    
395                                    response.setContentType(contentType);
396                            }
397    
398                            return cacheDataFile;
399                    }
400    
401                    String content = null;
402    
403                    if (resourcePath.endsWith(_CSS_EXTENSION)) {
404                            if (_log.isInfoEnabled()) {
405                                    _log.info("Minifying CSS " + resourcePath);
406                            }
407    
408                            content = getCssContent(
409                                    request, response, resourceURL, resourcePath);
410    
411                            response.setContentType(ContentTypes.TEXT_CSS);
412    
413                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
414                    }
415                    else if (resourcePath.endsWith(_JAVASCRIPT_EXTENSION)) {
416                            if (_log.isInfoEnabled()) {
417                                    _log.info("Minifying JavaScript " + resourcePath);
418                            }
419    
420                            content = getJavaScriptContent(resourceURL);
421    
422                            response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
423    
424                            FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
425                    }
426                    else if (resourcePath.endsWith(_JSP_EXTENSION)) {
427                            if (_log.isInfoEnabled()) {
428                                    _log.info("Minifying JSP " + resourcePath);
429                            }
430    
431                            BufferCacheServletResponse bufferCacheServletResponse =
432                                    new BufferCacheServletResponse(response);
433    
434                            processFilter(
435                                    AggregateFilter.class.getName(), request,
436                                    bufferCacheServletResponse, filterChain);
437    
438                            bufferCacheServletResponse.finishResponse(false);
439    
440                            content = bufferCacheServletResponse.getString();
441    
442                            if (minifierType.equals("css")) {
443                                    content = getCssContent(
444                                            request, response, resourcePath, content);
445                            }
446                            else if (minifierType.equals("js")) {
447                                    content = getJavaScriptContent(resourcePath, content);
448                            }
449    
450                            FileUtil.write(
451                                    cacheContentTypeFile,
452                                    bufferCacheServletResponse.getContentType());
453                    }
454                    else {
455                            return null;
456                    }
457    
458                    FileUtil.write(cacheDataFile, content);
459    
460                    return content;
461            }
462    
463            protected String getCssContent(
464                    HttpServletRequest request, HttpServletResponse response,
465                    String resourcePath, String content) {
466    
467                    try {
468                            ServletContext cssServletContext = null;
469    
470                            String requestURI = request.getRequestURI();
471    
472                            if (PortalWebResourcesUtil.hasContextPath(requestURI)) {
473                                    cssServletContext =
474                                            PortalWebResourcesUtil.getPathServletContext(requestURI);
475                            }
476    
477                            if (cssServletContext == null) {
478                                    cssServletContext = _servletContext;
479                            }
480    
481                            content = DynamicCSSUtil.replaceToken(
482                                    cssServletContext, request, content);
483                    }
484                    catch (Exception e) {
485                            _log.error("Unable to replace tokens in CSS " + resourcePath, e);
486    
487                            if (_log.isDebugEnabled()) {
488                                    _log.debug(content);
489                            }
490    
491                            response.setHeader(
492                                    HttpHeaders.CACHE_CONTROL,
493                                    HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE);
494                    }
495    
496                    String browserId = ParamUtil.getString(request, "browserId");
497    
498                    if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
499                            Matcher matcher = _pattern.matcher(content);
500    
501                            content = matcher.replaceAll(StringPool.BLANK);
502                    }
503    
504                    return MinifierUtil.minifyCss(content);
505            }
506    
507            protected String getCssContent(
508                            HttpServletRequest request, HttpServletResponse response,
509                            URL resourceURL, String resourcePath)
510                    throws IOException {
511    
512                    URLConnection urlConnection = resourceURL.openConnection();
513    
514                    String content = StringUtil.read(urlConnection.getInputStream());
515    
516                    content = aggregateCss(
517                            new ServletPaths(
518                                    _servletContext, ServletPaths.getParentPath(resourcePath)),
519                            content);
520    
521                    return getCssContent(request, response, resourcePath, content);
522            }
523    
524            protected String getJavaScriptContent(URL resourceURL) throws IOException {
525                    URLConnection urlConnection = resourceURL.openConnection();
526    
527                    String content = StringUtil.read(urlConnection.getInputStream());
528    
529                    return getJavaScriptContent(resourceURL.toString(), content);
530            }
531    
532            @Override
533            protected boolean isModuleRequest(HttpServletRequest request) {
534                    String requestURI = request.getRequestURI();
535    
536                    if (PortalWebResourcesUtil.hasContextPath(requestURI)) {
537                            return false;
538                    }
539    
540                    return super.isModuleRequest(request);
541            }
542    
543            @Override
544            protected void processFilter(
545                            HttpServletRequest request, HttpServletResponse response,
546                            FilterChain filterChain)
547                    throws Exception {
548    
549                    Object minifiedContent = getContent(request, response, filterChain);
550    
551                    if (minifiedContent == null) {
552                            minifiedContent = getBundleContent(request, response);
553                    }
554    
555                    if (minifiedContent == null) {
556                            processFilter(
557                                    AggregateFilter.class.getName(), request, response,
558                                    filterChain);
559                    }
560                    else {
561                            if (minifiedContent instanceof File) {
562                                    ServletResponseUtil.write(response, (File)minifiedContent);
563                            }
564                            else if (minifiedContent instanceof String) {
565                                    ServletResponseUtil.write(response, (String)minifiedContent);
566                            }
567                    }
568            }
569    
570            protected String sterilizeQueryString(String queryString) {
571                    return StringUtil.replace(
572                            queryString, new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
573                            new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
574            }
575    
576            private static final String _BASE_URL = "@base_url@";
577    
578            private static final String _CSS_COMMENT_BEGIN = "/*";
579    
580            private static final String _CSS_COMMENT_END = "*/";
581    
582            private static final String _CSS_EXTENSION = ".css";
583    
584            private static final String _CSS_IMPORT_BEGIN = "@import url(";
585    
586            private static final String _CSS_IMPORT_END = ");";
587    
588            private static final String _CSS_MEDIA_QUERY = "@media";
589    
590            private static final String _JAVASCRIPT_EXTENSION = ".js";
591    
592            private static final String _JSP_EXTENSION = ".jsp";
593    
594            private static final String _TEMP_DIR = "aggregate";
595    
596            private static final Log _log = LogFactoryUtil.getLog(
597                    AggregateFilter.class);
598    
599            private static final Pattern _pattern = Pattern.compile(
600                    "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
601    
602            private ServletContext _servletContext;
603            private File _tempDir;
604    
605    }