001    /**
002     * Copyright (c) 2000-2011 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.FileUtil;
023    import com.liferay.portal.kernel.util.ParamUtil;
024    import com.liferay.portal.kernel.util.PortalClassLoaderUtil;
025    import com.liferay.portal.kernel.util.SessionParamUtil;
026    import com.liferay.portal.kernel.util.StringBundler;
027    import com.liferay.portal.kernel.util.StringPool;
028    import com.liferay.portal.kernel.util.StringUtil;
029    import com.liferay.portal.kernel.util.SystemProperties;
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.PortalUtil;
040    import com.liferay.portal.util.PropsValues;
041    
042    import java.io.File;
043    
044    import java.util.HashMap;
045    import java.util.Map;
046    import java.util.regex.Matcher;
047    import java.util.regex.Pattern;
048    
049    import javax.servlet.http.HttpServletRequest;
050    
051    import org.apache.commons.lang.time.StopWatch;
052    
053    /**
054     * @author Raymond Augé
055     * @author Sergio Sánchez
056     */
057    public class DynamicCSSUtil {
058    
059            public static void init() {
060                    try {
061                            _rubyScript = StringUtil.read(
062                                    PortalClassLoaderUtil.getClassLoader(),
063                                    "com/liferay/portal/servlet/filters/dynamiccss/main.rb");
064                    }
065                    catch (Exception e) {
066                            _log.error(e, e);
067                    }
068            }
069    
070            public static String parseSass(
071                            HttpServletRequest request, String cssRealPath, String content)
072                    throws Exception {
073    
074                    if (!DynamicCSSFilter.ENABLED) {
075                            return content;
076                    }
077    
078                    StopWatch stopWatch = null;
079    
080                    if (_log.isDebugEnabled()) {
081                            stopWatch = new StopWatch();
082    
083                            stopWatch.start();
084                    }
085    
086                    // Request will only be null when called by StripFilterTest
087    
088                    if (request == null) {
089                            return content;
090                    }
091    
092                    ThemeDisplay themeDisplay = (ThemeDisplay)request.getAttribute(
093                            WebKeys.THEME_DISPLAY);
094    
095                    Theme theme = null;
096    
097                    if (themeDisplay == null) {
098                            theme = _getTheme(request, cssRealPath);
099    
100                            if (theme == null) {
101                                    String currentURL = PortalUtil.getCurrentURL(request);
102    
103                                    if (_log.isWarnEnabled()) {
104                                            _log.warn("No theme found for " + currentURL);
105                                    }
106    
107                                    return content;
108                            }
109                    }
110    
111                    String parsedContent = null;
112    
113                    boolean themeCssFastLoad = _isThemeCssFastLoad(request, themeDisplay);
114    
115                    File cssRealFile = new File(cssRealPath);
116                    File cacheCssRealFile = SassToCssBuilder.getCacheFile(cssRealPath);
117    
118                    if (themeCssFastLoad && cacheCssRealFile.exists() &&
119                            (cacheCssRealFile.lastModified() == cssRealFile.lastModified())) {
120    
121                            parsedContent = FileUtil.read(cacheCssRealFile);
122    
123                            if (_log.isDebugEnabled()) {
124                                    _log.debug(
125                                            "Loading SASS cache from " + cacheCssRealFile + " takes " +
126                                                    stopWatch.getTime() + " ms");
127                            }
128                    }
129                    else {
130                            content = SassToCssBuilder.parseStaticTokens(content);
131    
132                            String queryString = request.getQueryString();
133    
134                            if (!themeCssFastLoad && Validator.isNotNull(queryString)) {
135                                    content = _propagateQueryString(content, queryString);
136                            }
137    
138                            parsedContent = _parseSass(
139                                    request, themeDisplay, theme, cssRealPath, content);
140    
141                            if (_log.isDebugEnabled()) {
142                                    _log.debug(
143                                            "Parsing SASS for " + cssRealPath + " takes " +
144                                                    stopWatch.getTime() + " ms");
145                            }
146                    }
147    
148                    if (Validator.isNull(parsedContent)) {
149                            return content;
150                    }
151    
152                    parsedContent = StringUtil.replace(
153                            parsedContent,
154                            new String[] {
155                                    "@portal_ctx@",
156                                    "@theme_image_path@"
157                            },
158                            new String[] {
159                                    PortalUtil.getPathContext(),
160                                    _getThemeImagesPath(request, themeDisplay, theme)
161                            });
162    
163                    return parsedContent;
164            }
165    
166            private static String _getCssThemePath(
167                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
168                    throws Exception {
169    
170                    String cssThemePath = null;
171    
172                    if (themeDisplay != null) {
173                            cssThemePath = themeDisplay.getPathThemeCss();
174                    }
175                    else {
176                            String cdnHost = PortalUtil.getCDNHost(request);
177                            String themeStaticResourcePath = theme.getStaticResourcePath();
178    
179                            cssThemePath =
180                                    cdnHost + themeStaticResourcePath + theme.getCssPath();
181                    }
182    
183                    return cssThemePath;
184            }
185    
186            private static Theme _getTheme(
187                    HttpServletRequest request, String cssRealPath) {
188    
189                    long companyId = PortalUtil.getCompanyId(request);
190    
191                    String themeId = ParamUtil.getString(request, "themeId");
192    
193                    Matcher portalThemeMatcher = _portalThemePattern.matcher(cssRealPath);
194    
195                    if (portalThemeMatcher.find()) {
196                            String themePathId = portalThemeMatcher.group(1);
197    
198                            themePathId = StringUtil.replace(
199                                    themePathId, StringPool.UNDERLINE, StringPool.BLANK);
200    
201                            themeId = PortalUtil.getJsSafePortletId(themePathId);
202                    }
203                    else {
204                            Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
205                                    cssRealPath);
206    
207                            if (pluginThemeMatcher.find()) {
208                                    String themePathId = pluginThemeMatcher.group(1);
209    
210                                    themePathId = StringUtil.replace(
211                                            themePathId, StringPool.UNDERLINE, StringPool.BLANK);
212    
213                                    StringBundler sb = new StringBundler(4);
214    
215                                    sb.append(themePathId);
216                                    sb.append(PortletConstants.WAR_SEPARATOR);
217                                    sb.append(themePathId);
218                                    sb.append("theme");
219    
220                                    themePathId = sb.toString();
221    
222                                    themeId = PortalUtil.getJsSafePortletId(themePathId);
223                            }
224                    }
225    
226                    if (Validator.isNull(themeId)) {
227                            return null;
228                    }
229    
230                    try {
231                            Theme theme = ThemeLocalServiceUtil.getTheme(
232                                    companyId, themeId, false);
233    
234                            return theme;
235                    }
236                    catch (Exception e) {
237                            _log.error(e, e);
238                    }
239    
240                    return null;
241            }
242    
243            private static String _getThemeImagesPath(
244                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
245                    throws Exception {
246    
247                    String themeImagesPath = null;
248    
249                    if (themeDisplay != null) {
250                            themeImagesPath = themeDisplay.getPathThemeImages();
251                    }
252                    else {
253                            String cdnHost = PortalUtil.getCDNHost(request);
254                            String themeStaticResourcePath = theme.getStaticResourcePath();
255    
256                            themeImagesPath =
257                                    cdnHost + themeStaticResourcePath + theme.getImagesPath();
258                    }
259    
260                    return themeImagesPath;
261            }
262    
263            private static boolean _isThemeCssFastLoad(
264                    HttpServletRequest request, ThemeDisplay themeDisplay) {
265    
266                    if (themeDisplay != null) {
267                            return themeDisplay.isThemeCssFastLoad();
268                    }
269    
270                    return SessionParamUtil.getBoolean(
271                            request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
272            }
273    
274            private static String _parseSass(
275                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme,
276                            String cssRealPath, String content)
277                    throws Exception {
278    
279                    Map<String, Object> inputObjects = new HashMap<String, Object>();
280    
281                    inputObjects.put("content", content);
282                    inputObjects.put("cssRealPath", cssRealPath);
283                    inputObjects.put(
284                            "cssThemePath", _getCssThemePath(request, themeDisplay, theme));
285                    inputObjects.put("sassCachePath", _SASS_DIR);
286    
287                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
288                            new UnsyncByteArrayOutputStream();
289    
290                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
291                            unsyncByteArrayOutputStream);
292    
293                    inputObjects.put("out", unsyncPrintWriter);
294    
295                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
296    
297                    unsyncPrintWriter.flush();
298    
299                    return unsyncByteArrayOutputStream.toString();
300            }
301    
302            /**
303             * @see {@link MinifierFilter#aggregateCss(String, String)}
304             */
305            private static String _propagateQueryString(
306                    String content, String queryString) {
307    
308                    StringBuilder sb = new StringBuilder(content.length());
309    
310                    int pos = 0;
311    
312                    while (true) {
313                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
314                            int importY = content.indexOf(
315                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
316    
317                            if ((importX == -1) || (importY == -1)) {
318                                    sb.append(content.substring(pos, content.length()));
319    
320                                    break;
321                            }
322                            else {
323                                    sb.append(content.substring(pos, importY));
324                                    sb.append(CharPool.QUESTION);
325                                    sb.append(queryString);
326                                    sb.append(_CSS_IMPORT_END);
327    
328                                    pos = importY + _CSS_IMPORT_END.length();
329                            }
330                    }
331    
332                    return sb.toString();
333            }
334    
335            private static final String _CSS_IMPORT_BEGIN = "@import url(";
336    
337            private static final String _CSS_IMPORT_END = ");";
338    
339            private static final String _SASS_DIR =
340                    SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/sass";
341    
342            private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
343    
344            private static Pattern _pluginThemePattern =
345                    Pattern.compile("\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
346            private static Pattern _portalThemePattern =
347                    Pattern.compile("themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
348            private static RubyExecutor _rubyExecutor = new RubyExecutor();
349            private static String _rubyScript;
350    
351    }