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