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