/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.servlet.filters.aggregate; import com.liferay.portal.kernel.configuration.Filter; import com.liferay.portal.kernel.log.Log; import com.liferay.portal.kernel.log.LogFactoryUtil; import com.liferay.portal.kernel.servlet.BrowserSniffer; import com.liferay.portal.kernel.servlet.BufferCacheServletResponse; import com.liferay.portal.kernel.servlet.HttpHeaders; import com.liferay.portal.kernel.servlet.PortalWebResourceConstants; import com.liferay.portal.kernel.servlet.PortalWebResourcesUtil; import com.liferay.portal.kernel.servlet.ServletResponseUtil; import com.liferay.portal.kernel.util.ArrayUtil; import com.liferay.portal.kernel.util.CharPool; import com.liferay.portal.kernel.util.ContentTypes; import com.liferay.portal.kernel.util.FileUtil; import com.liferay.portal.kernel.util.JavaConstants; import com.liferay.portal.kernel.util.ParamUtil; import com.liferay.portal.kernel.util.PortalUtil; import com.liferay.portal.kernel.util.PropsKeys; import com.liferay.portal.kernel.util.StringBundler; import com.liferay.portal.kernel.util.StringPool; import com.liferay.portal.kernel.util.StringUtil; import com.liferay.portal.kernel.util.URLUtil; import com.liferay.portal.kernel.util.Validator; import com.liferay.portal.minifier.MinifierUtil; import com.liferay.portal.servlet.filters.IgnoreModuleRequestFilter; import com.liferay.portal.servlet.filters.dynamiccss.DynamicCSSUtil; import com.liferay.portal.servlet.filters.util.CacheFileNameGenerator; import com.liferay.portal.util.AggregateUtil; import com.liferay.portal.util.JavaScriptBundleUtil; import com.liferay.portal.util.PropsUtil; import com.liferay.portal.util.PropsValues; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLConnection; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author Brian Wing Shun Chan * @author Raymond Augé * @author Eduardo Lundgren */ public class AggregateFilter extends IgnoreModuleRequestFilter { /** * @see DynamicCSSUtil#propagateQueryString(String, String) */ public static String aggregateCss(ServletPaths servletPaths, String content) throws IOException { StringBundler sb = new StringBundler(); int pos = 0; while (true) { int commentX = content.indexOf(_CSS_COMMENT_BEGIN, pos); int commentY = content.indexOf( _CSS_COMMENT_END, commentX + _CSS_COMMENT_BEGIN.length()); int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos); int importY = content.indexOf( _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length()); if ((importX == -1) || (importY == -1)) { sb.append(content.substring(pos)); break; } else if ((commentX != -1) && (commentY != -1) && (commentX < importX) && (commentY > importX)) { commentY += _CSS_COMMENT_END.length(); sb.append(content.substring(pos, commentY)); pos = commentY; } else { sb.append(content.substring(pos, importX)); String mediaQuery = StringPool.BLANK; int mediaQueryImportX = content.indexOf( CharPool.CLOSE_PARENTHESIS, importX + _CSS_IMPORT_BEGIN.length()); int mediaQueryImportY = content.indexOf( CharPool.SEMICOLON, importX + _CSS_IMPORT_BEGIN.length()); String importFileName = null; if (importY != mediaQueryImportX) { mediaQuery = content.substring( mediaQueryImportX + 1, mediaQueryImportY); importFileName = content.substring( importX + _CSS_IMPORT_BEGIN.length(), mediaQueryImportX); } else { importFileName = content.substring( importX + _CSS_IMPORT_BEGIN.length(), importY); } String importContent = null; if (Validator.isUrl(importFileName)) { URL url = new URL(importFileName); URLConnection urlConnection = url.openConnection(); importContent = StringUtil.read( urlConnection.getInputStream()); } else { ServletPaths importFileServletPaths = servletPaths.down( importFileName); importContent = importFileServletPaths.getContent(); if (importContent == null) { if (_log.isWarnEnabled()) { _log.warn( "File " + importFileServletPaths.getResourcePath() + " does not exist"); } importContent = StringPool.BLANK; } String importDirName = StringPool.BLANK; int slashPos = importFileName.lastIndexOf(CharPool.SLASH); if (slashPos != -1) { importDirName = importFileName.substring( 0, slashPos + 1); } ServletPaths importDirServletPaths = servletPaths.down( importDirName); importContent = aggregateCss( importDirServletPaths, importContent); // LEP-7540 String baseURL = _BASE_URL.concat( importDirServletPaths.getResourcePath()); if (!baseURL.endsWith(StringPool.SLASH)) { baseURL += StringPool.SLASH; } importContent = AggregateUtil.updateRelativeURLs( importContent, baseURL); } if (Validator.isNotNull(mediaQuery)) { sb.append(_CSS_MEDIA_QUERY); sb.append(CharPool.SPACE); sb.append(mediaQuery); sb.append(CharPool.OPEN_CURLY_BRACE); sb.append(importContent); sb.append(CharPool.CLOSE_CURLY_BRACE); pos = mediaQueryImportY + 1; } else { sb.append(importContent); pos = importY + _CSS_IMPORT_END.length(); } } } return sb.toString(); } public static String aggregateJavaScript( ServletPaths servletPaths, String[] fileNames) { StringBundler sb = new StringBundler(fileNames.length * 2); for (String fileName : fileNames) { ServletPaths fileServletPaths = servletPaths.down(fileName); String content = fileServletPaths.getContent(); if (Validator.isNull(content)) { continue; } sb.append(content); sb.append(StringPool.NEW_LINE); } return getJavaScriptContent( StringUtil.merge(fileNames, "+"), sb.toString()); } @Override public void init(FilterConfig filterConfig) { super.init(filterConfig); _servletContext = filterConfig.getServletContext(); File tempDir = (File)_servletContext.getAttribute( JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR); _tempDir = new File(tempDir, _TEMP_DIR); _tempDir.mkdirs(); } protected static String getJavaScriptContent( String resourceName, String content) { return MinifierUtil.minifyJavaScript(resourceName, content); } protected Object getBundleContent( HttpServletRequest request, HttpServletResponse response) throws IOException { String minifierType = ParamUtil.getString(request, "minifierType"); String bundleId = ParamUtil.getString( request, "bundleId", ParamUtil.getString(request, "minifierBundleId")); if (Validator.isNull(minifierType) || Validator.isNull(bundleId) || !ArrayUtil.contains(PropsValues.JAVASCRIPT_BUNDLE_IDS, bundleId)) { return null; } String bundleDirName = PropsUtil.get( PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(bundleId)); ServletContext jsServletContext = PortalWebResourcesUtil.getServletContext( PortalWebResourceConstants.RESOURCE_TYPE_JS); URL bundleDirURL = jsServletContext.getResource(bundleDirName); if (bundleDirURL == null) { return null; } String cacheFileName = bundleId; String[] fileNames = JavaScriptBundleUtil.getFileNames(bundleId); File cacheFile = new File(_tempDir, cacheFileName); if (cacheFile.exists()) { long lastModified = PortalWebResourcesUtil.getLastModified( PortalWebResourceConstants.RESOURCE_TYPE_JS); if (lastModified <= cacheFile.lastModified()) { response.setContentType(ContentTypes.TEXT_JAVASCRIPT); return cacheFile; } } if (_log.isInfoEnabled()) { _log.info("Aggregating JavaScript bundle " + bundleId); } String content = null; if (fileNames.length == 0) { content = StringPool.BLANK; } else { content = aggregateJavaScript( new ServletPaths(jsServletContext, bundleDirName), fileNames); } response.setContentType(ContentTypes.TEXT_JAVASCRIPT); FileUtil.write(cacheFile, content); return content; } protected String getCacheFileName(HttpServletRequest request) { return _cacheFileNameGenerator.getCacheFileName( AggregateFilter.class, request, _REMOVE_PARAMETER_NAMES, null); } protected Object getContent( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { String minifierType = ParamUtil.getString(request, "minifierType"); String minifierBundleId = ParamUtil.getString( request, "minifierBundleId"); String minifierBundleDirName = ParamUtil.getString( request, "minifierBundleDir"); if (Validator.isNull(minifierType) || Validator.isNotNull(minifierBundleId) || Validator.isNotNull(minifierBundleDirName)) { return null; } String requestURI = request.getRequestURI(); String resourcePath = requestURI; String contextPath = request.getContextPath(); if (!contextPath.equals(StringPool.SLASH)) { resourcePath = resourcePath.substring(contextPath.length()); } if (resourcePath.endsWith(_CSS_EXTENSION) && PortalUtil.isRightToLeft(request)) { int pos = resourcePath.lastIndexOf(StringPool.PERIOD); resourcePath = resourcePath.substring(0, pos) + "_rtl" + resourcePath.substring(pos); } URL resourceURL = _servletContext.getResource(resourcePath); if (resourceURL == null) { resourceURL = PortalWebResourcesUtil.getResource(resourcePath); if (resourceURL == null) { return null; } } String cacheCommonFileName = getCacheFileName(request); File cacheContentTypeFile = new File( _tempDir, cacheCommonFileName + "_E_CTYPE"); File cacheDataFile = new File( _tempDir, cacheCommonFileName + "_E_DATA"); if (cacheDataFile.exists() && (cacheDataFile.lastModified() >= URLUtil.getLastModifiedTime(resourceURL))) { if (cacheContentTypeFile.exists()) { String contentType = FileUtil.read(cacheContentTypeFile); response.setContentType(contentType); } return cacheDataFile; } String content = null; if (resourcePath.endsWith(_CSS_EXTENSION)) { if (_log.isInfoEnabled()) { _log.info("Minifying CSS " + resourcePath); } content = getCssContent( request, response, resourceURL, resourcePath); response.setContentType(ContentTypes.TEXT_CSS); FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS); } else if (resourcePath.endsWith(_JAVASCRIPT_EXTENSION)) { if (_log.isInfoEnabled()) { _log.info("Minifying JavaScript " + resourcePath); } content = getJavaScriptContent(resourceURL); response.setContentType(ContentTypes.TEXT_JAVASCRIPT); FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT); } else if (resourcePath.endsWith(_JSP_EXTENSION)) { if (_log.isInfoEnabled()) { _log.info("Minifying JSP " + resourcePath); } BufferCacheServletResponse bufferCacheServletResponse = new BufferCacheServletResponse(response); processFilter( AggregateFilter.class.getName(), request, bufferCacheServletResponse, filterChain); bufferCacheServletResponse.finishResponse(false); content = bufferCacheServletResponse.getString(); if (minifierType.equals("css")) { content = getCssContent( request, response, resourcePath, content); } else if (minifierType.equals("js")) { content = getJavaScriptContent(resourcePath, content); } FileUtil.write( cacheContentTypeFile, bufferCacheServletResponse.getContentType()); } else { return null; } FileUtil.write(cacheDataFile, content); return content; } protected String getCssContent( HttpServletRequest request, HttpServletResponse response, String resourcePath, String content) { try { ServletContext cssServletContext = null; String requestURI = request.getRequestURI(); if (PortalWebResourcesUtil.hasContextPath(requestURI)) { cssServletContext = PortalWebResourcesUtil.getPathServletContext(requestURI); } if (cssServletContext == null) { cssServletContext = _servletContext; } content = DynamicCSSUtil.replaceToken( cssServletContext, request, content); } catch (Exception e) { _log.error("Unable to replace tokens in CSS " + resourcePath, e); if (_log.isDebugEnabled()) { _log.debug(content); } response.setHeader( HttpHeaders.CACHE_CONTROL, HttpHeaders.CACHE_CONTROL_NO_CACHE_VALUE); } String browserId = ParamUtil.getString(request, "browserId"); if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) { Matcher matcher = _pattern.matcher(content); content = matcher.replaceAll(StringPool.BLANK); } return MinifierUtil.minifyCss(content); } protected String getCssContent( HttpServletRequest request, HttpServletResponse response, URL resourceURL, String resourcePath) throws IOException { ServletContext cssServletContext = null; String resourcePathRoot = null; String requestURI = request.getRequestURI(); if (PortalWebResourcesUtil.hasContextPath(requestURI)) { cssServletContext = PortalWebResourcesUtil.getPathServletContext( requestURI); resourcePathRoot = "/"; } if (cssServletContext == null) { cssServletContext = _servletContext; resourcePathRoot = ServletPaths.getParentPath(resourcePath); } URLConnection urlConnection = resourceURL.openConnection(); String content = StringUtil.read(urlConnection.getInputStream()); content = aggregateCss( new ServletPaths(cssServletContext, resourcePathRoot), content); return getCssContent(request, response, resourcePath, content); } protected String getJavaScriptContent(URL resourceURL) throws IOException { URLConnection urlConnection = resourceURL.openConnection(); String content = StringUtil.read(urlConnection.getInputStream()); return getJavaScriptContent(resourceURL.toString(), content); } @Override protected boolean isModuleRequest(HttpServletRequest request) { String requestURI = request.getRequestURI(); if (PortalWebResourcesUtil.hasContextPath(requestURI)) { return false; } return super.isModuleRequest(request); } @Override protected void processFilter( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws Exception { Object minifiedContent = getContent(request, response, filterChain); if (minifiedContent == null) { minifiedContent = getBundleContent(request, response); } if (minifiedContent == null) { processFilter( AggregateFilter.class.getName(), request, response, filterChain); } else { if (minifiedContent instanceof File) { ServletResponseUtil.write(response, (File)minifiedContent); } else if (minifiedContent instanceof String) { ServletResponseUtil.write(response, (String)minifiedContent); } } } private static final String _BASE_URL = "@base_url@"; private static final String _CSS_COMMENT_BEGIN = "/*"; private static final String _CSS_COMMENT_END = "*/"; private static final String _CSS_EXTENSION = ".css"; private static final String _CSS_IMPORT_BEGIN = "@import url("; private static final String _CSS_IMPORT_END = ");"; private static final String _CSS_MEDIA_QUERY = "@media"; private static final String _JAVASCRIPT_EXTENSION = ".js"; private static final String _JSP_EXTENSION = ".jsp"; private static final String[] _REMOVE_PARAMETER_NAMES = {"zx"}; private static final String _TEMP_DIR = "aggregate"; private static final Log _log = LogFactoryUtil.getLog( AggregateFilter.class); private static final Pattern _pattern = Pattern.compile( "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE); private final CacheFileNameGenerator _cacheFileNameGenerator = new CacheFileNameGenerator(); private ServletContext _servletContext; private File _tempDir; }