001    /**
002     * Copyright (c) 2000-2013 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.dynamiccss;
016    
017    import com.liferay.portal.kernel.io.unsync.UnsyncByteArrayOutputStream;
018    import com.liferay.portal.kernel.io.unsync.UnsyncPrintWriter;
019    import com.liferay.portal.kernel.log.Log;
020    import com.liferay.portal.kernel.log.LogFactoryUtil;
021    import com.liferay.portal.kernel.util.CharPool;
022    import com.liferay.portal.kernel.util.ContextPathUtil;
023    import com.liferay.portal.kernel.util.GetterUtil;
024    import com.liferay.portal.kernel.util.JavaConstants;
025    import com.liferay.portal.kernel.util.ParamUtil;
026    import com.liferay.portal.kernel.util.SessionParamUtil;
027    import com.liferay.portal.kernel.util.StringBundler;
028    import com.liferay.portal.kernel.util.StringPool;
029    import com.liferay.portal.kernel.util.StringUtil;
030    import com.liferay.portal.kernel.util.UnsyncPrintWriterPool;
031    import com.liferay.portal.kernel.util.Validator;
032    import com.liferay.portal.kernel.util.WebKeys;
033    import com.liferay.portal.model.PortletConstants;
034    import com.liferay.portal.model.Theme;
035    import com.liferay.portal.scripting.ruby.RubyExecutor;
036    import com.liferay.portal.service.ThemeLocalServiceUtil;
037    import com.liferay.portal.theme.ThemeDisplay;
038    import com.liferay.portal.tools.SassToCssBuilder;
039    import com.liferay.portal.util.ClassLoaderUtil;
040    import com.liferay.portal.util.PortalUtil;
041    import com.liferay.portal.util.PropsValues;
042    
043    import java.io.File;
044    
045    import java.net.URL;
046    import java.net.URLConnection;
047    import java.net.URLDecoder;
048    
049    import java.util.HashMap;
050    import java.util.Map;
051    import java.util.regex.Matcher;
052    import java.util.regex.Pattern;
053    
054    import javax.servlet.ServletContext;
055    import javax.servlet.http.HttpServletRequest;
056    
057    import org.apache.commons.lang.time.StopWatch;
058    
059    /**
060     * @author Raymond Aug??
061     * @author Sergio S??nchez
062     */
063    public class DynamicCSSUtil {
064    
065            public static void init() {
066                    try {
067                            if (_initialized) {
068                                    return;
069                            }
070    
071                            _rubyScript = StringUtil.read(
072                                    ClassLoaderUtil.getPortalClassLoader(),
073                                    "com/liferay/portal/servlet/filters/dynamiccss/main.rb");
074    
075                            _initialized = true;
076                    }
077                    catch (Exception e) {
078                            _log.error(e, e);
079                    }
080            }
081    
082            public static String parseSass(
083                            ServletContext servletContext, HttpServletRequest request,
084                            String resourcePath, String content)
085                    throws Exception {
086    
087                    if (!DynamicCSSFilter.ENABLED) {
088                            return content;
089                    }
090    
091                    StopWatch stopWatch = new StopWatch();
092    
093                    stopWatch.start();
094    
095                    // Request will only be null when called by StripFilterTest
096    
097                    if (request == null) {
098                            return content;
099                    }
100    
101                    ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
102                            WebKeys.THEME_DISPLAY);
103    
104                    Theme theme = null;
105    
106                    if (themeDisplay == null) {
107                            theme = _getTheme(request);
108    
109                            if (theme == null) {
110                                    String currentURL = PortalUtil.getCurrentURL(request);
111    
112                                    if (_log.isWarnEnabled()) {
113                                            _log.warn("No theme found for " + currentURL);
114                                    }
115    
116                                    return content;
117                            }
118                    }
119    
120                    String parsedContent = null;
121    
122                    boolean themeCssFastLoad = _isThemeCssFastLoad(request, themeDisplay);
123    
124                    URLConnection cacheResourceURLConnection = null;
125    
126                    URL cacheResourceURL = _getCacheResource(servletContext, resourcePath);
127    
128                    if (cacheResourceURL != null) {
129                            cacheResourceURLConnection = cacheResourceURL.openConnection();
130    
131                            if (!themeCssFastLoad) {
132                                    URL resourceURL = servletContext.getResource(resourcePath);
133    
134                                    if (resourceURL != null) {
135                                            URLConnection resourceURLConnection =
136                                                    resourceURL.openConnection();
137    
138                                            if (cacheResourceURLConnection.getLastModified() <
139                                                            resourceURLConnection.getLastModified()) {
140    
141                                                    cacheResourceURLConnection = null;
142                                            }
143                                    }
144                            }
145                    }
146    
147                    if ((themeCssFastLoad || !content.contains(_CSS_IMPORT_BEGIN)) &&
148                            (cacheResourceURLConnection != null)) {
149    
150                            parsedContent = StringUtil.read(
151                                    cacheResourceURLConnection.getInputStream());
152    
153                            if (_log.isDebugEnabled()) {
154                                    _log.debug(
155                                            "Loading SASS cache from " + cacheResourceURL.getPath() +
156                                                    " takes " + stopWatch.getTime() + " ms");
157                            }
158                    }
159                    else {
160                            content = SassToCssBuilder.parseStaticTokens(content);
161    
162                            String queryString = request.getQueryString();
163    
164                            if (!themeCssFastLoad && Validator.isNotNull(queryString)) {
165                                    content = propagateQueryString(content, queryString);
166                            }
167    
168                            parsedContent = _parseSass(
169                                    servletContext, request, themeDisplay, theme, resourcePath,
170                                    content);
171    
172                            if (_log.isDebugEnabled()) {
173                                    _log.debug(
174                                            "Parsing SASS for " + resourcePath + " takes " +
175                                                    stopWatch.getTime() + " ms");
176                            }
177                    }
178    
179                    if (Validator.isNull(parsedContent)) {
180                            return content;
181                    }
182    
183                    String portalContextPath = PortalUtil.getPathContext();
184    
185                    String baseURL = portalContextPath;
186    
187                    String contextPath = ContextPathUtil.getContextPath(servletContext);
188    
189                    if (!contextPath.equals(portalContextPath)) {
190                            baseURL = StringPool.SLASH.concat(
191                                    GetterUtil.getString(servletContext.getServletContextName()));
192                    }
193    
194                    if (baseURL.endsWith(StringPool.SLASH)) {
195                            baseURL = baseURL.substring(0, baseURL.length() - 1);
196                    }
197    
198                    parsedContent = StringUtil.replace(
199                            parsedContent,
200                            new String[] {
201                                    "@base_url@", "@portal_ctx@", "@theme_image_path@"
202                            },
203                            new String[] {
204                                    baseURL, portalContextPath,
205                                    _getThemeImagesPath(request, themeDisplay, theme)
206                            });
207    
208                    return parsedContent;
209            }
210    
211            /**
212             * @see com.liferay.portal.servlet.filters.aggregate.AggregateFilter#aggregateCss(
213             *      com.liferay.portal.servlet.filters.aggregate.ServletPaths, String)
214             */
215            protected static String propagateQueryString(
216                    String content, String queryString) {
217    
218                    StringBuilder sb = new StringBuilder(content.length());
219    
220                    int pos = 0;
221    
222                    while (true) {
223                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
224                            int importY = content.indexOf(
225                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
226    
227                            if ((importX == -1) || (importY == -1)) {
228                                    sb.append(content.substring(pos));
229    
230                                    break;
231                            }
232    
233                            sb.append(content.substring(pos, importX));
234                            sb.append(_CSS_IMPORT_BEGIN);
235    
236                            String url = content.substring(
237                                    importX + _CSS_IMPORT_BEGIN.length(), importY);
238    
239                            char firstChar = url.charAt(0);
240    
241                            if (firstChar == CharPool.APOSTROPHE) {
242                                    sb.append(CharPool.APOSTROPHE);
243                            }
244                            else if (firstChar == CharPool.QUOTE) {
245                                    sb.append(CharPool.QUOTE);
246                            }
247    
248                            url = StringUtil.unquote(url);
249    
250                            sb.append(url);
251    
252                            if (url.indexOf(CharPool.QUESTION) != -1) {
253                                    sb.append(CharPool.AMPERSAND);
254                            }
255                            else {
256                                    sb.append(CharPool.QUESTION);
257                            }
258    
259                            sb.append(queryString);
260    
261                            if (firstChar == CharPool.APOSTROPHE) {
262                                    sb.append(CharPool.APOSTROPHE);
263                            }
264                            else if (firstChar == CharPool.QUOTE) {
265                                    sb.append(CharPool.QUOTE);
266                            }
267    
268                            sb.append(_CSS_IMPORT_END);
269    
270                            pos = importY + _CSS_IMPORT_END.length();
271                    }
272    
273                    return sb.toString();
274            }
275    
276            private static URL _getCacheResource(
277                            ServletContext servletContext, String resourcePath)
278                    throws Exception {
279    
280                    int pos = resourcePath.lastIndexOf(StringPool.SLASH);
281    
282                    String cacheFileName =
283                            resourcePath.substring(0, pos + 1) + ".sass-cache/" +
284                                    resourcePath.substring(pos + 1);
285    
286                    return servletContext.getResource(cacheFileName);
287            }
288    
289            private static String _getCssThemePath(
290                            ServletContext servletContext, HttpServletRequest request,
291                            ThemeDisplay themeDisplay, Theme theme)
292                    throws Exception {
293    
294                    if (themeDisplay != null) {
295                            return themeDisplay.getPathThemeCss();
296                    }
297    
298                    if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
299                            String cdnHost = PortalUtil.getCDNHost(request);
300    
301                            if (Validator.isNotNull(cdnHost)) {
302                                    return cdnHost.concat(theme.getStaticResourcePath()).concat(
303                                            theme.getCssPath());
304                            }
305                    }
306    
307                    return servletContext.getRealPath(theme.getCssPath());
308            }
309    
310            private static File _getSassTempDir(ServletContext servletContext) {
311                    File sassTempDir = (File)servletContext.getAttribute(_SASS_DIR_KEY);
312    
313                    if (sassTempDir != null) {
314                            return sassTempDir;
315                    }
316    
317                    File tempDir = (File)servletContext.getAttribute(
318                            JavaConstants.JAVAX_SERVLET_CONTEXT_TEMPDIR);
319    
320                    sassTempDir = new File(tempDir, _SASS_DIR);
321    
322                    sassTempDir.mkdirs();
323    
324                    servletContext.setAttribute(_SASS_DIR_KEY, sassTempDir);
325    
326                    return sassTempDir;
327            }
328    
329            private static Theme _getTheme(HttpServletRequest request)
330                    throws Exception {
331    
332                    long companyId = PortalUtil.getCompanyId(request);
333    
334                    String themeId = ParamUtil.getString(request, "themeId");
335    
336                    if (Validator.isNotNull(themeId)) {
337                            try {
338                                    Theme theme = ThemeLocalServiceUtil.getTheme(
339                                            companyId, themeId, false);
340    
341                                    return theme;
342                            }
343                            catch (Exception e) {
344                                    _log.error(e, e);
345                            }
346                    }
347    
348                    String requestURI = URLDecoder.decode(
349                            request.getRequestURI(), StringPool.UTF8);
350    
351                    Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
352    
353                    if (portalThemeMatcher.find()) {
354                            String themePathId = portalThemeMatcher.group(1);
355    
356                            themePathId = StringUtil.replace(
357                                    themePathId, StringPool.UNDERLINE, StringPool.BLANK);
358    
359                            themeId = PortalUtil.getJsSafePortletId(themePathId);
360                    }
361                    else {
362                            Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
363                                    requestURI);
364    
365                            if (pluginThemeMatcher.find()) {
366                                    String themePathId = pluginThemeMatcher.group(1);
367    
368                                    themePathId = StringUtil.replace(
369                                            themePathId, StringPool.UNDERLINE, StringPool.BLANK);
370    
371                                    StringBundler sb = new StringBundler(4);
372    
373                                    sb.append(themePathId);
374                                    sb.append(PortletConstants.WAR_SEPARATOR);
375                                    sb.append(themePathId);
376                                    sb.append("theme");
377    
378                                    themePathId = sb.toString();
379    
380                                    themeId = PortalUtil.getJsSafePortletId(themePathId);
381                            }
382                    }
383    
384                    if (Validator.isNull(themeId)) {
385                            return null;
386                    }
387    
388                    try {
389                            Theme theme = ThemeLocalServiceUtil.getTheme(
390                                    companyId, themeId, false);
391    
392                            return theme;
393                    }
394                    catch (Exception e) {
395                            _log.error(e, e);
396                    }
397    
398                    return null;
399            }
400    
401            private static String _getThemeImagesPath(
402                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
403                    throws Exception {
404    
405                    String themeImagesPath = null;
406    
407                    if (themeDisplay != null) {
408                            themeImagesPath = themeDisplay.getPathThemeImages();
409                    }
410                    else {
411                            String cdnHost = PortalUtil.getCDNHost(request);
412                            String themeStaticResourcePath = theme.getStaticResourcePath();
413    
414                            themeImagesPath =
415                                    cdnHost + themeStaticResourcePath + theme.getImagesPath();
416                    }
417    
418                    return themeImagesPath;
419            }
420    
421            private static boolean _isThemeCssFastLoad(
422                    HttpServletRequest request, ThemeDisplay themeDisplay) {
423    
424                    if (themeDisplay != null) {
425                            return themeDisplay.isThemeCssFastLoad();
426                    }
427    
428                    return SessionParamUtil.getBoolean(
429                            request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
430            }
431    
432            private static String _parseSass(
433                            ServletContext servletContext, HttpServletRequest request,
434                            ThemeDisplay themeDisplay, Theme theme, String resourcePath,
435                            String content)
436                    throws Exception {
437    
438                    Map<String, Object> inputObjects = new HashMap<String, Object>();
439    
440                    String portalWebDir = PortalUtil.getPortalWebDir();
441    
442                    inputObjects.put(
443                            "commonSassPath", portalWebDir.concat(_SASS_COMMON_DIR));
444    
445                    inputObjects.put("content", content);
446                    inputObjects.put("cssRealPath", resourcePath);
447                    inputObjects.put(
448                            "cssThemePath",
449                            _getCssThemePath(servletContext, request, themeDisplay, theme));
450    
451                    File sassTempDir = _getSassTempDir(servletContext);
452    
453                    inputObjects.put("sassCachePath", sassTempDir.getCanonicalPath());
454    
455                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
456                            new UnsyncByteArrayOutputStream();
457    
458                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
459                            unsyncByteArrayOutputStream);
460    
461                    inputObjects.put("out", unsyncPrintWriter);
462    
463                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
464    
465                    unsyncPrintWriter.flush();
466    
467                    return unsyncByteArrayOutputStream.toString();
468            }
469    
470            private static final String _CSS_IMPORT_BEGIN = "@import url(";
471    
472            private static final String _CSS_IMPORT_END = ");";
473    
474            private static final String _SASS_COMMON_DIR = "/html/css/common";
475    
476            private static final String _SASS_DIR = "sass";
477    
478            private static final String _SASS_DIR_KEY =
479                    DynamicCSSUtil.class.getName() + "#sass";
480    
481            private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
482    
483            private static boolean _initialized;
484            private static Pattern _pluginThemePattern = Pattern.compile(
485                    "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
486            private static Pattern _portalThemePattern = Pattern.compile(
487                    "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
488            private static RubyExecutor _rubyExecutor = new RubyExecutor();
489            private static String _rubyScript;
490    
491    }