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