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.service.ThemeLocalServiceUtil;
035    import com.liferay.portal.theme.ThemeDisplay;
036    import com.liferay.portal.tools.SassToCssBuilder;
037    import com.liferay.portal.util.ClassLoaderUtil;
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                                    ClassLoaderUtil.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                    if (Validator.isNotNull(themeId)) {
251                            try {
252                                    Theme theme = ThemeLocalServiceUtil.getTheme(
253                                            companyId, themeId, false);
254    
255                                    return theme;
256                            }
257                            catch (Exception e) {
258                                    _log.error(e, e);
259                            }
260                    }
261    
262                    String requestURI = URLDecoder.decode(
263                            request.getRequestURI(), StringPool.UTF8);
264    
265                    Matcher portalThemeMatcher = _portalThemePattern.matcher(requestURI);
266    
267                    if (portalThemeMatcher.find()) {
268                            String themePathId = portalThemeMatcher.group(1);
269    
270                            themePathId = StringUtil.replace(
271                                    themePathId, StringPool.UNDERLINE, StringPool.BLANK);
272    
273                            themeId = PortalUtil.getJsSafePortletId(themePathId);
274                    }
275                    else {
276                            Matcher pluginThemeMatcher = _pluginThemePattern.matcher(
277                                    requestURI);
278    
279                            if (pluginThemeMatcher.find()) {
280                                    String themePathId = pluginThemeMatcher.group(1);
281    
282                                    themePathId = StringUtil.replace(
283                                            themePathId, StringPool.UNDERLINE, StringPool.BLANK);
284    
285                                    StringBundler sb = new StringBundler(4);
286    
287                                    sb.append(themePathId);
288                                    sb.append(PortletConstants.WAR_SEPARATOR);
289                                    sb.append(themePathId);
290                                    sb.append("theme");
291    
292                                    themePathId = sb.toString();
293    
294                                    themeId = PortalUtil.getJsSafePortletId(themePathId);
295                            }
296                    }
297    
298                    if (Validator.isNull(themeId)) {
299                            return null;
300                    }
301    
302                    try {
303                            Theme theme = ThemeLocalServiceUtil.getTheme(
304                                    companyId, themeId, false);
305    
306                            return theme;
307                    }
308                    catch (Exception e) {
309                            _log.error(e, e);
310                    }
311    
312                    return null;
313            }
314    
315            private static String _getThemeImagesPath(
316                            HttpServletRequest request, ThemeDisplay themeDisplay, Theme theme)
317                    throws Exception {
318    
319                    String themeImagesPath = null;
320    
321                    if (themeDisplay != null) {
322                            themeImagesPath = themeDisplay.getPathThemeImages();
323                    }
324                    else {
325                            String cdnHost = PortalUtil.getCDNHost(request);
326                            String themeStaticResourcePath = theme.getStaticResourcePath();
327    
328                            themeImagesPath =
329                                    cdnHost + themeStaticResourcePath + theme.getImagesPath();
330                    }
331    
332                    return themeImagesPath;
333            }
334    
335            private static boolean _isThemeCssFastLoad(
336                    HttpServletRequest request, ThemeDisplay themeDisplay) {
337    
338                    if (themeDisplay != null) {
339                            return themeDisplay.isThemeCssFastLoad();
340                    }
341    
342                    return SessionParamUtil.getBoolean(
343                            request, "css_fast_load", PropsValues.THEME_CSS_FAST_LOAD);
344            }
345    
346            private static String _parseSass(
347                            ServletContext servletContext, HttpServletRequest request,
348                            ThemeDisplay themeDisplay, Theme theme, String resourcePath,
349                            String content)
350                    throws Exception {
351    
352                    Map<String, Object> inputObjects = new HashMap<String, Object>();
353    
354                    inputObjects.put("content", content);
355                    inputObjects.put("cssRealPath", resourcePath);
356                    inputObjects.put(
357                            "cssThemePath", _getCssThemePath(request, themeDisplay, theme));
358    
359                    File sassTempDir = _getSassTempDir(servletContext);
360    
361                    inputObjects.put("sassCachePath", sassTempDir.getCanonicalPath());
362    
363                    UnsyncByteArrayOutputStream unsyncByteArrayOutputStream =
364                            new UnsyncByteArrayOutputStream();
365    
366                    UnsyncPrintWriter unsyncPrintWriter = UnsyncPrintWriterPool.borrow(
367                            unsyncByteArrayOutputStream);
368    
369                    inputObjects.put("out", unsyncPrintWriter);
370    
371                    _rubyExecutor.eval(null, inputObjects, null, _rubyScript);
372    
373                    unsyncPrintWriter.flush();
374    
375                    return unsyncByteArrayOutputStream.toString();
376            }
377    
378            /**
379             * @see {@link AggregateFilter#aggregateCss(String, String)}
380             */
381            private static String propagateQueryString(
382                    String content, String queryString) {
383    
384                    StringBuilder sb = new StringBuilder(content.length());
385    
386                    int pos = 0;
387    
388                    while (true) {
389                            int importX = content.indexOf(_CSS_IMPORT_BEGIN, pos);
390                            int importY = content.indexOf(
391                                    _CSS_IMPORT_END, importX + _CSS_IMPORT_BEGIN.length());
392    
393                            if ((importX == -1) || (importY == -1)) {
394                                    sb.append(content.substring(pos));
395    
396                                    break;
397                            }
398    
399                            sb.append(content.substring(pos, importY));
400                            sb.append(CharPool.QUESTION);
401                            sb.append(queryString);
402                            sb.append(_CSS_IMPORT_END);
403    
404                            pos = importY + _CSS_IMPORT_END.length();
405                    }
406    
407                    return sb.toString();
408            }
409    
410            private static final String _CSS_IMPORT_BEGIN = "@import url(";
411    
412            private static final String _CSS_IMPORT_END = ");";
413    
414            private static final String _SASS_DIR = "sass";
415    
416            private static final String _SASS_DIR_KEY =
417                    DynamicCSSUtil.class.getName() + "#sass";
418    
419            private static Log _log = LogFactoryUtil.getLog(DynamicCSSUtil.class);
420    
421            private static Pattern _pluginThemePattern = Pattern.compile(
422                    "\\/([^\\/]+)-theme\\/", Pattern.CASE_INSENSITIVE);
423            private static Pattern _portalThemePattern = Pattern.compile(
424                    "themes\\/([^\\/]+)\\/css", Pattern.CASE_INSENSITIVE);
425            private static RubyExecutor _rubyExecutor = new RubyExecutor();
426            private static String _rubyScript;
427    
428    }