001    /**
002     * Copyright (c) 2000-2010 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.minifier;
016    
017    import com.liferay.portal.kernel.configuration.Filter;
018    import com.liferay.portal.kernel.log.Log;
019    import com.liferay.portal.kernel.log.LogFactoryUtil;
020    import com.liferay.portal.kernel.servlet.BrowserSniffer;
021    import com.liferay.portal.kernel.servlet.ServletContextUtil;
022    import com.liferay.portal.kernel.servlet.StringServletResponse;
023    import com.liferay.portal.kernel.util.ArrayUtil;
024    import com.liferay.portal.kernel.util.CharPool;
025    import com.liferay.portal.kernel.util.ContentTypes;
026    import com.liferay.portal.kernel.util.FileUtil;
027    import com.liferay.portal.kernel.util.GetterUtil;
028    import com.liferay.portal.kernel.util.ParamUtil;
029    import com.liferay.portal.kernel.util.PropsKeys;
030    import com.liferay.portal.kernel.util.StringBundler;
031    import com.liferay.portal.kernel.util.StringPool;
032    import com.liferay.portal.kernel.util.StringUtil;
033    import com.liferay.portal.kernel.util.Validator;
034    import com.liferay.portal.servlet.filters.BasePortalFilter;
035    import com.liferay.portal.util.JavaScriptBundleUtil;
036    import com.liferay.portal.util.MinifierUtil;
037    import com.liferay.portal.util.PropsUtil;
038    import com.liferay.portal.util.PropsValues;
039    import com.liferay.util.SystemProperties;
040    import com.liferay.util.servlet.ServletResponseUtil;
041    import com.liferay.util.servlet.filters.CacheResponseUtil;
042    
043    import java.io.File;
044    import java.io.IOException;
045    
046    import java.util.regex.Matcher;
047    import java.util.regex.Pattern;
048    
049    import javax.servlet.FilterChain;
050    import javax.servlet.FilterConfig;
051    import javax.servlet.ServletContext;
052    import javax.servlet.http.HttpServletRequest;
053    import javax.servlet.http.HttpServletResponse;
054    
055    /**
056     * @author Brian Wing Shun Chan
057     */
058    public class MinifierFilter extends BasePortalFilter {
059    
060            public void init(FilterConfig filterConfig) {
061                    super.init(filterConfig);
062    
063                    _servletContext = filterConfig.getServletContext();
064                    _servletContextName = GetterUtil.getString(
065                            _servletContext.getServletContextName());
066    
067                    if (Validator.isNull(_servletContextName)) {
068                            _tempDir += "/portal";
069                    }
070            }
071    
072            protected String aggregateCss(String dir, String content)
073                    throws IOException {
074    
075                    StringBuilder sb = new StringBuilder(content.length());
076    
077                    int pos = 0;
078    
079                    while (true) {
080                            int x = content.indexOf(_CSS_IMPORT_BEGIN, pos);
081                            int y = content.indexOf(
082                                    _CSS_IMPORT_END, x + _CSS_IMPORT_BEGIN.length());
083    
084                            if ((x == -1) || (y == -1)) {
085                                    sb.append(content.substring(pos, content.length()));
086    
087                                    break;
088                            }
089                            else {
090                                    sb.append(content.substring(pos, x));
091    
092                                    String importFileName = content.substring(
093                                            x + _CSS_IMPORT_BEGIN.length(), y);
094    
095                                    String importFullFileName = dir.concat(StringPool.SLASH).concat(
096                                            importFileName);
097    
098                                    String importContent = FileUtil.read(importFullFileName);
099    
100                                    if (importContent == null) {
101                                            if (_log.isWarnEnabled()) {
102                                                    _log.warn(
103                                                            "File " + importFullFileName + " does not exist");
104                                            }
105    
106                                            importContent = StringPool.BLANK;
107                                    }
108    
109                                    String importDir = StringPool.BLANK;
110    
111                                    int slashPos = importFileName.lastIndexOf(CharPool.SLASH);
112    
113                                    if (slashPos != -1) {
114                                            importDir = StringPool.SLASH.concat(
115                                                    importFileName.substring(0, slashPos + 1));
116                                    }
117    
118                                    importContent = aggregateCss(dir + importDir, importContent);
119    
120                                    int importDepth = StringUtil.count(
121                                            importFileName, StringPool.SLASH);
122    
123                                    // LEP-7540
124    
125                                    String relativePath = StringPool.BLANK;
126    
127                                    for (int i = 0; i < importDepth; i++) {
128                                            relativePath += "../";
129                                    }
130    
131                                    importContent = StringUtil.replace(
132                                            importContent,
133                                            new String[] {
134                                                    "url('" + relativePath,
135                                                    "url(\"" + relativePath,
136                                                    "url(" + relativePath
137                                            },
138                                            new String[] {
139                                                    "url('[$TEMP_RELATIVE_PATH$]",
140                                                    "url(\"[$TEMP_RELATIVE_PATH$]",
141                                                    "url([$TEMP_RELATIVE_PATH$]"
142                                            });
143    
144                                    importContent = StringUtil.replace(
145                                            importContent, "[$TEMP_RELATIVE_PATH$]", StringPool.BLANK);
146    
147                                    sb.append(importContent);
148    
149                                    pos = y + _CSS_IMPORT_END.length();
150                            }
151                    }
152    
153                    return sb.toString();
154            }
155    
156            protected String getMinifiedBundleContent(
157                            HttpServletRequest request, HttpServletResponse response)
158                    throws IOException {
159    
160                    String minifierType = ParamUtil.getString(request, "minifierType");
161                    String minifierBundleId = ParamUtil.getString(
162                            request, "minifierBundleId");
163    
164                    if (Validator.isNull(minifierType) ||
165                            Validator.isNull(minifierBundleId) ||
166                            !ArrayUtil.contains(
167                                    PropsValues.JAVASCRIPT_BUNDLE_IDS, minifierBundleId)) {
168    
169                            return null;
170                    }
171    
172                    String minifierBundleDir = PropsUtil.get(
173                            PropsKeys.JAVASCRIPT_BUNDLE_DIR, new Filter(minifierBundleId));
174    
175                    String bundleDirRealPath = ServletContextUtil.getRealPath(
176                            _servletContext, minifierBundleDir);
177    
178                    if (bundleDirRealPath == null) {
179                            return null;
180                    }
181    
182                    StringBundler sb = new StringBundler(4);
183    
184                    sb.append(_tempDir);
185                    sb.append(request.getRequestURI());
186    
187                    String queryString = request.getQueryString();
188    
189                    if (queryString != null) {
190                            sb.append(_QUESTION_SEPARATOR);
191                            sb.append(sterilizeQueryString(queryString));
192                    }
193    
194                    String cacheFileName = sb.toString();
195    
196                    String[] fileNames = JavaScriptBundleUtil.getFileNames(
197                            minifierBundleId);
198    
199                    File cacheFile = new File(cacheFileName);
200    
201                    if (cacheFile.exists()) {
202                            boolean staleCache = false;
203    
204                            for (String fileName : fileNames) {
205                                    File file = new File(
206                                            bundleDirRealPath + StringPool.SLASH + fileName);
207    
208                                    if (file.lastModified() > cacheFile.lastModified()) {
209                                            staleCache = true;
210    
211                                            break;
212                                    }
213                            }
214    
215                            if (!staleCache) {
216                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
217    
218                                    return FileUtil.read(cacheFile);
219                            }
220                    }
221    
222                    if (_log.isInfoEnabled()) {
223                            _log.info("Minifying JavaScript bundle " + minifierBundleId);
224                    }
225    
226                    String minifiedContent = null;
227    
228                    if (fileNames.length == 0) {
229                            minifiedContent = StringPool.BLANK;
230                    }
231                    else {
232                            sb = new StringBundler(fileNames.length * 2);
233    
234                            for (String fileName : fileNames) {
235                                    String content = FileUtil.read(
236                                            bundleDirRealPath + StringPool.SLASH + fileName);
237    
238                                    sb.append(content);
239                                    sb.append(StringPool.NEW_LINE);
240                            }
241    
242                            minifiedContent = minifyJavaScript(sb.toString());
243                    }
244    
245                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
246    
247                    FileUtil.write(cacheFile, minifiedContent);
248    
249                    return minifiedContent;
250            }
251    
252            protected String getMinifiedContent(
253                            HttpServletRequest request, HttpServletResponse response,
254                            FilterChain filterChain)
255                    throws Exception {
256    
257                    String minifierType = ParamUtil.getString(request, "minifierType");
258                    String minifierBundleId = ParamUtil.getString(
259                            request, "minifierBundleId");
260                    String minifierBundleDir = ParamUtil.getString(
261                            request, "minifierBundleDir");
262    
263                    if (Validator.isNull(minifierType) ||
264                            Validator.isNotNull(minifierBundleId) ||
265                            Validator.isNotNull(minifierBundleDir)) {
266    
267                            return null;
268                    }
269    
270                    String requestURI = request.getRequestURI();
271    
272                    String requestPath = requestURI;
273    
274                    String contextPath = request.getContextPath();
275    
276                    if (!contextPath.equals(StringPool.SLASH)) {
277                            requestPath = requestPath.substring(contextPath.length());
278                    }
279    
280                    String realPath = ServletContextUtil.getRealPath(
281                            _servletContext, requestPath);
282    
283                    if (realPath == null) {
284                            return null;
285                    }
286    
287                    realPath = StringUtil.replace(
288                            realPath, CharPool.BACK_SLASH, CharPool.SLASH);
289    
290                    File file = new File(realPath);
291    
292                    if (!file.exists()) {
293                            return null;
294                    }
295    
296                    String minifiedContent = null;
297    
298                    StringBundler sb = new StringBundler(4);
299    
300                    sb.append(_tempDir);
301                    sb.append(requestURI);
302    
303                    String queryString = request.getQueryString();
304    
305                    if (queryString != null) {
306                            sb.append(_QUESTION_SEPARATOR);
307                            sb.append(sterilizeQueryString(queryString));
308                    }
309    
310                    String cacheCommonFileName = sb.toString();
311    
312                    File cacheContentTypeFile = new File(
313                            cacheCommonFileName + "_E_CONTENT_TYPE");
314                    File cacheDataFile = new File(cacheCommonFileName + "_E_DATA");
315    
316                    if ((cacheDataFile.exists()) &&
317                            (cacheDataFile.lastModified() >= file.lastModified())) {
318    
319                            minifiedContent = FileUtil.read(cacheDataFile);
320    
321                            if (cacheContentTypeFile.exists()) {
322                                    String contentType = FileUtil.read(cacheContentTypeFile);
323    
324                                    response.setContentType(contentType);
325                            }
326                    }
327                    else {
328                            if (realPath.endsWith(_CSS_EXTENSION)) {
329                                    if (_log.isInfoEnabled()) {
330                                            _log.info("Minifying CSS " + file);
331                                    }
332    
333                                    minifiedContent = minifyCss(request, file);
334    
335                                    response.setContentType(ContentTypes.TEXT_CSS);
336    
337                                    FileUtil.write(cacheContentTypeFile, ContentTypes.TEXT_CSS);
338                            }
339                            else if (realPath.endsWith(_JAVASCRIPT_EXTENSION)) {
340                                    if (_log.isInfoEnabled()) {
341                                            _log.info("Minifying JavaScript " + file);
342                                    }
343    
344                                    minifiedContent = minifyJavaScript(file);
345    
346                                    response.setContentType(ContentTypes.TEXT_JAVASCRIPT);
347    
348                                    FileUtil.write(
349                                            cacheContentTypeFile, ContentTypes.TEXT_JAVASCRIPT);
350                            }
351                            else if (realPath.endsWith(_JSP_EXTENSION)) {
352                                    if (_log.isInfoEnabled()) {
353                                            _log.info("Minifying JSP " + file);
354                                    }
355    
356                                    StringServletResponse stringResponse =
357                                            new StringServletResponse(response);
358    
359                                    processFilter(
360                                            MinifierFilter.class, request, stringResponse, filterChain);
361    
362                                    CacheResponseUtil.setHeaders(
363                                            response, stringResponse.getHeaders());
364    
365                                    response.setContentType(stringResponse.getContentType());
366    
367                                    minifiedContent = stringResponse.getString();
368    
369                                    if (minifierType.equals("css")) {
370                                            minifiedContent = minifyCss(request, minifiedContent);
371                                    }
372                                    else if (minifierType.equals("js")) {
373                                            minifiedContent = minifyJavaScript(minifiedContent);
374                                    }
375    
376                                    FileUtil.write(
377                                            cacheContentTypeFile, stringResponse.getContentType());
378                            }
379                            else {
380                                    return null;
381                            }
382    
383                            FileUtil.write(cacheDataFile, minifiedContent);
384                    }
385    
386                    return minifiedContent;
387            }
388    
389            protected String minifyCss(HttpServletRequest request, File file)
390                    throws IOException {
391    
392                    String content = FileUtil.read(file);
393    
394                    content = aggregateCss(file.getParent(), content);
395    
396                    return minifyCss(request, content);
397            }
398    
399            protected String minifyCss(HttpServletRequest request, String content) {
400                    String browserId = ParamUtil.getString(request, "browserId");
401    
402                    if (!browserId.equals(BrowserSniffer.BROWSER_ID_IE)) {
403                            Matcher matcher = _pattern.matcher(content);
404    
405                            content = matcher.replaceAll(StringPool.BLANK);
406                    }
407    
408                    return MinifierUtil.minifyCss(content);
409            }
410    
411            protected String minifyJavaScript(File file) throws IOException {
412                    String content = FileUtil.read(file);
413    
414                    return minifyJavaScript(content);
415            }
416    
417            protected String minifyJavaScript(String content) {
418                    return MinifierUtil.minifyJavaScript(content);
419            }
420    
421            protected void processFilter(
422                            HttpServletRequest request, HttpServletResponse response,
423                            FilterChain filterChain)
424                    throws Exception {
425    
426                    String minifiedContent = getMinifiedContent(
427                            request, response, filterChain);
428    
429                    if (Validator.isNull(minifiedContent)) {
430                            minifiedContent = getMinifiedBundleContent(request, response);
431                    }
432    
433                    if (Validator.isNull(minifiedContent)) {
434                            processFilter(MinifierFilter.class, request, response, filterChain);
435                    }
436                    else {
437                            ServletResponseUtil.write(response, minifiedContent);
438                    }
439            }
440    
441            protected String sterilizeQueryString(String queryString) {
442                    return StringUtil.replace(
443                            queryString,
444                            new String[] {StringPool.SLASH, StringPool.BACK_SLASH},
445                            new String[] {StringPool.UNDERLINE, StringPool.UNDERLINE});
446            }
447    
448            private static final String _CSS_IMPORT_BEGIN = "@import url(";
449    
450            private static final String _CSS_IMPORT_END = ");";
451    
452            private static final String _CSS_EXTENSION = ".css";
453    
454            private static final String _JAVASCRIPT_EXTENSION = ".js";
455    
456            private static final String _JSP_EXTENSION = ".jsp";
457    
458            private static final String _QUESTION_SEPARATOR = "_Q_";
459    
460            private static final String _TEMP_DIR =
461                    SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/minifier";
462    
463            private static Log _log = LogFactoryUtil.getLog(MinifierFilter.class);
464    
465            private static Pattern _pattern = Pattern.compile(
466                    "^(\\.ie|\\.js\\.ie)([^}]*)}", Pattern.MULTILINE);
467    
468            private ServletContext _servletContext;
469            private String _servletContextName;
470            private String _tempDir = _TEMP_DIR;
471    
472    }