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.FileUtil;
023    import com.liferay.portal.kernel.util.ParamUtil;
024    import com.liferay.portal.kernel.util.SessionParamUtil;
025    import com.liferay.portal.kernel.util.StringBundler;
026    import com.liferay.portal.kernel.util.StringPool;
027    import com.liferay.portal.kernel.util.StringUtil;
028    import com.liferay.portal.kernel.util.SystemProperties;
029    import com.liferay.portal.kernel.util.UnsyncPrintWriterPool;
030    import com.liferay.portal.kernel.util.Validator;
031    import com.liferay.portal.kernel.util.WebKeys;
032    import com.liferay.portal.model.PortletConstants;
033    import com.liferay.portal.model.Theme;
034    import com.liferay.portal.scripting.ruby.RubyExecutor;
035    import com.liferay.portal.service.ThemeLocalServiceUtil;
036    import com.liferay.portal.theme.ThemeDisplay;
037    import com.liferay.portal.tools.SassToCssBuilder;
038    import com.liferay.portal.util.ClassLoaderUtil;
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                                    ClassLoaderUtil.getPortalClassLoader(),
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@", "@theme_image_path@"
156                            },
157                            new String[] {
158                                    PortalUtil.getPathContext(),
159                                    _getThemeImagesPath(request, themeDisplay, theme)
160                            });
161    
162                    return parsedContent;
163            }
164    
165            private static String _getCssThemePath(
166                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
167                    throws Exception {
168    
169                    String cssThemePath = null;
170    
171                    if (themeDisplay != null) {
172                            cssThemePath = themeDisplay.getPathThemeCss();
173                    }
174                    else {
175                            String cdnHost = StringPool.BLANK;
176    
177                            if (PortalUtil.isCDNDynamicResourcesEnabled(request)) {
178                                    cdnHost = PortalUtil.getCDNHost(request);
179                            }
180    
181                            String themeStaticResourcePath = theme.getStaticResourcePath();
182    
183                            cssThemePath =
184                                    cdnHost + themeStaticResourcePath + theme.getCssPath();
185                    }
186    
187                    return cssThemePath;
188            }
189    
190            private static Theme _getTheme(
191                    HttpServletRequest request, String cssRealPath) {
192    
193                    long companyId = PortalUtil.getCompanyId(request);
194    
195                    String themeId = ParamUtil.getString(request, "themeId");
196    
197                    Matcher portalThemeMatcher = _portalThemePattern.matcher(cssRealPath);
198    
199                    if (portalThemeMatcher.find()) {
200                            String themePathId = portalThemeMatcher.group(1);
201    
202                            themePathId = StringUtil.replace(
203                                    themePathId, StringPool.UNDERLINE, StringPool.BLANK);
204    
205                            themeId = PortalUtil.getJsSafePortletId(themePathId);
206                    }
207                    else {
208                            Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
209                                    cssRealPath);
210    
211                            if (pluginThemeMatcher.find()) {
212                                    String themePathId = pluginThemeMatcher.group(1);
213    
214                                    themePathId = StringUtil.replace(
215                                            themePathId, StringPool.UNDERLINE, StringPool.BLANK);
216    
217                                    StringBundler sb = new StringBundler(4);
218    
219                                    sb.append(themePathId);
220                                    sb.append(PortletConstants.WAR_SEPARATOR);
221                                    sb.append(themePathId);
222                                    sb.append("theme");
223    
224                                    themePathId = sb.toString();
225    
226                                    themeId = PortalUtil.getJsSafePortletId(themePathId);
227                            }
228                    }
229    
230                    if (Validator.isNull(themeId)) {
231                            return null;
232                    }
233    
234                    try {
235                            Theme theme = ThemeLocalServiceUtil.getTheme(
236                                    companyId, themeId, false);
237    
238                            return theme;
239                    }
240                    catch (Exception e) {
241                            _log.error(e, e);
242                    }
243    
244                    return null;
245            }
246    
247            private static String _getThemeImagesPath(
248                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
249                    throws Exception {
250    
251                    String themeImagesPath = null;
252    
253                    if (themeDisplay != null) {
254                            themeImagesPath = themeDisplay.getPathThemeImages();
255                    }
256                    else {
257                            String cdnHost = PortalUtil.getCDNHost(request);
258                            String themeStaticResourcePath = theme.getStaticResourcePath();
259    
260                            themeImagesPath =
261                                    cdnHost + themeStaticResourcePath + theme.getImagesPath();
262                    }
263    
264                    return themeImagesPath;
265            }
266    
267            private static boolean _isThemeCssFastLoad(
268                    HttpServletRequest request, ThemeDisplay themeDisplay) {
269    
270                    if (themeDisplay != null) {
271                            return themeDisplay.isThemeCssFastLoad();
272                    }
273    
274                    return SessionParamUtil.getBoolean(
275                            request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
276            }
277    
278            private static String _parseSass(
279                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme,
280                            String cssRealPath, String content)
281                    throws Exception {
282    
283                    Map<String, Object> inputObjects = new HashMap<String, Object>();
284    
285                    inputObjects.put("content", content);
286                    inputObjects.put("cssRealPath", cssRealPath);
287                    inputObjects.put(
288                            "cssThemePath", _getCssThemePath(request, themeDisplay, theme));
289                    inputObjects.put("sassCachePath", _SASS_DIR);
290    
291                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
292                            new UnsyncByteArrayOutputStream();
293    
294                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
295                            unsyncByteArrayOutputStream);
296    
297                    inputObjects.put("out", unsyncPrintWriter);
298    
299                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
300    
301                    unsyncPrintWriter.flush();
302    
303                    return unsyncByteArrayOutputStream.toString();
304            }
305    
306            /**
307             * @see {@link MinifierFilter#aggregateCss(String, String)}
308             */
309            private static String _propagateQueryString(
310                    String content, String queryString) {
311    
312                    StringBuilder sb = new StringBuilder(content.length());
313    
314                    int pos = 0;
315    
316                    while (true) {
317                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
318                            int importY = content.indexOf(
319                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
320    
321                            if ((importX == -1) || (importY == -1)) {
322                                    sb.append(content.substring(pos));
323    
324                                    break;
325                            }
326                            else {
327                                    sb.append(content.substring(pos, importY));
328                                    sb.append(CharPool.QUESTION);
329                                    sb.append(queryString);
330                                    sb.append(_CSS_IMPORT_END);
331    
332                                    pos = importY + _CSS_IMPORT_END.length();
333                            }
334                    }
335    
336                    return sb.toString();
337            }
338    
339            private static final String _CSS_IMPORT_BEGIN = "@import url(";
340    
341            private static final String _CSS_IMPORT_END = ");";
342    
343            private static final String _SASS_DIR =
344                    SystemProperties.get(SystemProperties.TMP_DIR) + "/liferay/sass";
345    
346            private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
347    
348            private static Pattern _pluginThemePattern = Pattern.compile(
349                    "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
350            private static Pattern _portalThemePattern = Pattern.compile(
351                    "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
352            private static RubyExecutor _rubyExecutor = new RubyExecutor();
353            private static String _rubyScript;
354    
355    }